From 32a593b19bd449ebea4afa9b8cc650acead24075 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Thu, 1 Dec 2011 10:02:38 +0000 Subject: [PATCH 0001/2357] Initial commit --- LICENSE | 31 ++++++++++++ Setup.hs | 2 + network-transport.cabal | 21 +++++++++ src/Network/Transport.hs | 89 +++++++++++++++++++++++++++++++++++ src/Network/Transport/MVar.hs | 59 +++++++++++++++++++++++ src/Network/Transport/TCP.hs | 28 +++++++++++ 6 files changed, 230 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 network-transport.cabal create mode 100644 src/Network/Transport.hs create mode 100644 src/Network/Transport/MVar.hs create mode 100644 src/Network/Transport/TCP.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bbc98067 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/network-transport.cabal b/network-transport.cabal new file mode 100644 index 00000000..032855a0 --- /dev/null +++ b/network-transport.cabal @@ -0,0 +1,21 @@ +Name: network-transport +Version: 0.0.1 +Description: Network transport interface +License: BSD3 +License-file: LICENSE +Author: Duncan Coutts, Nicolas Wu +Maintainer: duncan@well-typed.com +Homepage: http://github.com/haskell-distributed/ +Build-Type: Simple +Cabal-Version: >=1.2 + +Library + Build-Depends: base >= 3 && < 5, + bytestring >= 0.9, + containers >= 0.4 + Exposed-modules: Network.Transport, + Network.Transport.MVar, + Network.Transport.TCP + Extensions: BangPatterns + ghc-options: -Wall + HS-Source-Dirs: src diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs new file mode 100644 index 00000000..d0d23b3b --- /dev/null +++ b/src/Network/Transport.hs @@ -0,0 +1,89 @@ +module Network.Transport + ( Transport (..) + , SendAddr (..) + , SendEnd (..) + , ReceiveEnd (..) + , Hints (..) + , defaultHints + , SendHints (..) + , defaultSendHints + , newConnection + , newMulticast + , connect + ) where + +import Data.ByteString.Char8 (ByteString) + +------------------------ +-- Transport interface +-- + +-- Buffer size +-- Sending: eager or buffered +-- Big record of defaults +data Hints = Hints +data SendHints = SendHints + +data Transport = Transport + { newConnectionWith :: Hints -> IO (SendAddr, ReceiveEnd) + , newMulticastWith :: Hints -> IO (MulticastSendEnd, MulticastReceiveAddr) + , deserialize :: ByteString -> Maybe SendAddr + } + +newConnection :: Transport -> IO (SendAddr, ReceiveEnd) +newConnection transport = newConnectionWith transport defaultHints + +newMulticast :: Transport -> IO (MulticastSendEnd, MulticastReceiveAddr) +newMulticast transport = newMulticastWith transport defaultHints + +defaultHints :: Hints +defaultHints = Hints + +data SendAddr = SendAddr + { connectWith :: SendHints -> IO SendEnd + , serialize :: ByteString + } + +connect :: SendAddr -> IO SendEnd +connect sendAddr = connectWith sendAddr defaultSendHints + +defaultSendHints :: SendHints +defaultSendHints = SendHints + +-- Send and receive are vectored +data SendEnd = SendEnd + { send :: [ByteString] -> IO () + -- , sendAddress :: SendAddr + } + +newtype ReceiveEnd = ReceiveEnd + { receive :: IO [ByteString] + -- , receiveAddress :: SendAddr + } + +data MulticastSendEnd = MulticastSendEnd + { multicastSend :: ByteString -> IO () + } + +data MulticastReceiveAddr = MulticastReceiveAddr + { multicastConnect :: IO MulticastReceiveEnd + } + +data MulticastReceiveEnd = MulticastReceiveEnd + { multicastReceive :: IO ByteString + } + +-- data UnorderedSendEnd -- this is reliable +-- data UnreliableSendEnd -- this is also unordered +-- +-- multicast is alwaysw unordered and unreliable + +-- TODO: +-- * Multicast +-- * Dual of the Transport: one to many, rather than many to one +-- * Optional: not supported by all transports +-- * Different transport types +-- * Unreliable +-- * Unordered +-- * Send / receive should be vectored + diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs new file mode 100644 index 00000000..78da47ca --- /dev/null +++ b/src/Network/Transport/MVar.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE BangPatterns #-} + +module Network.Transport.MVar + ( mkTransport + ) where + +import Control.Concurrent.MVar +import Data.IntMap (IntMap) +import Data.ByteString.Char8 (ByteString) + +import qualified Data.IntMap as IntMap +import qualified Data.ByteString.Char8 as BS + +import Network.Transport + +type Chans = MVar (Int, IntMap (MVar [ByteString])) + +mkTransport :: IO Transport +mkTransport = do + channels <- newMVar (0, IntMap.empty) + return Transport + { newConnectionWith = \_ -> do + (i, m) <- takeMVar channels + receiveChan <- newEmptyMVar + let sendAddr = i + !i' = i+1 + !m' = IntMap.insert i receiveChan m + putMVar channels (i', m') + return (mkSendAddr channels sendAddr, mkReceiveEnd receiveChan) + , newMulticastWith = undefined + , deserialize = \bs -> + case BS.readInt bs of + Nothing -> error "dummyBackend.deserializeSendEnd: cannot parse" + Just (n,_) -> Just . mkSendAddr channels $ n + } + where + mkSendAddr :: Chans -> Int -> SendAddr + mkSendAddr channels addr = SendAddr + { connectWith = \_ -> mkSendEnd channels $ addr + , serialize = BS.pack (show addr) + } + -- mkSendEnd channels sendAddr + + mkSendEnd :: Chans -> Int -> IO SendEnd + mkSendEnd channels addr = do + (_, m) <- readMVar channels + case IntMap.lookup addr m of + Nothing -> fail "dummyBackend.send: bad send address" + Just chan -> return SendEnd + { send = realSend chan + } + + mkReceiveEnd :: MVar [ByteString] -> ReceiveEnd + mkReceiveEnd chan = ReceiveEnd + { receive = takeMVar chan + } + + realSend :: MVar [ByteString] -> [ByteString] -> IO () + realSend = putMVar diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs new file mode 100644 index 00000000..1bceca3c --- /dev/null +++ b/src/Network/Transport/TCP.hs @@ -0,0 +1,28 @@ +module Network.Transport.TCP + ( mkTransport + ) where + +import Network.Transport +import Data.Word + +data Address = Address String +type Port = Word16 + +-- | This deals with several different configuration properties: +-- * Buffer size, specified in Hints +-- * LAN/WAN, since we can inspect the addresses +data TCPConfig = TCPConfig Hints Port Address Address + +mkTransport :: TCPConfig -> IO Transport +mkTransport tcpConfig = + -- create buffer + -- allocate listening socket + -- fork thread on that socket + return Transport + { newConnectionWith = undefined + -- create listening mailbox + -- send back mailbox details + , newMulticastWith = undefined + , deserialize = undefined + } + From b0937faef891f455a1647a0bd33b4fa660db09e0 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Wed, 14 Dec 2011 10:34:02 +0000 Subject: [PATCH 0002/2357] Fix network-transport.cabal. --- network-transport.cabal | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/network-transport.cabal b/network-transport.cabal index 032855a0..fc13ed9d 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -12,7 +12,9 @@ Cabal-Version: >=1.2 Library Build-Depends: base >= 3 && < 5, bytestring >= 0.9, - containers >= 0.4 + containers >= 0.4, + network >= 2.3, + safe >= 0.3 Exposed-modules: Network.Transport, Network.Transport.MVar, Network.Transport.TCP From 4477485e0d907f2c51b5259ff2a0ea2460f8954e Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Wed, 14 Dec 2011 10:36:03 +0000 Subject: [PATCH 0003/2357] Add TCP transport implementation. --- src/Network/Transport/TCP.hs | 108 +++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 11 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 1bceca3c..29cb8f33 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -1,28 +1,114 @@ module Network.Transport.TCP ( mkTransport + , TCPConfig (..) ) where import Network.Transport + +import Control.Concurrent (forkIO) +import Control.Concurrent.Chan +import Control.Concurrent.MVar +import Control.Monad (forever, unless) +import Data.ByteString.Char8 (ByteString) +import Data.IntMap (IntMap) import Data.Word +import Network.Socket +import Safe + +import qualified Data.ByteString.Char8 as BS +import qualified Data.IntMap as IntMap +import qualified Network.Socket as N +import qualified Network.Socket.ByteString as NBS -data Address = Address String -type Port = Word16 +type Chans = MVar (Int, IntMap (Chan ByteString)) +type ChanId = Int -- | This deals with several different configuration properties: -- * Buffer size, specified in Hints -- * LAN/WAN, since we can inspect the addresses -data TCPConfig = TCPConfig Hints Port Address Address +-- Note that HostName could be an IP address, and ServiceName could be +-- a port number +data TCPConfig = TCPConfig Hints HostName ServiceName +-- | This creates a TCP connection between a server and a number of +-- clients. Behind the scenes, the server hostname is passed as the SendAddr +-- and when a connection is made, messages sent down the SendEnd go +-- via a socket made for the client that connected. +-- Messages are all queued using an unbounded Chan. mkTransport :: TCPConfig -> IO Transport -mkTransport tcpConfig = - -- create buffer - -- allocate listening socket - -- fork thread on that socket +mkTransport (TCPConfig _hints host service) = withSocketsDo $ do + channels <- newMVar (0, IntMap.empty) + serverAddrs <- getAddrInfo + (Just (N.defaultHints { addrFlags = [AI_PASSIVE] })) + Nothing + (Just service) + let serverAddr = case serverAddrs of + [] -> error "mkTransport: getAddrInfo returned []" + as -> head as + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + bindSocket sock (addrAddress serverAddr) + listen sock 5 + forkIO $ procConnections channels sock + return Transport - { newConnectionWith = undefined - -- create listening mailbox - -- send back mailbox details + { newConnectionWith = \_ -> do + (chanId, chanMap) <- takeMVar channels + chan <- newChan + putMVar channels (chanId + 1, IntMap.insert chanId chan chanMap) + return (mkSendAddr host service chanId, mkReceiveEnd chan) , newMulticastWith = undefined - , deserialize = undefined + , deserialize = \bs -> + case readMay . BS.unpack $ bs of + Nothing -> error "deserialize: cannot parse" + Just (host, service, chanId) -> Just $ mkSendAddr host service chanId } + where + mkSendAddr :: HostName -> ServiceName -> ChanId -> SendAddr + mkSendAddr host service chanId = SendAddr + { connectWith = \_ -> mkSendEnd chanId + , serialize = BS.pack . show $ (host, service, chanId) + } + + mkSendEnd :: ChanId -> IO SendEnd + mkSendEnd chanId = withSocketsDo $ do + serverAddrs <- getAddrInfo Nothing (Just host) (Just service) + let serverAddr = case serverAddrs of + [] -> error "mkSendEnd: getAddrInfo returned []" + as -> head as + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock KeepAlive 1 + N.connect sock (addrAddress serverAddr) + NBS.sendAll sock $ BS.pack . show $ chanId + return $ SendEnd + { Network.Transport.send = \bss -> NBS.sendMany sock bss + } + + mkReceiveEnd :: Chan ByteString -> ReceiveEnd + mkReceiveEnd chan = ReceiveEnd + { -- for now we will implement this as a Chan + receive = do + bs <- readChan chan + return [bs] + } + + procConnections :: Chans -> Socket -> IO () + procConnections chans sock = forever $ do + (clientSock, _clientAddr) <- accept sock + -- decode the first message to find the correct chanId + bs <- NBS.recv clientSock 4096 + case BS.readInt bs of + Nothing -> error "procConnections: cannot parse chanId" + Just (chanId, bs') -> do + -- lookup the channel + (_, chanMap) <- readMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> error "procConnections: cannot find chanId" + Just chan -> forkIO $ do + unless (BS.null bs') $ writeChan chan bs' + procMessages chan clientSock + + procMessages :: Chan ByteString -> Socket -> IO () + procMessages chan sock = forever $ do + bs <- NBS.recv sock 4096 + writeChan chan bs \ No newline at end of file From 7ac245db0fc3b6116925db9c293f1fc67d18f725 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Wed, 14 Dec 2011 10:44:00 +0000 Subject: [PATCH 0004/2357] Remove useless comment. --- src/Network/Transport/MVar.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs index 78da47ca..5eb4863a 100644 --- a/src/Network/Transport/MVar.hs +++ b/src/Network/Transport/MVar.hs @@ -36,10 +36,9 @@ mkTransport = do where mkSendAddr :: Chans -> Int -> SendAddr mkSendAddr channels addr = SendAddr - { connectWith = \_ -> mkSendEnd channels $ addr + { connectWith = \_ -> mkSendEnd channels addr , serialize = BS.pack (show addr) } - -- mkSendEnd channels sendAddr mkSendEnd :: Chans -> Int -> IO SendEnd mkSendEnd channels addr = do From 67511dc131b7f9a56e271e2ac5e6e8a4f4aa5b7a Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Tue, 3 Jan 2012 15:35:33 +0000 Subject: [PATCH 0005/2357] Add documentation about Transports. --- src/Network/Transport.hs | 75 ++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index d0d23b3b..2506ee86 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -1,89 +1,96 @@ module Network.Transport - ( Transport (..) + ( Hints (..) + , ReceiveEnd (..) , SendAddr (..) , SendEnd (..) - , ReceiveEnd (..) - , Hints (..) - , defaultHints , SendHints (..) + , Transport (..) + , connect + , defaultHints , defaultSendHints , newConnection , newMulticast - , connect ) where import Data.ByteString.Char8 (ByteString) ------------------------- --- Transport interface --- - --- Buffer size --- Sending: eager or buffered --- Big record of defaults +-- | The `Hints` and `SendHints` provide hints to the underlying transport +-- about the kind of connection that is required. This might include details +-- such as whether the connection is eager or buffered, and the buffer size. data Hints = Hints data SendHints = SendHints +-- | A `Transport` encapsulates the functions required to establish many-to-one +-- and one-to-many connections between clients and servers. +-- The `newConnectionWith` function creates a `ReceiveEnd` that listens to +-- messages sent using the corresponding `SendAddr`. This connection is +-- established using a `Hints` value, which provides information about the +-- connection topology. +-- Each `SendAddr` can be serialised into a `ByteString`, and the `deserialize` +-- function converts this back into a `SendAddr`. +-- Note that these connections provide reliable and ordered messages. data Transport = Transport { newConnectionWith :: Hints -> IO (SendAddr, ReceiveEnd) , newMulticastWith :: Hints -> IO (MulticastSendEnd, MulticastReceiveAddr) , deserialize :: ByteString -> Maybe SendAddr } +-- | This is a convenience function that creates a new connection on a transport +-- using the default hints. newConnection :: Transport -> IO (SendAddr, ReceiveEnd) newConnection transport = newConnectionWith transport defaultHints newMulticast :: Transport -> IO (MulticastSendEnd, MulticastReceiveAddr) newMulticast transport = newMulticastWith transport defaultHints +-- | The default `Hints` for establishing a new transport connection. defaultHints :: Hints defaultHints = Hints +-- | A `SendAddr` is an address that corresponds to a listening `ReceiveEnd` +-- initially created using `newConnection`. A `SendAddr` can be shared between +-- clients by using `serialize`, and passing the resulting `ByteString`. +-- Given a `SendAddr`, the `connectWith` function creates a `SendEnd` which +-- can be used to send messages. data SendAddr = SendAddr { connectWith :: SendHints -> IO SendEnd , serialize :: ByteString } +-- | This is a convenience function that connects with a given `SendAddr` using +-- the default hints for sending. connect :: SendAddr -> IO SendEnd connect sendAddr = connectWith sendAddr defaultSendHints +-- | The default `SendHints` for establishing a `SendEnd`. defaultSendHints :: SendHints defaultSendHints = SendHints --- Send and receive are vectored -data SendEnd = SendEnd - { send :: [ByteString] -> IO () - -- , sendAddress :: SendAddr +-- | A `SendEnd` provides a `send` function that allows vectored messages +-- to be sent to the corresponding `ReceiveEnd`. +newtype SendEnd = SendEnd + { send :: [ByteString] -> IO () } +-- | A `ReceiveEnd` provides a `receive` function that allows messages +-- to be received from the corresponding `SendEnd`s. newtype ReceiveEnd = ReceiveEnd - { receive :: IO [ByteString] - -- , receiveAddress :: SendAddr + { receive :: IO [ByteString] } -data MulticastSendEnd = MulticastSendEnd +newtype MulticastSendEnd = MulticastSendEnd { multicastSend :: ByteString -> IO () } -data MulticastReceiveAddr = MulticastReceiveAddr +newtype MulticastReceiveAddr = MulticastReceiveAddr { multicastConnect :: IO MulticastReceiveEnd } -data MulticastReceiveEnd = MulticastReceiveEnd +newtype MulticastReceiveEnd = MulticastReceiveEnd { multicastReceive :: IO ByteString } --- data UnorderedSendEnd -- this is reliable --- data UnreliableSendEnd -- this is also unordered --- --- multicast is alwaysw unordered and unreliable - --- TODO: --- * Multicast --- * Dual of the Transport: one to many, rather than many to one --- * Optional: not supported by all transports --- * Different transport types --- * Unreliable --- * Unordered --- * Send / receive should be vectored +-- TODO: Other SendEnds that might be of use: +-- data UnorderedSendEnd -- reliable, unordered +-- data UnreliableSendEnd -- unreliable, unordered From 0333b79b62bb9c63269fa11b6757d2258d99fda2 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Tue, 10 Jan 2012 17:49:40 +0000 Subject: [PATCH 0006/2357] Fix mkSendEnd to connect to correct host and service. --- src/Network/Transport/TCP.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 29cb8f33..8dbd392c 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -66,12 +66,12 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do where mkSendAddr :: HostName -> ServiceName -> ChanId -> SendAddr mkSendAddr host service chanId = SendAddr - { connectWith = \_ -> mkSendEnd chanId + { connectWith = \_ -> mkSendEnd host service chanId , serialize = BS.pack . show $ (host, service, chanId) } - mkSendEnd :: ChanId -> IO SendEnd - mkSendEnd chanId = withSocketsDo $ do + mkSendEnd :: HostName -> ServiceName -> ChanId -> IO SendEnd + mkSendEnd host service chanId = withSocketsDo $ do serverAddrs <- getAddrInfo Nothing (Just host) (Just service) let serverAddr = case serverAddrs of [] -> error "mkSendEnd: getAddrInfo returned []" @@ -111,4 +111,4 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do procMessages :: Chan ByteString -> Socket -> IO () procMessages chan sock = forever $ do bs <- NBS.recv sock 4096 - writeChan chan bs \ No newline at end of file + writeChan chan bs From f6fd2fb0aa50cb49fcf98a887db56ad391102bf4 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Wed, 11 Jan 2012 13:17:36 +0000 Subject: [PATCH 0007/2357] Replace SendEnd/SourceEnd ReceiveEnd/TargetEnd. --- src/Network/Transport.hs | 82 +++++++++++++++++------------------ src/Network/Transport/MVar.hs | 24 +++++----- src/Network/Transport/TCP.hs | 28 ++++++------ 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 2506ee86..cc26ec28 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -1,96 +1,96 @@ module Network.Transport ( Hints (..) - , ReceiveEnd (..) - , SendAddr (..) - , SendEnd (..) - , SendHints (..) + , TargetEnd (..) + , SourceAddr (..) + , SourceEnd (..) + , SourceHints (..) , Transport (..) , connect , defaultHints - , defaultSendHints + , defaultSourceHints , newConnection , newMulticast ) where import Data.ByteString.Char8 (ByteString) --- | The `Hints` and `SendHints` provide hints to the underlying transport +-- | The `Hints` and `SourceHints` provide hints to the underlying transport -- about the kind of connection that is required. This might include details -- such as whether the connection is eager or buffered, and the buffer size. data Hints = Hints -data SendHints = SendHints +data SourceHints = SourceHints -- | A `Transport` encapsulates the functions required to establish many-to-one -- and one-to-many connections between clients and servers. --- The `newConnectionWith` function creates a `ReceiveEnd` that listens to --- messages sent using the corresponding `SendAddr`. This connection is +-- The `newConnectionWith` function creates a `TargetEnd` that listens to +-- messages sent using the corresponding `SourceAddr`. This connection is -- established using a `Hints` value, which provides information about the -- connection topology. --- Each `SendAddr` can be serialised into a `ByteString`, and the `deserialize` --- function converts this back into a `SendAddr`. +-- Each `SourceAddr` can be serialised into a `ByteString`, and the `deserialize` +-- function converts this back into a `SourceAddr`. -- Note that these connections provide reliable and ordered messages. data Transport = Transport - { newConnectionWith :: Hints -> IO (SendAddr, ReceiveEnd) - , newMulticastWith :: Hints -> IO (MulticastSendEnd, MulticastReceiveAddr) - , deserialize :: ByteString -> Maybe SendAddr + { newConnectionWith :: Hints -> IO (SourceAddr, TargetEnd) + , newMulticastWith :: Hints -> IO (MulticastSourceEnd, MulticastTargetAddr) + , deserialize :: ByteString -> Maybe SourceAddr } -- | This is a convenience function that creates a new connection on a transport -- using the default hints. -newConnection :: Transport -> IO (SendAddr, ReceiveEnd) +newConnection :: Transport -> IO (SourceAddr, TargetEnd) newConnection transport = newConnectionWith transport defaultHints -newMulticast :: Transport -> IO (MulticastSendEnd, MulticastReceiveAddr) +newMulticast :: Transport -> IO (MulticastSourceEnd, MulticastTargetAddr) newMulticast transport = newMulticastWith transport defaultHints -- | The default `Hints` for establishing a new transport connection. defaultHints :: Hints defaultHints = Hints --- | A `SendAddr` is an address that corresponds to a listening `ReceiveEnd` --- initially created using `newConnection`. A `SendAddr` can be shared between +-- | A `SourceAddr` is an address that corresponds to a listening `TargetEnd` +-- initially created using `newConnection`. A `SourceAddr` can be shared between -- clients by using `serialize`, and passing the resulting `ByteString`. --- Given a `SendAddr`, the `connectWith` function creates a `SendEnd` which +-- Given a `SourceAddr`, the `connectWith` function creates a `SourceEnd` which -- can be used to send messages. -data SendAddr = SendAddr - { connectWith :: SendHints -> IO SendEnd +data SourceAddr = SourceAddr + { connectWith :: SourceHints -> IO SourceEnd , serialize :: ByteString } --- | This is a convenience function that connects with a given `SendAddr` using +-- | This is a convenience function that connects with a given `SourceAddr` using -- the default hints for sending. -connect :: SendAddr -> IO SendEnd -connect sendAddr = connectWith sendAddr defaultSendHints +connect :: SourceAddr -> IO SourceEnd +connect sourceAddr = connectWith sourceAddr defaultSourceHints --- | The default `SendHints` for establishing a `SendEnd`. -defaultSendHints :: SendHints -defaultSendHints = SendHints +-- | The default `SourceHints` for establishing a `SourceEnd`. +defaultSourceHints :: SourceHints +defaultSourceHints = SourceHints --- | A `SendEnd` provides a `send` function that allows vectored messages --- to be sent to the corresponding `ReceiveEnd`. -newtype SendEnd = SendEnd +-- | A `SourceEnd` provides a `send` function that allows vectored messages +-- to be sent to the corresponding `TargetEnd`. +newtype SourceEnd = SourceEnd { send :: [ByteString] -> IO () } --- | A `ReceiveEnd` provides a `receive` function that allows messages --- to be received from the corresponding `SendEnd`s. -newtype ReceiveEnd = ReceiveEnd +-- | A `TargetEnd` provides a `receive` function that allows messages +-- to be received from the corresponding `SourceEnd`s. +newtype TargetEnd = TargetEnd { receive :: IO [ByteString] } -newtype MulticastSendEnd = MulticastSendEnd - { multicastSend :: ByteString -> IO () +newtype MulticastSourceEnd = MulticastSourceEnd + { multicastSource :: ByteString -> IO () } -newtype MulticastReceiveAddr = MulticastReceiveAddr - { multicastConnect :: IO MulticastReceiveEnd +newtype MulticastTargetAddr = MulticastTargetAddr + { multicastConnect :: IO MulticastTargetEnd } -newtype MulticastReceiveEnd = MulticastReceiveEnd +newtype MulticastTargetEnd = MulticastTargetEnd { multicastReceive :: IO ByteString } --- TODO: Other SendEnds that might be of use: --- data UnorderedSendEnd -- reliable, unordered --- data UnreliableSendEnd -- unreliable, unordered +-- TODO: Other SourceEnds that might be of use: +-- data UnorderedSourceEnd -- reliable, unordered +-- data UnreliableSourceEnd -- unreliable, unordered diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs index 5eb4863a..a416c0d1 100644 --- a/src/Network/Transport/MVar.hs +++ b/src/Network/Transport/MVar.hs @@ -22,35 +22,35 @@ mkTransport = do { newConnectionWith = \_ -> do (i, m) <- takeMVar channels receiveChan <- newEmptyMVar - let sendAddr = i + let sourceAddr = i !i' = i+1 !m' = IntMap.insert i receiveChan m putMVar channels (i', m') - return (mkSendAddr channels sendAddr, mkReceiveEnd receiveChan) + return (mkSourceAddr channels sourceAddr, mkTargetEnd receiveChan) , newMulticastWith = undefined , deserialize = \bs -> case BS.readInt bs of - Nothing -> error "dummyBackend.deserializeSendEnd: cannot parse" - Just (n,_) -> Just . mkSendAddr channels $ n + Nothing -> error "dummyBackend.deserializeSourceEnd: cannot parse" + Just (n,_) -> Just . mkSourceAddr channels $ n } where - mkSendAddr :: Chans -> Int -> SendAddr - mkSendAddr channels addr = SendAddr - { connectWith = \_ -> mkSendEnd channels addr + mkSourceAddr :: Chans -> Int -> SourceAddr + mkSourceAddr channels addr = SourceAddr + { connectWith = \_ -> mkSourceEnd channels addr , serialize = BS.pack (show addr) } - mkSendEnd :: Chans -> Int -> IO SendEnd - mkSendEnd channels addr = do + mkSourceEnd :: Chans -> Int -> IO SourceEnd + mkSourceEnd channels addr = do (_, m) <- readMVar channels case IntMap.lookup addr m of Nothing -> fail "dummyBackend.send: bad send address" - Just chan -> return SendEnd + Just chan -> return SourceEnd { send = realSend chan } - mkReceiveEnd :: MVar [ByteString] -> ReceiveEnd - mkReceiveEnd chan = ReceiveEnd + mkTargetEnd :: MVar [ByteString] -> TargetEnd + mkTargetEnd chan = TargetEnd { receive = takeMVar chan } diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 8dbd392c..8518172f 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -31,8 +31,8 @@ type ChanId = Int data TCPConfig = TCPConfig Hints HostName ServiceName -- | This creates a TCP connection between a server and a number of --- clients. Behind the scenes, the server hostname is passed as the SendAddr --- and when a connection is made, messages sent down the SendEnd go +-- clients. Behind the scenes, the server hostname is passed as the SourceAddr +-- and when a connection is made, messages sent down the SourceEnd go -- via a socket made for the client that connected. -- Messages are all queued using an unbounded Chan. mkTransport :: TCPConfig -> IO Transport @@ -55,37 +55,37 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do (chanId, chanMap) <- takeMVar channels chan <- newChan putMVar channels (chanId + 1, IntMap.insert chanId chan chanMap) - return (mkSendAddr host service chanId, mkReceiveEnd chan) + return (mkSourceAddr host service chanId, mkTargetEnd chan) , newMulticastWith = undefined , deserialize = \bs -> case readMay . BS.unpack $ bs of Nothing -> error "deserialize: cannot parse" - Just (host, service, chanId) -> Just $ mkSendAddr host service chanId + Just (host, service, chanId) -> Just $ mkSourceAddr host service chanId } where - mkSendAddr :: HostName -> ServiceName -> ChanId -> SendAddr - mkSendAddr host service chanId = SendAddr - { connectWith = \_ -> mkSendEnd host service chanId + mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr + mkSourceAddr host service chanId = SourceAddr + { connectWith = \_ -> mkSourceEnd host service chanId , serialize = BS.pack . show $ (host, service, chanId) } - mkSendEnd :: HostName -> ServiceName -> ChanId -> IO SendEnd - mkSendEnd host service chanId = withSocketsDo $ do + mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd + mkSourceEnd host service chanId = withSocketsDo $ do serverAddrs <- getAddrInfo Nothing (Just host) (Just service) let serverAddr = case serverAddrs of - [] -> error "mkSendEnd: getAddrInfo returned []" + [] -> error "mkSourceEnd: getAddrInfo returned []" as -> head as sock <- socket (addrFamily serverAddr) Stream defaultProtocol - setSocketOption sock KeepAlive 1 + setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) NBS.sendAll sock $ BS.pack . show $ chanId - return $ SendEnd + return $ SourceEnd { Network.Transport.send = \bss -> NBS.sendMany sock bss } - mkReceiveEnd :: Chan ByteString -> ReceiveEnd - mkReceiveEnd chan = ReceiveEnd + mkTargetEnd :: Chan ByteString -> TargetEnd + mkTargetEnd chan = TargetEnd { -- for now we will implement this as a Chan receive = do bs <- readChan chan From 2a838db720df71bbf0b0f11847246c93ff713b70 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Thu, 12 Jan 2012 14:58:57 +0000 Subject: [PATCH 0008/2357] Explicit Network.Socket imports. --- src/Network/Transport/TCP.hs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 8518172f..e09d6a6e 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -13,6 +13,10 @@ import Data.ByteString.Char8 (ByteString) import Data.IntMap (IntMap) import Data.Word import Network.Socket + ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , getAddrInfo, listen, setSocketOption, socket, withSocketsDo ) import Safe import qualified Data.ByteString.Char8 as BS @@ -81,7 +85,7 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do N.connect sock (addrAddress serverAddr) NBS.sendAll sock $ BS.pack . show $ chanId return $ SourceEnd - { Network.Transport.send = \bss -> NBS.sendMany sock bss + { send = \bss -> NBS.sendMany sock bss } mkTargetEnd :: Chan ByteString -> TargetEnd From c397fa74b6107fa39e75032f052f50cc5748759c Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Thu, 12 Jan 2012 15:40:16 +0000 Subject: [PATCH 0009/2357] Remove redundant import. --- src/Network/Transport/TCP.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index e09d6a6e..d20d5f78 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -11,7 +11,6 @@ import Control.Concurrent.MVar import Control.Monad (forever, unless) import Data.ByteString.Char8 (ByteString) import Data.IntMap (IntMap) -import Data.Word import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket , SocketType (Stream), SocketOption (ReuseAddr) From 6a6001ffcbfe70e2242b756b1890ba957b91c022 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Thu, 12 Jan 2012 16:09:34 +0000 Subject: [PATCH 0010/2357] Add connection closing. --- src/Network/Transport.hs | 10 ++++++++-- src/Network/Transport/TCP.hs | 25 +++++++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index cc26ec28..eb58c8a5 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -68,14 +68,20 @@ defaultSourceHints = SourceHints -- | A `SourceEnd` provides a `send` function that allows vectored messages -- to be sent to the corresponding `TargetEnd`. -newtype SourceEnd = SourceEnd +-- The `close` function closes the connection between this source and the target +-- end. Connections between other sources the target end remain unaffected +data SourceEnd = SourceEnd { send :: [ByteString] -> IO () + , close :: IO () } -- | A `TargetEnd` provides a `receive` function that allows messages -- to be received from the corresponding `SourceEnd`s. -newtype TargetEnd = TargetEnd +-- The `closeAll` function closes all connections to this target, +-- and all new connections will be refused. +data TargetEnd = TargetEnd { receive :: IO [ByteString] + , closeAll :: IO () } newtype MulticastSourceEnd = MulticastSourceEnd diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index d20d5f78..6d087605 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -15,7 +15,7 @@ import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket , SocketType (Stream), SocketOption (ReuseAddr) , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol - , getAddrInfo, listen, setSocketOption, socket, withSocketsDo ) + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) import Safe import qualified Data.ByteString.Char8 as BS @@ -23,7 +23,7 @@ import qualified Data.IntMap as IntMap import qualified Network.Socket as N import qualified Network.Socket.ByteString as NBS -type Chans = MVar (Int, IntMap (Chan ByteString)) +type Chans = MVar (Int, IntMap (Chan ByteString, MVar [Socket])) type ChanId = Int -- | This deals with several different configuration properties: @@ -51,14 +51,15 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do sock <- socket (addrFamily serverAddr) Stream defaultProtocol bindSocket sock (addrAddress serverAddr) listen sock 5 - forkIO $ procConnections channels sock + _ <- forkIO $ procConnections channels sock return Transport { newConnectionWith = \_ -> do (chanId, chanMap) <- takeMVar channels chan <- newChan - putMVar channels (chanId + 1, IntMap.insert chanId chan chanMap) - return (mkSourceAddr host service chanId, mkTargetEnd chan) + socks <- newMVar [sock] + putMVar channels (chanId + 1, IntMap.insert chanId (chan, socks) chanMap) + return (mkSourceAddr host service chanId, mkTargetEnd chan socks) , newMulticastWith = undefined , deserialize = \bs -> case readMay . BS.unpack $ bs of @@ -85,14 +86,16 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do NBS.sendAll sock $ BS.pack . show $ chanId return $ SourceEnd { send = \bss -> NBS.sendMany sock bss + , close = sClose sock } - mkTargetEnd :: Chan ByteString -> TargetEnd - mkTargetEnd chan = TargetEnd + mkTargetEnd :: Chan ByteString -> MVar [Socket] -> TargetEnd + mkTargetEnd chan socks = TargetEnd { -- for now we will implement this as a Chan receive = do bs <- readChan chan return [bs] + , closeAll = takeMVar socks >>= mapM_ sClose } procConnections :: Chans -> Socket -> IO () @@ -107,9 +110,11 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do (_, chanMap) <- readMVar chans case IntMap.lookup chanId chanMap of Nothing -> error "procConnections: cannot find chanId" - Just chan -> forkIO $ do - unless (BS.null bs') $ writeChan chan bs' - procMessages chan clientSock + Just (chan, socks) -> do + modifyMVar_ socks (return . (clientSock :)) + forkIO $ do + unless (BS.null bs') $ writeChan chan bs' + procMessages chan clientSock procMessages :: Chan ByteString -> Socket -> IO () procMessages chan sock = forever $ do From ab9ebf9f1463c05206a01a14fe577b4089714b0f Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Thu, 12 Jan 2012 17:37:55 +0000 Subject: [PATCH 0011/2357] Rename close/closeAll to closeSourceEnd/closeTargetEnd --- src/Network/Transport.hs | 4 ++-- src/Network/Transport/TCP.hs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index eb58c8a5..16fbd32a 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -72,7 +72,7 @@ defaultSourceHints = SourceHints -- end. Connections between other sources the target end remain unaffected data SourceEnd = SourceEnd { send :: [ByteString] -> IO () - , close :: IO () + , closeSourceEnd :: IO () } -- | A `TargetEnd` provides a `receive` function that allows messages @@ -81,7 +81,7 @@ data SourceEnd = SourceEnd -- and all new connections will be refused. data TargetEnd = TargetEnd { receive :: IO [ByteString] - , closeAll :: IO () + , closeTargetEnd :: IO () } newtype MulticastSourceEnd = MulticastSourceEnd diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 6d087605..68b898d9 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -86,7 +86,7 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do NBS.sendAll sock $ BS.pack . show $ chanId return $ SourceEnd { send = \bss -> NBS.sendMany sock bss - , close = sClose sock + , closeSourceEnd = sClose sock } mkTargetEnd :: Chan ByteString -> MVar [Socket] -> TargetEnd @@ -95,7 +95,7 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do receive = do bs <- readChan chan return [bs] - , closeAll = takeMVar socks >>= mapM_ sClose + , closeTargetEnd = takeMVar socks >>= mapM_ sClose } procConnections :: Chans -> Socket -> IO () From 20a072404ac6499be5677d4cfeb11b02cf4f1ca2 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 23 Jan 2012 11:56:44 +0000 Subject: [PATCH 0012/2357] Change to lazy ByteStrings. --- src/Network/Transport.hs | 2 +- src/Network/Transport/MVar.hs | 4 ++-- src/Network/Transport/TCP.hs | 17 ++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index cc26ec28..20143801 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -12,7 +12,7 @@ module Network.Transport , newMulticast ) where -import Data.ByteString.Char8 (ByteString) +import Data.ByteString.Lazy.Char8 (ByteString) -- | The `Hints` and `SourceHints` provide hints to the underlying transport -- about the kind of connection that is required. This might include details diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs index a416c0d1..801575a6 100644 --- a/src/Network/Transport/MVar.hs +++ b/src/Network/Transport/MVar.hs @@ -6,10 +6,10 @@ module Network.Transport.MVar import Control.Concurrent.MVar import Data.IntMap (IntMap) -import Data.ByteString.Char8 (ByteString) +import Data.ByteString.Lazy.Char8 (ByteString) import qualified Data.IntMap as IntMap -import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy.Char8 as BS import Network.Transport diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 8518172f..2b544920 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -9,19 +9,18 @@ import Control.Concurrent (forkIO) import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Monad (forever, unless) -import Data.ByteString.Char8 (ByteString) +import Data.ByteString.Lazy.Char8 (ByteString) import Data.IntMap (IntMap) -import Data.Word import Network.Socket import Safe -import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy.Char8 as BS import qualified Data.IntMap as IntMap import qualified Network.Socket as N -import qualified Network.Socket.ByteString as NBS +import qualified Network.Socket.ByteString.Lazy as NBS -type Chans = MVar (Int, IntMap (Chan ByteString)) -type ChanId = Int +type ChanId = Int +type Chans = MVar (ChanId, IntMap (Chan ByteString)) -- | This deals with several different configuration properties: -- * Buffer size, specified in Hints @@ -56,7 +55,7 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do chan <- newChan putMVar channels (chanId + 1, IntMap.insert chanId chan chanMap) return (mkSourceAddr host service chanId, mkTargetEnd chan) - , newMulticastWith = undefined + , newMulticastWith = error "newMulticastWith: not defined" , deserialize = \bs -> case readMay . BS.unpack $ bs of Nothing -> error "deserialize: cannot parse" @@ -80,8 +79,8 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) NBS.sendAll sock $ BS.pack . show $ chanId - return $ SourceEnd - { Network.Transport.send = \bss -> NBS.sendMany sock bss + return SourceEnd + { Network.Transport.send = mapM_ (NBS.send sock) } mkTargetEnd :: Chan ByteString -> TargetEnd From a011a85750354495f35ef8d667a9bc8bf1d201c2 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 23 Jan 2012 14:17:41 +0000 Subject: [PATCH 0013/2357] Use Data.Binary for encoding/decoding in TCP. --- network-transport.cabal | 1 + src/Network/Transport/TCP.hs | 57 ++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index fc13ed9d..168d6e28 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -11,6 +11,7 @@ Cabal-Version: >=1.2 Library Build-Depends: base >= 3 && < 5, + binary >= 0.5, bytestring >= 0.9, containers >= 0.4, network >= 2.3, diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 2b544920..371dc7c0 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -11,9 +11,11 @@ import Control.Concurrent.MVar import Control.Monad (forever, unless) import Data.ByteString.Lazy.Char8 (ByteString) import Data.IntMap (IntMap) +import Data.Int import Network.Socket import Safe +import qualified Data.Binary as B import qualified Data.ByteString.Lazy.Char8 as BS import qualified Data.IntMap as IntMap import qualified Network.Socket as N @@ -57,16 +59,15 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do return (mkSourceAddr host service chanId, mkTargetEnd chan) , newMulticastWith = error "newMulticastWith: not defined" , deserialize = \bs -> - case readMay . BS.unpack $ bs of - Nothing -> error "deserialize: cannot parse" - Just (host, service, chanId) -> Just $ mkSourceAddr host service chanId + let (host, service, chanId) = B.decode bs in + Just $ mkSourceAddr host service chanId } where mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr mkSourceAddr host service chanId = SourceAddr { connectWith = \_ -> mkSourceEnd host service chanId - , serialize = BS.pack . show $ (host, service, chanId) + , serialize = B.encode (host, service, chanId) } mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd @@ -78,9 +79,12 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do sock <- socket (addrFamily serverAddr) Stream defaultProtocol setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) - NBS.sendAll sock $ BS.pack . show $ chanId + NBS.sendAll sock $ B.encode (fromIntegral chanId :: Int64) return SourceEnd - { Network.Transport.send = mapM_ (NBS.send sock) + { Network.Transport.send = \bs -> do + let size = fromIntegral (sum . map BS.length $ bs) :: Int64 + NBS.sendAll sock (B.encode size) + mapM_ (NBS.sendAll sock) bs } mkTargetEnd :: Chan ByteString -> TargetEnd @@ -95,19 +99,36 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do procConnections chans sock = forever $ do (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId - bs <- NBS.recv clientSock 4096 - case BS.readInt bs of - Nothing -> error "procConnections: cannot parse chanId" - Just (chanId, bs') -> do - -- lookup the channel - (_, chanMap) <- readMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> error "procConnections: cannot find chanId" - Just chan -> forkIO $ do - unless (BS.null bs') $ writeChan chan bs' - procMessages chan clientSock + bs <- recvExact clientSock 8 + let chanId = fromIntegral (B.decode bs :: Int64) + (_, chanMap) <- readMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> error "procConnections: cannot find chanId" + Just chan -> forkIO $ procMessages chan clientSock + -- This function first extracts a header of type Int64, which determines + -- the size of the ByteString that follows. The ByteString is then + -- extracted from the socket, and then written to the Chan only when + -- complete. procMessages :: Chan ByteString -> Socket -> IO () procMessages chan sock = forever $ do - bs <- NBS.recv sock 4096 + sizeBS <- recvExact sock 8 + let size = B.decode sizeBS + bs <- recvExact sock size writeChan chan bs + +-- The result of `recvExact sock n` is a `ByteString` whose length is exactly +-- `n`. No more bytes than necessary are read from the socket. +-- NB: This uses Network.Socket.ByteString.recv, which may *discard* +-- superfluous input depending on the socket type. +recvExact :: Socket -> Int64 -> IO ByteString +recvExact sock n = do + bs <- NBS.recv sock n + let remainder = n - BS.length bs + if remainder > 0 + then do + bs' <- recvExact sock remainder + return (BS.append bs bs') + else + return bs + From f2135700146acbdbbaae5f655ffb3d3a7221b620 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 23 Jan 2012 21:47:47 +0000 Subject: [PATCH 0014/2357] Fix closeAll references. --- src/Network/Transport.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 0fe25074..f375590f 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -77,7 +77,7 @@ data SourceEnd = SourceEnd -- | A `TargetEnd` provides a `receive` function that allows messages -- to be received from the corresponding `SourceEnd`s. --- The `closeAll` function closes all connections to this target, +-- The `closeTargetEnd` function closes all connections to this target, -- and all new connections will be refused. data TargetEnd = TargetEnd { receive :: IO [ByteString] From 4cc702ace88cc6651f13d8cbc4e308da2eb2f4cb Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Tue, 24 Jan 2012 16:18:38 +0000 Subject: [PATCH 0015/2357] Add transport and endpoint closing. --- src/Network/Transport.hs | 1 + src/Network/Transport/TCP.hs | 101 +++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index f375590f..bdc0b5fa 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -33,6 +33,7 @@ data Transport = Transport { newConnectionWith :: Hints -> IO (SourceAddr, TargetEnd) , newMulticastWith :: Hints -> IO (MulticastSourceEnd, MulticastTargetAddr) , deserialize :: ByteString -> Maybe SourceAddr + , closeTransport :: IO () } -- | This is a convenience function that creates a new connection on a transport diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index ee5ccd6e..267b7aaa 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -5,10 +5,10 @@ module Network.Transport.TCP import Network.Transport -import Control.Concurrent (forkIO) +import Control.Concurrent (forkIO, ThreadId, killThread) import Control.Concurrent.Chan import Control.Concurrent.MVar -import Control.Monad (forever) +import Control.Monad (forever, forM_) import Data.ByteString.Lazy.Char8 (ByteString) import Data.IntMap (IntMap) import Data.Int @@ -25,8 +25,10 @@ import qualified Data.IntMap as IntMap import qualified Network.Socket as N import qualified Network.Socket.ByteString.Lazy as NBS +import Debug.Trace + type ChanId = Int -type Chans = MVar (ChanId, IntMap (Chan ByteString, MVar [Socket])) +type Chans = MVar (ChanId, IntMap (Chan ByteString, [(ThreadId, Socket)])) -- | This deals with several different configuration properties: -- * Buffer size, specified in Hints @@ -42,7 +44,7 @@ data TCPConfig = TCPConfig Hints HostName ServiceName -- Messages are all queued using an unbounded Chan. mkTransport :: TCPConfig -> IO Transport mkTransport (TCPConfig _hints host service) = withSocketsDo $ do - channels <- newMVar (0, IntMap.empty) + chans <- newMVar (0, IntMap.empty) serverAddrs <- getAddrInfo (Just (N.defaultHints { addrFlags = [AI_PASSIVE] })) Nothing @@ -51,21 +53,31 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do [] -> error "mkTransport: getAddrInfo returned []" as -> head as sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 bindSocket sock (addrAddress serverAddr) listen sock 5 - _ <- forkIO $ procConnections channels sock + threadId <- forkIO $ procConnections chans sock return Transport { newConnectionWith = \_ -> do - (chanId, chanMap) <- takeMVar channels + (chanId, chanMap) <- takeMVar chans chan <- newChan - socks <- newMVar [sock] - putMVar channels (chanId + 1, IntMap.insert chanId (chan, socks) chanMap) - return (mkSourceAddr host service chanId, mkTargetEnd chan socks) + putMVar chans (chanId + 1, IntMap.insert chanId (chan, []) chanMap) + return (mkSourceAddr host service chanId, mkTargetEnd chans chanId chan) , newMulticastWith = error "newMulticastWith: not defined" , deserialize = \bs -> let (host, service, chanId) = B.decode bs in Just $ mkSourceAddr host service chanId + , closeTransport = do + -- Kill the transport channel process + killThread threadId + sClose sock + -- Kill all target end processes + (chanId, chanMap) <- takeMVar chans + forM_ (IntMap.elems chanMap) (\(chan, socks) -> + forM_ socks (\(threadId', sock') -> do + killThread threadId' + sClose sock')) } where @@ -93,13 +105,22 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do , closeSourceEnd = sClose sock } - mkTargetEnd :: Chan ByteString -> MVar [Socket] -> TargetEnd - mkTargetEnd chan socks = TargetEnd + mkTargetEnd :: Chans -> ChanId -> Chan ByteString -> TargetEnd + mkTargetEnd chans chanId chan = TargetEnd { -- for now we will implement this as a Chan receive = do bs <- readChan chan return [bs] - , closeTargetEnd = takeMVar socks >>= mapM_ sClose + , closeTargetEnd = do + (chanId', chanMap) <- takeMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> putMVar chans (chanId', chanMap) + Just (_, socks) -> do + forM_ socks $ \(threadId, sock) -> do + killThread threadId + sClose sock + let chanMap' = IntMap.delete chanId chanMap + putMVar chans (chanId', chanMap') } procConnections :: Chans -> Socket -> IO () @@ -108,36 +129,50 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do -- decode the first message to find the correct chanId bs <- recvExact clientSock 8 let chanId = fromIntegral (B.decode bs :: Int64) - (_, chanMap) <- readMVar chans + (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of - Nothing -> error "procConnections: cannot find chanId" + Nothing -> do + putMVar chans (chanId', chanMap) + error "procConnections: cannot find chanId" Just (chan, socks) -> do - modifyMVar_ socks (return . (clientSock :)) - forkIO $ procMessages chan clientSock + threadId <- forkIO $ procMessages chans chanId chan clientSock + let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap + putMVar chans (chanId', chanMap') -- This function first extracts a header of type Int64, which determines -- the size of the ByteString that follows. The ByteString is then -- extracted from the socket, and then written to the Chan only when - -- complete. - procMessages :: Chan ByteString -> Socket -> IO () - procMessages chan sock = forever $ do + -- complete. A length of -1 indicates that the socket has finished + -- communicating. + procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () + procMessages chans chanId chan sock = do sizeBS <- recvExact sock 8 let size = B.decode sizeBS - bs <- recvExact sock size - writeChan chan bs + if size == -1 + then do + (chanId', chanMap) <- takeMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> do + putMVar chans (chanId', chanMap) + error "procMessages: chanId not found." + Just (chan, socks) -> do + let socks' = filter ((/= sock) . snd) socks + let chanMap' = IntMap.insert chanId (chan, socks') chanMap + putMVar chans (chanId', chanMap') + sClose sock + else do + bs <- recvExact sock size + writeChan chan bs + procMessages chans chanId chan sock --- The result of `recvExact sock n` is a `ByteString` whose length is exactly --- `n`. No more bytes than necessary are read from the socket. +-- | The result of `recvExact sock n` is a `ByteString` of length `n`, received +-- from `sock`. No more bytes than necessary are read from the socket. -- NB: This uses Network.Socket.ByteString.recv, which may *discard* -- superfluous input depending on the socket type. recvExact :: Socket -> Int64 -> IO ByteString -recvExact sock n = do - bs <- NBS.recv sock n - let remainder = n - BS.length bs - if remainder > 0 - then do - bs' <- recvExact sock remainder - return (BS.append bs bs') - else - return bs - +recvExact sock n = go BS.empty sock n + where + go bs sock 0 = return bs + go bs sock n = do + bs' <- NBS.recv sock n + go (BS.append bs bs') sock (n - BS.length bs') From 22788432a3e7a1a9ba141403832ad6881cf89b44 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 30 Jan 2012 00:06:24 +0000 Subject: [PATCH 0016/2357] Move mkTransport where clauses to top level. --- src/Network/Transport/TCP.hs | 151 +++++++++++++++++------------------ 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 267b7aaa..24f98455 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -80,90 +80,89 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do sClose sock')) } - where - mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr - mkSourceAddr host service chanId = SourceAddr - { connectWith = \_ -> mkSourceEnd host service chanId - , serialize = B.encode (host, service, chanId) - } +mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr +mkSourceAddr host service chanId = SourceAddr + { connectWith = \_ -> mkSourceEnd host service chanId + , serialize = B.encode (host, service, chanId) + } - mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd - mkSourceEnd host service chanId = withSocketsDo $ do - serverAddrs <- getAddrInfo Nothing (Just host) (Just service) - let serverAddr = case serverAddrs of - [] -> error "mkSourceEnd: getAddrInfo returned []" - as -> head as - sock <- socket (addrFamily serverAddr) Stream defaultProtocol - setSocketOption sock ReuseAddr 1 - N.connect sock (addrAddress serverAddr) - NBS.sendAll sock $ B.encode (fromIntegral chanId :: Int64) - return SourceEnd - { send = \bss -> do - let size = fromIntegral (sum . map BS.length $ bss) :: Int64 - NBS.sendAll sock (B.encode size) - mapM_ (NBS.sendAll sock) bss - , closeSourceEnd = sClose sock - } +mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd +mkSourceEnd host service chanId = withSocketsDo $ do + serverAddrs <- getAddrInfo Nothing (Just host) (Just service) + let serverAddr = case serverAddrs of + [] -> error "mkSourceEnd: getAddrInfo returned []" + as -> head as + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + N.connect sock (addrAddress serverAddr) + NBS.sendAll sock $ B.encode (fromIntegral chanId :: Int64) + return SourceEnd + { send = \bss -> do + let size = fromIntegral (sum . map BS.length $ bss) :: Int64 + NBS.sendAll sock (B.encode size) + mapM_ (NBS.sendAll sock) bss + , closeSourceEnd = sClose sock + } - mkTargetEnd :: Chans -> ChanId -> Chan ByteString -> TargetEnd - mkTargetEnd chans chanId chan = TargetEnd - { -- for now we will implement this as a Chan - receive = do - bs <- readChan chan - return [bs] - , closeTargetEnd = do - (chanId', chanMap) <- takeMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> putMVar chans (chanId', chanMap) - Just (_, socks) -> do - forM_ socks $ \(threadId, sock) -> do - killThread threadId - sClose sock - let chanMap' = IntMap.delete chanId chanMap - putMVar chans (chanId', chanMap') - } +mkTargetEnd :: Chans -> ChanId -> Chan ByteString -> TargetEnd +mkTargetEnd chans chanId chan = TargetEnd + { -- for now we will implement this as a Chan + receive = do + bs <- readChan chan + return [bs] + , closeTargetEnd = do + (chanId', chanMap) <- takeMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> putMVar chans (chanId', chanMap) + Just (_, socks) -> do + forM_ socks $ \(threadId, sock) -> do + killThread threadId + sClose sock + let chanMap' = IntMap.delete chanId chanMap + putMVar chans (chanId', chanMap') + } - procConnections :: Chans -> Socket -> IO () - procConnections chans sock = forever $ do - (clientSock, _clientAddr) <- accept sock - -- decode the first message to find the correct chanId - bs <- recvExact clientSock 8 - let chanId = fromIntegral (B.decode bs :: Int64) +procConnections :: Chans -> Socket -> IO () +procConnections chans sock = forever $ do + (clientSock, _clientAddr) <- accept sock + -- decode the first message to find the correct chanId + bs <- recvExact clientSock 8 + let chanId = fromIntegral (B.decode bs :: Int64) + (chanId', chanMap) <- takeMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> do + putMVar chans (chanId', chanMap) + error "procConnections: cannot find chanId" + Just (chan, socks) -> do + threadId <- forkIO $ procMessages chans chanId chan clientSock + let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap + putMVar chans (chanId', chanMap') + +-- This function first extracts a header of type Int64, which determines +-- the size of the ByteString that follows. The ByteString is then +-- extracted from the socket, and then written to the Chan only when +-- complete. A length of -1 indicates that the socket has finished +-- communicating. +procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () +procMessages chans chanId chan sock = do + sizeBS <- recvExact sock 8 + let size = B.decode sizeBS + if size == -1 + then do (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of - Nothing -> do + Nothing -> do putMVar chans (chanId', chanMap) - error "procConnections: cannot find chanId" + error "procMessages: chanId not found." Just (chan, socks) -> do - threadId <- forkIO $ procMessages chans chanId chan clientSock - let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap + let socks' = filter ((/= sock) . snd) socks + let chanMap' = IntMap.insert chanId (chan, socks') chanMap putMVar chans (chanId', chanMap') - - -- This function first extracts a header of type Int64, which determines - -- the size of the ByteString that follows. The ByteString is then - -- extracted from the socket, and then written to the Chan only when - -- complete. A length of -1 indicates that the socket has finished - -- communicating. - procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () - procMessages chans chanId chan sock = do - sizeBS <- recvExact sock 8 - let size = B.decode sizeBS - if size == -1 - then do - (chanId', chanMap) <- takeMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> do - putMVar chans (chanId', chanMap) - error "procMessages: chanId not found." - Just (chan, socks) -> do - let socks' = filter ((/= sock) . snd) socks - let chanMap' = IntMap.insert chanId (chan, socks') chanMap - putMVar chans (chanId', chanMap') - sClose sock - else do - bs <- recvExact sock size - writeChan chan bs - procMessages chans chanId chan sock + sClose sock + else do + bs <- recvExact sock size + writeChan chan bs + procMessages chans chanId chan sock -- | The result of `recvExact sock n` is a `ByteString` of length `n`, received -- from `sock`. No more bytes than necessary are read from the socket. From 3694f229a256a3cf5964b93ac4947b5080c375e7 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 30 Jan 2012 02:27:12 +0000 Subject: [PATCH 0017/2357] Fix recvExact when recv returns empty. --- src/Network/Transport/TCP.hs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 24f98455..68b63c63 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -138,16 +138,14 @@ procConnections chans sock = forever $ do let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap putMVar chans (chanId', chanMap') --- This function first extracts a header of type Int64, which determines +-- | This function first extracts a header of type Int64, which determines -- the size of the ByteString that follows. The ByteString is then -- extracted from the socket, and then written to the Chan only when --- complete. A length of -1 indicates that the socket has finished --- communicating. +-- complete. procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () procMessages chans chanId chan sock = do sizeBS <- recvExact sock 8 - let size = B.decode sizeBS - if size == -1 + if BS.null sizeBS then do (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of @@ -160,6 +158,7 @@ procMessages chans chanId chan sock = do putMVar chans (chanId', chanMap') sClose sock else do + let size = B.decode sizeBS bs <- recvExact sock size writeChan chan bs procMessages chans chanId chan sock @@ -167,11 +166,16 @@ procMessages chans chanId chan sock = do -- | The result of `recvExact sock n` is a `ByteString` of length `n`, received -- from `sock`. No more bytes than necessary are read from the socket. -- NB: This uses Network.Socket.ByteString.recv, which may *discard* --- superfluous input depending on the socket type. +-- superfluous input depending on the socket type. Also note that +-- if `recv` returns an empty `ByteString` then this means that the socket +-- was closed: in this case, we return the empty `ByteString`. recvExact :: Socket -> Int64 -> IO ByteString recvExact sock n = go BS.empty sock n where go bs sock 0 = return bs go bs sock n = do bs' <- NBS.recv sock n - go (BS.append bs bs') sock (n - BS.length bs') + if BS.null bs' + then return bs' + else go (BS.append bs bs') sock (n - BS.length bs') + From d610e7cadd975b9ba0632d8c653cc8cb2344a784 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 30 Jan 2012 13:24:46 +0000 Subject: [PATCH 0018/2357] Reduce packet header size for small messages. --- src/Network/Transport/TCP.hs | 108 +++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 68b63c63..30a76a40 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -12,6 +12,7 @@ import Control.Monad (forever, forM_) import Data.ByteString.Lazy.Char8 (ByteString) import Data.IntMap (IntMap) import Data.Int +import Data.Word import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket , SocketType (Stream), SocketOption (ReuseAddr) @@ -99,7 +100,12 @@ mkSourceEnd host service chanId = withSocketsDo $ do return SourceEnd { send = \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int64 - NBS.sendAll sock (B.encode size) + if size <= fromIntegral (maxBound :: Word8) + then + NBS.sendAll sock (B.encode (fromIntegral size :: Word8)) + else do + NBS.sendAll sock (B.encode (0 :: Word8)) + NBS.sendAll sock (B.encode size) mapM_ (NBS.sendAll sock) bss , closeSourceEnd = sClose sock } @@ -122,60 +128,88 @@ mkTargetEnd chans chanId chan = TargetEnd putMVar chans (chanId', chanMap') } +-- | This function waits for inbound connections. If a connection fails +-- for some reason, an error is raised. procConnections :: Chans -> Socket -> IO () procConnections chans sock = forever $ do (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId - bs <- recvExact clientSock 8 - let chanId = fromIntegral (B.decode bs :: Int64) - (chanId', chanMap) <- takeMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> do - putMVar chans (chanId', chanMap) - error "procConnections: cannot find chanId" - Just (chan, socks) -> do - threadId <- forkIO $ procMessages chans chanId chan clientSock - let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap - putMVar chans (chanId', chanMap') - --- | This function first extracts a header of type Int64, which determines --- the size of the ByteString that follows. The ByteString is then --- extracted from the socket, and then written to the Chan only when --- complete. -procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () -procMessages chans chanId chan sock = do - sizeBS <- recvExact sock 8 - if BS.null sizeBS - then do + mBs <- recvExact clientSock 8 + case mBs of + Nothing -> error "procConnections: inbound chanId aborted" + Just bs -> do + let chanId = fromIntegral (B.decode bs :: Int64) (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of - Nothing -> do + Nothing -> do putMVar chans (chanId', chanMap) - error "procMessages: chanId not found." + error "procConnections: cannot find chanId" Just (chan, socks) -> do - let socks' = filter ((/= sock) . snd) socks - let chanMap' = IntMap.insert chanId (chan, socks') chanMap + threadId <- forkIO $ procMessages chans chanId chan clientSock + let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap putMVar chans (chanId', chanMap') - sClose sock - else do - let size = B.decode sizeBS - bs <- recvExact sock size - writeChan chan bs - procMessages chans chanId chan sock --- | The result of `recvExact sock n` is a `ByteString` of length `n`, received --- from `sock`. No more bytes than necessary are read from the socket. +-- | This function first extracts a header of type Word8, which determines +-- the size of the ByteString that follows. If this size is 0, this indicates +-- that the ByteString is large, so the next value is an Int64, which +-- determines the size of the ByteString that follows. The ByteString is then +-- extracted from the socket, and then written to the Chan only when +-- complete. If either of the first header size is null this indicates the +-- socket has closed. +procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () +procMessages chans chanId chan sock = do + mSizeBS <- recvExact sock 1 + case mSizeBS of + Nothing -> closeSocket + Just sizeBS -> do + let size = fromIntegral (B.decode sizeBS :: Word8) + if size == 0 + then do + mSizeBS' <- recvExact sock 8 + case mSizeBS' of + Nothing -> closeSocket + Just sizeBS' -> procMessage (B.decode sizeBS' :: Int64) + else procMessage size + where + closeSocket :: IO () + closeSocket = do + (chanId', chanMap) <- takeMVar chans + case IntMap.lookup chanId chanMap of + Nothing -> do + putMVar chans (chanId', chanMap) + error "procMessages: chanId not found." + Just (chan, socks) -> do + let socks' = filter ((/= sock) . snd) socks + let chanMap' = IntMap.insert chanId (chan, socks') chanMap + putMVar chans (chanId', chanMap') + sClose sock + procMessage :: Int64 -> IO () + procMessage size = do + mBs <- recvExact sock size + case mBs of + Nothing -> closeSocket + Just bs -> do + writeChan chan bs + procMessages chans chanId chan sock + +-- | The result of `recvExact sock n` is a `Maybe ByteString` of length `n`, +-- received from `sock`. No more bytes than necessary are read from the socket. -- NB: This uses Network.Socket.ByteString.recv, which may *discard* -- superfluous input depending on the socket type. Also note that -- if `recv` returns an empty `ByteString` then this means that the socket -- was closed: in this case, we return the empty `ByteString`. -recvExact :: Socket -> Int64 -> IO ByteString +-- NB: It may be appropriate to change the return type to +-- IO (Either ByteString ByteString), where `return Left bs` indicates +-- a partial retrieval since the socket was closed, and `return Right bs` +-- indicates success. This hasn't been implemented, since a Transport `receive` +-- represents an atomic receipt of a message. +recvExact :: Socket -> Int64 -> IO (Maybe ByteString) recvExact sock n = go BS.empty sock n where - go bs sock 0 = return bs + go bs sock 0 = return (Just bs) go bs sock n = do bs' <- NBS.recv sock n if BS.null bs' - then return bs' + then return Nothing else go (BS.append bs bs') sock (n - BS.length bs') From 142175677536a54a80cdff7e4625a0c687a9f060 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 30 Jan 2012 14:46:42 +0000 Subject: [PATCH 0019/2357] Allow 0 sized messages. --- src/Network/Transport/TCP.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 30a76a40..f7a96246 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -100,11 +100,11 @@ mkSourceEnd host service chanId = withSocketsDo $ do return SourceEnd { send = \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int64 - if size <= fromIntegral (maxBound :: Word8) + if size < 255 then NBS.sendAll sock (B.encode (fromIntegral size :: Word8)) else do - NBS.sendAll sock (B.encode (0 :: Word8)) + NBS.sendAll sock (B.encode (255 :: Word8)) NBS.sendAll sock (B.encode size) mapM_ (NBS.sendAll sock) bss , closeSourceEnd = sClose sock @@ -163,7 +163,7 @@ procMessages chans chanId chan sock = do Nothing -> closeSocket Just sizeBS -> do let size = fromIntegral (B.decode sizeBS :: Word8) - if size == 0 + if size == 255 then do mSizeBS' <- recvExact sock 8 case mSizeBS' of From cd8495ce6468c138cdd7302eb30ece0bcd196494 Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Mon, 30 Jan 2012 18:11:22 +0000 Subject: [PATCH 0020/2357] Add data sending benchmark. --- src/Network/Transport/TCP.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index f7a96246..1b09c1cd 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -26,8 +26,6 @@ import qualified Data.IntMap as IntMap import qualified Network.Socket as N import qualified Network.Socket.ByteString.Lazy as NBS -import Debug.Trace - type ChanId = Int type Chans = MVar (ChanId, IntMap (Chan ByteString, [(ThreadId, Socket)])) @@ -168,7 +166,9 @@ procMessages chans chanId chan sock = do mSizeBS' <- recvExact sock 8 case mSizeBS' of Nothing -> closeSocket - Just sizeBS' -> procMessage (B.decode sizeBS' :: Int64) + Just sizeBS' -> do + let size' = B.decode sizeBS' :: Int64 + procMessage size' else procMessage size where closeSocket :: IO () From fa91c053d2dbf097812f5cf36e26353ffd75237d Mon Sep 17 00:00:00 2001 From: Nicolas Wu Date: Tue, 31 Jan 2012 23:34:53 +0000 Subject: [PATCH 0021/2357] Improve recvExact and add SCCs. --- src/Network/Transport/TCP.hs | 89 ++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 1b09c1cd..37c007fd 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -27,7 +27,7 @@ import qualified Network.Socket as N import qualified Network.Socket.ByteString.Lazy as NBS type ChanId = Int -type Chans = MVar (ChanId, IntMap (Chan ByteString, [(ThreadId, Socket)])) +type Chans = MVar (ChanId, IntMap (Chan [ByteString], [(ThreadId, Socket)])) -- | This deals with several different configuration properties: -- * Buffer size, specified in Hints @@ -58,16 +58,16 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do threadId <- forkIO $ procConnections chans sock return Transport - { newConnectionWith = \_ -> do + { newConnectionWith = {-# SCC "newConnectionWith" #-}\_ -> do (chanId, chanMap) <- takeMVar chans chan <- newChan putMVar chans (chanId + 1, IntMap.insert chanId (chan, []) chanMap) return (mkSourceAddr host service chanId, mkTargetEnd chans chanId chan) , newMulticastWith = error "newMulticastWith: not defined" - , deserialize = \bs -> + , deserialize = {-# SCC "deserialize" #-} \bs -> let (host, service, chanId) = B.decode bs in Just $ mkSourceAddr host service chanId - , closeTransport = do + , closeTransport = {-# SCC "closeTransport" #-} do -- Kill the transport channel process killThread threadId sClose sock @@ -81,8 +81,8 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr mkSourceAddr host service chanId = SourceAddr - { connectWith = \_ -> mkSourceEnd host service chanId - , serialize = B.encode (host, service, chanId) + { connectWith = {-# SCC "connectWith" #-} \_ -> mkSourceEnd host service chanId + , serialize = {-# SCC "serialize" #-} B.encode (host, service, chanId) } mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd @@ -96,7 +96,7 @@ mkSourceEnd host service chanId = withSocketsDo $ do N.connect sock (addrAddress serverAddr) NBS.sendAll sock $ B.encode (fromIntegral chanId :: Int64) return SourceEnd - { send = \bss -> do + { send = {-# SCC "send" #-} \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int64 if size < 255 then @@ -105,16 +105,14 @@ mkSourceEnd host service chanId = withSocketsDo $ do NBS.sendAll sock (B.encode (255 :: Word8)) NBS.sendAll sock (B.encode size) mapM_ (NBS.sendAll sock) bss - , closeSourceEnd = sClose sock + , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } -mkTargetEnd :: Chans -> ChanId -> Chan ByteString -> TargetEnd +mkTargetEnd :: Chans -> ChanId -> Chan [ByteString] -> TargetEnd mkTargetEnd chans chanId chan = TargetEnd { -- for now we will implement this as a Chan - receive = do - bs <- readChan chan - return [bs] - , closeTargetEnd = do + receive = {-# SCC "receive" #-} readChan chan + , closeTargetEnd = {-# SCC "closeTargetEnd" #-} do (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of Nothing -> putMVar chans (chanId', chanMap) @@ -132,10 +130,11 @@ procConnections :: Chans -> Socket -> IO () procConnections chans sock = forever $ do (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId - mBs <- recvExact clientSock 8 - case mBs of - Nothing -> error "procConnections: inbound chanId aborted" - Just bs -> do + bss <- recvExact clientSock 8 + case bss of + [] -> error "procConnections: inbound chanId aborted" + bss -> do + let bs = BS.concat bss let chanId = fromIntegral (B.decode bs :: Int64) (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of @@ -154,21 +153,19 @@ procConnections chans sock = forever $ do -- extracted from the socket, and then written to the Chan only when -- complete. If either of the first header size is null this indicates the -- socket has closed. -procMessages :: Chans -> ChanId -> Chan ByteString -> Socket -> IO () +procMessages :: Chans -> ChanId -> Chan [ByteString] -> Socket -> IO () procMessages chans chanId chan sock = do - mSizeBS <- recvExact sock 1 - case mSizeBS of - Nothing -> closeSocket - Just sizeBS -> do - let size = fromIntegral (B.decode sizeBS :: Word8) + sizeBSs <- recvExact sock 1 + case sizeBSs of + [] -> closeSocket + _ -> do + let size = fromIntegral (B.decode . BS.concat $ sizeBSs :: Word8) if size == 255 then do - mSizeBS' <- recvExact sock 8 - case mSizeBS' of - Nothing -> closeSocket - Just sizeBS' -> do - let size' = B.decode sizeBS' :: Int64 - procMessage size' + sizeBSs' <- recvExact sock 8 + case sizeBSs' of + [] -> closeSocket + _ -> procMessage (B.decode . BS.concat $ sizeBSs' :: Int64) else procMessage size where closeSocket :: IO () @@ -185,31 +182,33 @@ procMessages chans chanId chan sock = do sClose sock procMessage :: Int64 -> IO () procMessage size = do - mBs <- recvExact sock size - case mBs of - Nothing -> closeSocket - Just bs -> do - writeChan chan bs + bss <- recvExact sock size + case bss of + [] -> closeSocket + _ -> do + writeChan chan bss procMessages chans chanId chan sock --- | The result of `recvExact sock n` is a `Maybe ByteString` of length `n`, --- received from `sock`. No more bytes than necessary are read from the socket. +-- | The result of `recvExact sock n` is a `[ByteString]` whose concatenation +-- is of length `n`, received from `sock`. No more bytes than necessary are +-- read from the socket. -- NB: This uses Network.Socket.ByteString.recv, which may *discard* -- superfluous input depending on the socket type. Also note that -- if `recv` returns an empty `ByteString` then this means that the socket --- was closed: in this case, we return the empty `ByteString`. +-- was closed: in this case, we return an empty list. -- NB: It may be appropriate to change the return type to -- IO (Either ByteString ByteString), where `return Left bs` indicates -- a partial retrieval since the socket was closed, and `return Right bs` -- indicates success. This hasn't been implemented, since a Transport `receive` -- represents an atomic receipt of a message. -recvExact :: Socket -> Int64 -> IO (Maybe ByteString) -recvExact sock n = go BS.empty sock n +recvExact :: Socket -> Int64 -> IO [ByteString] +recvExact sock n = go [] sock n where - go bs sock 0 = return (Just bs) - go bs sock n = do - bs' <- NBS.recv sock n - if BS.null bs' - then return Nothing - else go (BS.append bs bs') sock (n - BS.length bs') + go :: [ByteString] -> Socket -> Int64 -> IO [ByteString] + go bss _ 0 = return (reverse bss) + go bss sock n = do + bs <- NBS.recv sock n + if BS.null bs + then return [] + else go (bs:bss) sock (n - BS.length bs) From f8050c5e9b9a71e8fd68a6023fe88f5fc9db3ab2 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Sun, 19 Feb 2012 16:20:38 -0500 Subject: [PATCH 0022/2357] Added a simple named Pipes transport. Currently in debugging mode. --- network-transport.cabal | 6 +- src/Network/Transport/Pipes.hs | 121 +++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/Network/Transport/Pipes.hs diff --git a/network-transport.cabal b/network-transport.cabal index fc13ed9d..7fc89151 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -14,10 +14,12 @@ Library bytestring >= 0.9, containers >= 0.4, network >= 2.3, - safe >= 0.3 + safe >= 0.3, + unix >= 2.5.0.0, random, cereal Exposed-modules: Network.Transport, Network.Transport.MVar, - Network.Transport.TCP + Network.Transport.TCP, + Network.Transport.Pipes Extensions: BangPatterns ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs new file mode 100644 index 00000000..1dec0519 --- /dev/null +++ b/src/Network/Transport/Pipes.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE BangPatterns, CPP #-} + +module Network.Transport.Pipes + ( mkTransport + ) where + +import Control.Monad (when) +import Control.Concurrent.MVar +import Control.Concurrent (threadDelay) +import Data.IntMap (IntMap) +-- import Data.ByteString.Char8 (ByteString) +import Data.Word +import qualified Data.IntMap as IntMap +import qualified Data.ByteString.Char8 as BS +-- import qualified Data.ByteString.Lazy.Char8 as BS + +-- import Data.Binary (decode) +import Data.Serialize (encode,decode) +import Data.List (foldl') +import Network.Transport +import System.Random (randomIO) +import System.IO (IOMode(ReadMode,AppendMode), openFile, hClose) +import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) + +-- Option 1: low level unix routines: +-- Option 2: ByteString-provided IO routines +#if UNIXIO +-- Added in unix-2.5.1.0. Let's not depend on this yet: +-- import qualified System.Posix.IO.ByteString as PIO +import qualified System.Posix.IO as PIO +import System.Posix.Types (Fd) +#else + +#endif + + +-- An address is just a filename -- the named pipe. +data Addr = Addr String + deriving (Show,Read) + +-- The msg header consists of just a length field represented as a Word32 +sizeof_header = 4 + +mkTransport :: IO Transport +mkTransport = do + uid <- randomIO :: IO Word64 + lock <- newMVar () + let filename = "/tmp/pipe_"++show uid + createNamedPipe filename $ unionFileModes ownerReadMode ownerWriteMode +#if UNIXIO + pipe <- PIO.openFd filename PIO.ReadOnly Nothing PIO.defaultFileFlags +#else + pipe <- openFile filename ReadMode +#endif + + return Transport + { newConnectionWith = \ _ -> do + return (mkSourceAddr filename, + mkTargetEnd pipe lock) + , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" + , deserialize = \bs -> return$ mkSourceAddr (BS.unpack bs) + } + where + mkSourceAddr :: String -> SourceAddr + mkSourceAddr filename = SourceAddr + { connectWith = \_ -> mkSourceEnd filename + , serialize = BS.pack filename + } + + mkSourceEnd :: String -> IO SourceEnd + mkSourceEnd filename = do +-- fd <- openFile filename AppendMode + return $ + -- Write to the named pipe. If the message is less than + -- PIPE_BUF (4KB on linux) then this should be atomic, otherwise + -- we have to do something more sophisticated. + SourceEnd + { send = \bss -> do + putStrLn$ "SENDING ... "++ show bss + + -- This may happen on multiple processes/threads: + let msgsize = foldl' (\n s -> n + BS.length s) 0 bss + when (msgsize > 4096)$ -- TODO, look up PIPE_BUF in foreign code + error "Message larger than blocksize written atomically to a named pipe. Unimplemented." + -- Otherwise it's just a simple write: + -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: + fd <- openFile filename AppendMode + BS.hPut fd (BS.concat (encode msgsize : bss)) + hClose fd -- TEMP -- opening and closing on each send! + } + +-- mkTargetEnd :: Fd -> MVar () -> TargetEnd + mkTargetEnd fd lock = TargetEnd + { receive = do + -- This should only happen on a single process. But it may + -- happen on multiple threads so we grab a lock. + takeMVar lock +#ifdef UNIXIO + (bytes,cnt) <- PIO.fdRead fd sizeof_header +#else + putStrLn$ " Attempt read header..." + + let rdloop = do + putStr "." + hdr <- BS.hGet fd sizeof_header + case BS.length hdr of + n | n == sizeof_header -> return hdr + 0 -> do threadDelay (10*1000) + rdloop + l -> error$ "Inclomplete read of msg header, only "++ show l ++ " bytes" + hdr <- rdloop + putStrLn$ " Got header "++ show hdr ++ " attempt read payload" + payload <- case decode hdr of + Left err -> error$ "ERROR: "++ err + Right size -> BS.hGet fd size + putStrLn$ " Got payload "++ show payload +#endif + putMVar lock () + return [payload] + } + From ea0b0c048fb5433ec29e4c2a0145d73b56425cc5 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Sun, 19 Feb 2012 23:09:26 -0500 Subject: [PATCH 0023/2357] This checkin passes a simple demo test, but it still depends on opening and closing the fifo on every send. This is proving trickier to get right than I thought. --- src/Network/Transport/Pipes.hs | 95 +++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 1dec0519..29546483 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE BangPatterns, CPP #-} +{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables #-} module Network.Transport.Pipes ( mkTransport @@ -6,7 +6,7 @@ module Network.Transport.Pipes import Control.Monad (when) import Control.Concurrent.MVar -import Control.Concurrent (threadDelay) +import Control.Concurrent (threadDelay, forkOS) import Data.IntMap (IntMap) -- import Data.ByteString.Char8 (ByteString) import Data.Word @@ -19,12 +19,14 @@ import Data.Serialize (encode,decode) import Data.List (foldl') import Network.Transport import System.Random (randomIO) -import System.IO (IOMode(ReadMode,AppendMode), openFile, hClose) +import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), + openFile, hClose, hPutStrLn, hPutStr, stderr, stdout) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) -- Option 1: low level unix routines: -- Option 2: ByteString-provided IO routines -#if UNIXIO +#define UNIXIO +#ifdef UNIXIO -- Added in unix-2.5.1.0. Let's not depend on this yet: -- import qualified System.Posix.IO.ByteString as PIO import qualified System.Posix.IO as PIO @@ -47,16 +49,14 @@ mkTransport = do lock <- newMVar () let filename = "/tmp/pipe_"++show uid createNamedPipe filename $ unionFileModes ownerReadMode ownerWriteMode -#if UNIXIO - pipe <- PIO.openFd filename PIO.ReadOnly Nothing PIO.defaultFileFlags -#else - pipe <- openFile filename ReadMode -#endif + + dbgprint1$ " Created pipe at location: "++ filename return Transport { newConnectionWith = \ _ -> do + dbgprint1$ " Creating new connection" return (mkSourceAddr filename, - mkTargetEnd pipe lock) + mkTargetEnd filename lock) , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" , deserialize = \bs -> return$ mkSourceAddr (BS.unpack bs) } @@ -70,52 +70,85 @@ mkTransport = do mkSourceEnd :: String -> IO SourceEnd mkSourceEnd filename = do -- fd <- openFile filename AppendMode +-- mv <- onOSThread$ openFile filename WriteMode return $ -- Write to the named pipe. If the message is less than -- PIPE_BUF (4KB on linux) then this should be atomic, otherwise -- we have to do something more sophisticated. SourceEnd { send = \bss -> do - putStrLn$ "SENDING ... "++ show bss + dbgprint1$ "SENDING ... "++ show bss -- This may happen on multiple processes/threads: - let msgsize = foldl' (\n s -> n + BS.length s) 0 bss + let msgsize :: Word32 = fromIntegral$ foldl' (\n s -> n + BS.length s) 0 bss when (msgsize > 4096)$ -- TODO, look up PIPE_BUF in foreign code error "Message larger than blocksize written atomically to a named pipe. Unimplemented." -- Otherwise it's just a simple write: -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: - fd <- openFile filename AppendMode - BS.hPut fd (BS.concat (encode msgsize : bss)) + +-- fd <- readMVar mv + dbgprint1$ " (sending... got file descriptor) " +-- fd <- openFile filename AppendMode + fd <- openFile filename WriteMode + +-- TODO: Consider nonblocking opening of file to make things simpler. + + let finalmsg = BS.concat (encode msgsize : bss) + dbgprint1$ " Final send msg: " ++ show finalmsg + BS.hPut fd finalmsg hClose fd -- TEMP -- opening and closing on each send! } -- mkTargetEnd :: Fd -> MVar () -> TargetEnd - mkTargetEnd fd lock = TargetEnd + mkTargetEnd filename lock = TargetEnd { receive = do + dbgprint2$ "Begin receive action..." -- This should only happen on a single process. But it may -- happen on multiple threads so we grab a lock. takeMVar lock #ifdef UNIXIO - (bytes,cnt) <- PIO.fdRead fd sizeof_header +-- fd <- PIO.openFd filename PIO.ReadOnly Nothing PIO.defaultFileFlags + fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + let oneread n = do (s,cnt) <- PIO.fdRead fd (fromIntegral n) + return (BS.pack s, fromIntegral cnt) #else - putStrLn$ " Attempt read header..." + fd <- openFile filename ReadMode + let oneread n = do bs <- BS.hGet fd n; + return (bs, BS.length bs) +#endif + dbgprint2$ " Attempt read header..." - let rdloop = do - putStr "." - hdr <- BS.hGet fd sizeof_header - case BS.length hdr of - n | n == sizeof_header -> return hdr + let spinread :: Int -> IO BS.ByteString + spinread desired = do +-- hPutStr stderr "." + (bytes,len) <- oneread desired + case len of + n | n == desired -> return bytes 0 -> do threadDelay (10*1000) - rdloop - l -> error$ "Inclomplete read of msg header, only "++ show l ++ " bytes" - hdr <- rdloop - putStrLn$ " Got header "++ show hdr ++ " attempt read payload" - payload <- case decode hdr of - Left err -> error$ "ERROR: "++ err - Right size -> BS.hGet fd size - putStrLn$ " Got payload "++ show payload -#endif + spinread desired + l -> error$ "Inclomplete read expected either 0 bytes or complete msg ("++ + show desired ++" bytes) got "++ show l ++ " bytes" + + hdr <- spinread sizeof_header + dbgprint2$ " Got header "++ show hdr ++ " attempt read payload" + payload <- case decode hdr of + Left err -> error$ "ERROR: "++ err + Right size -> spinread (fromIntegral (size::Word32)) + dbgprint2$ " Got payload "++ show payload + putMVar lock () return [payload] } +-- dbgprint2 = hPutStrLn stdout +-- dbgprint1 = hPutStrLn stderr +dbgprint1 _ = return () +dbgprint2 _ = return () + + +-- Execute an action on its own OS thread. Return an MVar to synchronize on. +onOSThread :: IO a -> IO (MVar a) +onOSThread action = do + mv <- newEmptyMVar + forkOS (action >>= putMVar mv ) + return mv From 2647238e88d0b5de4d27b5802152c472b0245d0b Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Sun, 19 Feb 2012 23:38:40 -0500 Subject: [PATCH 0024/2357] This is still covered in debugging cruft, but it works without opening and closing the named pipe on every send. --- src/Network/Transport/Pipes.hs | 55 ++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 29546483..4cc49e14 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -4,12 +4,13 @@ module Network.Transport.Pipes ( mkTransport ) where -import Control.Monad (when) +import Control.Monad (when, unless) import Control.Concurrent.MVar import Control.Concurrent (threadDelay, forkOS) import Data.IntMap (IntMap) -- import Data.ByteString.Char8 (ByteString) import Data.Word +import Data.IORef import qualified Data.IntMap as IntMap import qualified Data.ByteString.Char8 as BS -- import qualified Data.ByteString.Lazy.Char8 as BS @@ -69,8 +70,16 @@ mkTransport = do mkSourceEnd :: String -> IO SourceEnd mkSourceEnd filename = do +# ifndef KEEP_CLOSED -- fd <- openFile filename AppendMode +-- _ <- openFile filename WriteMode -- mv <- onOSThread$ openFile filename WriteMode + mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + + -- [2012.02.19] Still getting indefinite MVar block errors even + -- using IORefs here: +-- fdref <- newIORef Nothing +# endif return $ -- Write to the named pipe. If the message is less than -- PIPE_BUF (4KB on linux) then this should be atomic, otherwise @@ -85,20 +94,43 @@ mkTransport = do error "Message larger than blocksize written atomically to a named pipe. Unimplemented." -- Otherwise it's just a simple write: -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: - --- fd <- readMVar mv - dbgprint1$ " (sending... got file descriptor) " + +# ifdef KEEP_CLOSED -- fd <- openFile filename AppendMode fd <- openFile filename WriteMode +# else + -- OPTION 1: Lazy opening: + -- r <- readIORef fdref + -- fd <- case r of + -- Nothing -> do + -- dbgprint1$ " (No FD for sending yet... opening) " + -- fd <- openFile filename AppendMode + -- writeIORef fdref (Just fd) + -- return fd + -- Just fd -> return fd + + -- OPTION 2: Speculative file opening, plus this synchronnization: + fd <- readMVar mv + dbgprint1$ " (sending... got file descriptor) " +# endif -- TODO: Consider nonblocking opening of file to make things simpler. let finalmsg = BS.concat (encode msgsize : bss) dbgprint1$ " Final send msg: " ++ show finalmsg - BS.hPut fd finalmsg + ---------------------------------------- + -- BS.hPut fd finalmsg -- The actual send! + cnt <- PIO.fdWrite fd (BS.unpack finalmsg) -- inefficient! + unless (fromIntegral cnt == BS.length finalmsg) $ + error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) + ---------------------------------------- +# ifdef KEEP_CLOSED hClose fd -- TEMP -- opening and closing on each send! +# endif + return () } + -- mkTargetEnd :: Fd -> MVar () -> TargetEnd mkTargetEnd filename lock = TargetEnd { receive = do @@ -108,7 +140,8 @@ mkTransport = do takeMVar lock #ifdef UNIXIO -- fd <- PIO.openFd filename PIO.ReadOnly Nothing PIO.defaultFileFlags - fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + fd <- takeMVar mv let oneread n = do (s,cnt) <- PIO.fdRead fd (fromIntegral n) return (BS.pack s, fromIntegral cnt) #else @@ -117,7 +150,7 @@ mkTransport = do return (bs, BS.length bs) #endif dbgprint2$ " Attempt read header..." - + let spinread :: Int -> IO BS.ByteString spinread desired = do -- hPutStr stderr "." @@ -140,10 +173,13 @@ mkTransport = do return [payload] } --- dbgprint2 = hPutStrLn stdout --- dbgprint1 = hPutStrLn stderr +#ifdef DEBUG +dbgprint2 = hPutStrLn stdout +dbgprint1 = hPutStrLn stderr +#else dbgprint1 _ = return () dbgprint2 _ = return () +#endif -- Execute an action on its own OS thread. Return an MVar to synchronize on. @@ -152,3 +188,4 @@ onOSThread action = do mv <- newEmptyMVar forkOS (action >>= putMVar mv ) return mv +-- [2012.02.19] This didn't seem to help. From 6c2123d56eed27173cd61a9ee73c2b84ece37194 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 00:01:51 -0500 Subject: [PATCH 0025/2357] Removed some of the unused options now that something is working. Both opening up the writing end immediately and the speculative strategy (opening it on another thread) work. --- src/Network/Transport/Pipes.hs | 76 ++++++++-------------------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 4cc49e14..1bbe2dc1 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -24,22 +24,11 @@ import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), openFile, hClose, hPutStrLn, hPutStr, stderr, stdout) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) --- Option 1: low level unix routines: --- Option 2: ByteString-provided IO routines -#define UNIXIO -#ifdef UNIXIO --- Added in unix-2.5.1.0. Let's not depend on this yet: --- import qualified System.Posix.IO.ByteString as PIO +-- Basing this on low level unix routines: import qualified System.Posix.IO as PIO +-- Bytestring-version added in unix-2.5.1.0. Let's not depend on this yet: +-- import qualified System.Posix.IO.ByteString as PIO import System.Posix.Types (Fd) -#else - -#endif - - --- An address is just a filename -- the named pipe. -data Addr = Addr String - deriving (Show,Read) -- The msg header consists of just a length field represented as a Word32 sizeof_header = 4 @@ -70,16 +59,12 @@ mkTransport = do mkSourceEnd :: String -> IO SourceEnd mkSourceEnd filename = do -# ifndef KEEP_CLOSED --- fd <- openFile filename AppendMode --- _ <- openFile filename WriteMode --- mv <- onOSThread$ openFile filename WriteMode - mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags - - -- [2012.02.19] Still getting indefinite MVar block errors even - -- using IORefs here: --- fdref <- newIORef Nothing -# endif + -- Initiate but do not block on file opening: + -- Note: Linux fifo semantics are NOT to block on open until +-- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags +-- fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags + return $ -- Write to the named pipe. If the message is less than -- PIPE_BUF (4KB on linux) then this should be atomic, otherwise @@ -94,61 +79,34 @@ mkTransport = do error "Message larger than blocksize written atomically to a named pipe. Unimplemented." -- Otherwise it's just a simple write: -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: - -# ifdef KEEP_CLOSED --- fd <- openFile filename AppendMode - fd <- openFile filename WriteMode -# else - -- OPTION 1: Lazy opening: - -- r <- readIORef fdref - -- fd <- case r of - -- Nothing -> do - -- dbgprint1$ " (No FD for sending yet... opening) " - -- fd <- openFile filename AppendMode - -- writeIORef fdref (Just fd) - -- return fd - -- Just fd -> return fd - - -- OPTION 2: Speculative file opening, plus this synchronnization: - fd <- readMVar mv - dbgprint1$ " (sending... got file descriptor) " -# endif - --- TODO: Consider nonblocking opening of file to make things simpler. - let finalmsg = BS.concat (encode msgsize : bss) dbgprint1$ " Final send msg: " ++ show finalmsg + + -- OPTION 2: Speculative file opening, plus this synchronnization: +-- fd <- readMVar mv +-- dbgprint1$ " (sending... got file descriptor) " + ---------------------------------------- - -- BS.hPut fd finalmsg -- The actual send! cnt <- PIO.fdWrite fd (BS.unpack finalmsg) -- inefficient! unless (fromIntegral cnt == BS.length finalmsg) $ error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) ---------------------------------------- -# ifdef KEEP_CLOSED - hClose fd -- TEMP -- opening and closing on each send! -# endif + return () } - --- mkTargetEnd :: Fd -> MVar () -> TargetEnd + mkTargetEnd :: String -> MVar () -> TargetEnd mkTargetEnd filename lock = TargetEnd { receive = do dbgprint2$ "Begin receive action..." -- This should only happen on a single process. But it may -- happen on multiple threads so we grab a lock. takeMVar lock -#ifdef UNIXIO --- fd <- PIO.openFd filename PIO.ReadOnly Nothing PIO.defaultFileFlags + mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags fd <- takeMVar mv let oneread n = do (s,cnt) <- PIO.fdRead fd (fromIntegral n) return (BS.pack s, fromIntegral cnt) -#else - fd <- openFile filename ReadMode - let oneread n = do bs <- BS.hGet fd n; - return (bs, BS.length bs) -#endif dbgprint2$ " Attempt read header..." let spinread :: Int -> IO BS.ByteString From cf7c5d7c530a1e4fa8cc99b28aa2f24926b4415d Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 01:10:11 -0500 Subject: [PATCH 0026/2357] Factored Pipes.hs to work with either unix or unix-bytestring. Bytestring versions should be more efficient --- network-transport.cabal | 1 + src/Network/Transport/Pipes.hs | 35 ++++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 7fc89151..75dcbaab 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -16,6 +16,7 @@ Library network >= 2.3, safe >= 0.3, unix >= 2.5.0.0, random, cereal +-- , unix-bytestring Exposed-modules: Network.Transport, Network.Transport.MVar, Network.Transport.TCP, diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 1bbe2dc1..2cc90a34 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables #-} +{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables, PackageImports #-} module Network.Transport.Pipes ( mkTransport @@ -23,13 +23,24 @@ import System.Random (randomIO) import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), openFile, hClose, hPutStrLn, hPutStr, stderr, stdout) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) - --- Basing this on low level unix routines: -import qualified System.Posix.IO as PIO --- Bytestring-version added in unix-2.5.1.0. Let's not depend on this yet: --- import qualified System.Posix.IO.ByteString as PIO import System.Posix.Types (Fd) +#ifdef USE_UNIX_BYTESTRING +import qualified "unix-bytestring" System.Posix.IO.ByteString as PIO +import System.Posix.IO as PIO (openFd, defaultFileFlags, OpenMode(ReadWrite, WriteOnly)) +(fromS,toS) = (BS.pack, BS.unpack) +(fromBS,toBS) = (id,id) +readit fd n = PIO.fdRead fd n +#else +import qualified System.Posix.IO as PIO +(toS,fromS) = (id,id) +(fromBS,toBS) = (BS.unpack, BS.pack) +readit fd n = do (s,_) <- PIO.fdRead fd n + return (BS.pack s) +#endif + +---------------------------------------------------------------------------------------------------- + -- The msg header consists of just a length field represented as a Word32 sizeof_header = 4 @@ -60,7 +71,9 @@ mkTransport = do mkSourceEnd :: String -> IO SourceEnd mkSourceEnd filename = do -- Initiate but do not block on file opening: - -- Note: Linux fifo semantics are NOT to block on open until + -- Note: Linux fifo semantics are NOT to block on open-RW, but this is not Posix standard. + -- + -- We may protect from blocking other threads by running on a separate (OS) thread: -- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags -- fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags @@ -84,10 +97,8 @@ mkTransport = do -- OPTION 2: Speculative file opening, plus this synchronnization: -- fd <- readMVar mv --- dbgprint1$ " (sending... got file descriptor) " - ---------------------------------------- - cnt <- PIO.fdWrite fd (BS.unpack finalmsg) -- inefficient! + cnt <- PIO.fdWrite fd (fromBS finalmsg) -- inefficient to use String here! unless (fromIntegral cnt == BS.length finalmsg) $ error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) ---------------------------------------- @@ -105,8 +116,8 @@ mkTransport = do mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags fd <- takeMVar mv - let oneread n = do (s,cnt) <- PIO.fdRead fd (fromIntegral n) - return (BS.pack s, fromIntegral cnt) + let oneread n = do bs <- readit fd (fromIntegral n) + return (bs, BS.length bs) dbgprint2$ " Attempt read header..." let spinread :: Int -> IO BS.ByteString From 2570c2c3b5167efeac4fdfa3f682b84fdd7d30cf Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 02:01:02 -0500 Subject: [PATCH 0027/2357] The Pipes transport works for demo1 and demo0. --- src/Network/Transport/Pipes.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index a88cb57b..157efd51 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -75,9 +75,10 @@ mkTransport = do -- Note: Linux fifo semantics are NOT to block on open-RW, but this is not Posix standard. -- -- We may protect from blocking other threads by running on a separate (OS) thread: --- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags -- fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags - fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags +-- fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags + fd <- takeMVar mv return $ -- Write to the named pipe. If the message is less than From 5a1fe70bdeb60b0a3aa2e9aed4e7be811c76ffcf Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 09:54:17 -0500 Subject: [PATCH 0028/2357] After merge making sure that TCP transport works for all demos. All demos enabled in this rev, TCP transport only. --- src/Network/Transport/Pipes.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 157efd51..0429c591 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -61,6 +61,9 @@ mkTransport = do mkTargetEnd filename lock) , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" , deserialize = \bs -> return$ mkSourceAddr (BS.unpack bs) + , closeTransport = do +-- removeFile filename + return () } where mkSourceAddr :: String -> SourceAddr From 5dd99692666d21def5c37d163338e6a37c9eff7d Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 09:57:28 -0500 Subject: [PATCH 0029/2357] Added a NOOP closeTransport to the MVar transport. It passes all demos too --- src/Network/Transport/MVar.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs index 801575a6..7cc84bd2 100644 --- a/src/Network/Transport/MVar.hs +++ b/src/Network/Transport/MVar.hs @@ -32,6 +32,7 @@ mkTransport = do case BS.readInt bs of Nothing -> error "dummyBackend.deserializeSourceEnd: cannot parse" Just (n,_) -> Just . mkSourceAddr channels $ n + , closeTransport = return () } where mkSourceAddr :: Chans -> Int -> SourceAddr From f5de31711330d63bc16fae86bbcc36ed9579bb12 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 13:00:06 -0500 Subject: [PATCH 0030/2357] Two small changes to TCP transport. Added propagation of exceptions from child threads. Added more defensive pattern matching for the one byte read. --- src/Network/Transport/TCP.hs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 37c007fd..6e37f543 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -5,9 +5,10 @@ module Network.Transport.TCP import Network.Transport -import Control.Concurrent (forkIO, ThreadId, killThread) +import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) import Control.Concurrent.Chan import Control.Concurrent.MVar +import Control.Exception (SomeException, throwTo, catch) import Control.Monad (forever, forM_) import Data.ByteString.Lazy.Char8 (ByteString) import Data.IntMap (IntMap) @@ -19,6 +20,7 @@ import Network.Socket , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) import Safe +import System.IO (stderr, hPutStrLn) import qualified Data.Binary as B import qualified Data.ByteString.Lazy.Char8 as BS @@ -55,7 +57,8 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do setSocketOption sock ReuseAddr 1 bindSocket sock (addrAddress serverAddr) listen sock 5 - threadId <- forkIO $ procConnections chans sock + threadId <- forkWithExceptions forkIO "Connection Listener" $ + procConnections chans sock return Transport { newConnectionWith = {-# SCC "newConnectionWith" #-}\_ -> do @@ -142,7 +145,8 @@ procConnections chans sock = forever $ do putMVar chans (chanId', chanMap) error "procConnections: cannot find chanId" Just (chan, socks) -> do - threadId <- forkIO $ procMessages chans chanId chan clientSock + threadId <- forkWithExceptions forkIO "Message Listener" $ + procMessages chans chanId chan clientSock let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap putMVar chans (chanId', chanMap') @@ -157,16 +161,17 @@ procMessages :: Chans -> ChanId -> Chan [ByteString] -> Socket -> IO () procMessages chans chanId chan sock = do sizeBSs <- recvExact sock 1 case sizeBSs of - [] -> closeSocket - _ -> do - let size = fromIntegral (B.decode . BS.concat $ sizeBSs :: Word8) + [] -> closeSocket + [onebyte] -> do + let size = fromIntegral (B.decode onebyte :: Word8) if size == 255 then do sizeBSs' <- recvExact sock 8 case sizeBSs' of [] -> closeSocket - _ -> procMessage (B.decode . BS.concat $ sizeBSs' :: Int64) + _ -> procMessage (B.decode . BS.concat$ sizeBSs' :: Int64) else procMessage size + ls -> error "Shouldn't receive more than one bytestring when expecting a single byte!" where closeSocket :: IO () closeSocket = do @@ -212,3 +217,15 @@ recvExact sock n = go [] sock n then return [] else go (bs:bss) sock (n - BS.length bs) + + +forkWithExceptions :: (IO () -> IO ThreadId) -> String -> IO () -> IO ThreadId +forkWithExceptions forkit descr action = do + parent <- myThreadId + forkit $ + Control.Exception.catch action + (\ e -> do + hPutStrLn stderr $ "Exception inside child thread "++descr++": "++show e + throwTo parent (e::SomeException) + ) + From 83ed51464a328e28ea253d2b922546ea9a71cbc2 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 14:05:46 -0500 Subject: [PATCH 0031/2357] Add more exception handling. Pipes is still failing outright, the listener reads the header of the first message, then attempts to read the payload and gets stuck: 'too few bytes'. --- src/Network/Transport/Pipes.hs | 2 ++ src/Network/Transport/TCP.hs | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 0429c591..3f3493de 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -26,6 +26,8 @@ import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) import System.Posix.Types (Fd) +-- define DEBUG + #ifdef USE_UNIX_BYTESTRING import qualified "unix-bytestring" System.Posix.IO.ByteString as PIO import System.Posix.IO as PIO (openFd, defaultFileFlags, OpenMode(ReadWrite, WriteOnly)) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 6e37f543..ba0f592c 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -8,7 +8,8 @@ import Network.Transport import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) import Control.Concurrent.Chan import Control.Concurrent.MVar -import Control.Exception (SomeException, throwTo, catch) +import Control.Exception (SomeException, IOException, AsyncException(ThreadKilled), + fromException, throwTo, throw, catch, handle) import Control.Monad (forever, forM_) import Data.ByteString.Lazy.Char8 (ByteString) import Data.IntMap (IntMap) @@ -23,6 +24,7 @@ import Safe import System.IO (stderr, hPutStrLn) import qualified Data.Binary as B +import qualified Data.ByteString.Char8 as BSS import qualified Data.ByteString.Lazy.Char8 as BS import qualified Data.IntMap as IntMap import qualified Network.Socket as N @@ -207,7 +209,9 @@ procMessages chans chanId chan sock = do -- indicates success. This hasn't been implemented, since a Transport `receive` -- represents an atomic receipt of a message. recvExact :: Socket -> Int64 -> IO [ByteString] -recvExact sock n = go [] sock n +recvExact sock n = + interceptAllExn "recvExact" $ + go [] sock n where go :: [ByteString] -> Socket -> Int64 -> IO [ByteString] go bss _ 0 = return (reverse bss) @@ -218,6 +222,10 @@ recvExact sock n = go [] sock n else go (bs:bss) sock (n - BS.length bs) +interceptAllExn msg = + Control.Exception.handle $ \ e -> do + BSS.hPutStrLn stderr $ BSS.pack$ "Exception inside "++msg++": "++show e + throw (e :: SomeException) forkWithExceptions :: (IO () -> IO ThreadId) -> String -> IO () -> IO ThreadId forkWithExceptions forkit descr action = do @@ -225,7 +233,12 @@ forkWithExceptions forkit descr action = do forkit $ Control.Exception.catch action (\ e -> do - hPutStrLn stderr $ "Exception inside child thread "++descr++": "++show e - throwTo parent (e::SomeException) + case fromException e of + Just ThreadKilled -> +-- BSS.hPutStrLn stderr $ BSS.pack$ "Note: Child thread killed: "++descr + return () + _ -> do + BSS.hPutStrLn stderr $ BSS.pack$ "Exception inside child thread "++descr++": "++show e + throwTo parent (e::SomeException) ) From e9b9d3b04bf8025b86e4bc69f6dca90c6cc4eba0 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 14:51:39 -0500 Subject: [PATCH 0032/2357] minor: make writes use strict bytestrings to reduce interleaving --- src/Network/Transport/Pipes.hs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 3f3493de..4cfed3a1 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -13,7 +13,7 @@ import Data.Word import Data.Int import Data.IORef import qualified Data.IntMap as IntMap --- import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Char8 as BSS import qualified Data.ByteString.Lazy.Char8 as BS import Data.Binary (encode,decode) @@ -80,10 +80,11 @@ mkTransport = do -- Note: Linux fifo semantics are NOT to block on open-RW, but this is not Posix standard. -- -- We may protect from blocking other threads by running on a separate (OS) thread: - mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags +-- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags -- fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags --- fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags - fd <- takeMVar mv + fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags +-- fd <- takeMVar mv + dbgprint1$ "GOT WRITING END OPEN ... " return $ -- Write to the named pipe. If the message is less than @@ -130,7 +131,10 @@ mkTransport = do let spinread :: Int64 -> IO BS.ByteString spinread desired = do +#ifdef DEBUG -- hPutStr stderr "." + BSS.hPutStr stdout (BSS.pack$ " "++show desired) +#endif (bytes,len) <- oneread desired case len of n | n == desired -> return bytes @@ -151,8 +155,8 @@ mkTransport = do } #ifdef DEBUG -dbgprint2 = hPutStrLn stdout -dbgprint1 = hPutStrLn stderr +dbgprint1 s = BSS.hPutStrLn stderr (BSS.pack s) +dbgprint2 s = BSS.hPutStrLn stdout (BSS.pack s) #else dbgprint1 _ = return () dbgprint2 _ = return () From 3b75555bbb1fa1de43f4a21b30ac8ce9a3508a71 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 16:51:58 -0500 Subject: [PATCH 0033/2357] Got to the bottom of the regression in the Pipes transport. The catalyst was switching between cereal and binary. I have no idea (still) why this is. This checkin switches back to cereal (and inefficiently converts lazy/strict bytestrings). Demo 0 and 1 work for all transports. The rest have remaining problems for Pipes. --- src/Network/Transport/Pipes.hs | 44 +++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 4cfed3a1..e4478042 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -5,6 +5,7 @@ module Network.Transport.Pipes ) where import Control.Monad (when, unless) +import Control.Exception (evaluate) import Control.Concurrent.MVar import Control.Concurrent (threadDelay, forkOS) import Data.IntMap (IntMap) @@ -16,8 +17,14 @@ import qualified Data.IntMap as IntMap import qualified Data.ByteString.Char8 as BSS import qualified Data.ByteString.Lazy.Char8 as BS -import Data.Binary (encode,decode) --- import Data.Serialize (encode,decode) +-- For some STRANGE reason this is not working with Data.Binary [2012.02.20]: +#define CEREAL +#ifdef CEREAL +import Data.Serialize (encode,decode) -- Uses strict BS +#else +import Data.Binary (encode,decode) -- Uses lazy BS +#endif + import Data.List (foldl') import Network.Transport import System.Random (randomIO) @@ -27,6 +34,7 @@ import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, owner import System.Posix.Types (Fd) -- define DEBUG +-- define USE_UNIX_BYTESTRING #ifdef USE_UNIX_BYTESTRING import qualified "unix-bytestring" System.Posix.IO.ByteString as PIO @@ -79,11 +87,19 @@ mkTransport = do -- Initiate but do not block on file opening: -- Note: Linux fifo semantics are NOT to block on open-RW, but this is not Posix standard. -- - -- We may protect from blocking other threads by running on a separate (OS) thread: + + -- All THREE of the below options were observed to work on a simple demo: + + -- OPTION (1) + -- Here we protect from blocking other threads by running on a separate (OS) thread: -- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags +-- fd <- takeMVar mv + + -- OPTION (2) -- fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + + -- OPTION (3) fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags --- fd <- takeMVar mv dbgprint1$ "GOT WRITING END OPEN ... " return $ @@ -92,7 +108,7 @@ mkTransport = do -- we have to do something more sophisticated. SourceEnd { send = \bss -> do - dbgprint1$ "SENDING ... "++ show bss + dbgprint1$ "Sending.. "++ show bss -- This may happen on multiple processes/threads: let msgsize :: Word32 = fromIntegral$ foldl' (\n s -> n + BS.length s) 0 bss @@ -100,7 +116,8 @@ mkTransport = do error "Message larger than blocksize written atomically to a named pipe. Unimplemented." -- Otherwise it's just a simple write: -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: - let finalmsg = BS.concat (encode msgsize : bss) +-- let finalmsg = BS.concat (encode msgsize : bss) + let finalmsg = BS.concat ((BS.fromChunks[encode msgsize]) : bss) dbgprint1$ " Final send msg: " ++ show finalmsg -- OPTION 2: Speculative file opening, plus this synchronnization: @@ -132,7 +149,8 @@ mkTransport = do let spinread :: Int64 -> IO BS.ByteString spinread desired = do #ifdef DEBUG --- hPutStr stderr "." + hPutStr stderr "." +-- dbgprint2$ " SPINREAD "++ show desired BSS.hPutStr stdout (BSS.pack$ " "++show desired) #endif (bytes,len) <- oneread desired @@ -144,8 +162,16 @@ mkTransport = do show desired ++" bytes) got "++ show l ++ " bytes" hdr <- spinread sizeof_header - dbgprint2$ " Got header "++ show hdr ++ " attempt read payload" - payload <- case decode hdr of + dbgprint2$ " Got header "++ show hdr ++ ", next attempt to read payload:" +#ifdef CEREAL + let decoded = decode (BSS.concat$ BS.toChunks hdr) +-- dbgprint2$ " DECODING HDR, bytes "++ show (BS.length hdr) ++ ": "++show hdr +-- evaluate decoded +-- dbgprint2$ " DONE DECODING HDR" +#else + let decoded = decode hdr +#endif + payload <- case decoded of Left err -> error$ "ERROR: "++ err Right size -> spinread (fromIntegral (size::Word32)) dbgprint2$ " Got payload "++ show payload From 4f65a1a889fbd8f8eb07a3072e447b695b7d8a32 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 23:23:15 -0500 Subject: [PATCH 0034/2357] Made Pipes use nonblocking mode and changed a number of things to make this happen. Now all three transports pass all demos except demo2. --- src/Network/Transport/Pipes.hs | 124 ++++++++++++++++++++++++++++----- src/Network/Transport/TCP.hs | 10 ++- 2 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index e4478042..4bb5827b 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -5,7 +5,8 @@ module Network.Transport.Pipes ) where import Control.Monad (when, unless) -import Control.Exception (evaluate) +import Control.Exception (evaluate, throw, handle, fromException, + SomeException, IOException) import Control.Concurrent.MVar import Control.Concurrent (threadDelay, forkOS) import Data.IntMap (IntMap) @@ -29,9 +30,10 @@ import Data.List (foldl') import Network.Transport import System.Random (randomIO) import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), - openFile, hClose, hPutStrLn, hPutStr, stderr, stdout) + openFile, hClose, hPutStrLn, hPutStr, stderr, stdout, hFlush) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) import System.Posix.Types (Fd) +import System.Directory (doesFileExist) -- define DEBUG -- define USE_UNIX_BYTESTRING @@ -55,6 +57,23 @@ readit fd n = do (s,_) <- PIO.fdRead fd n -- The msg header consists of just a length field represented as a Word32 sizeof_header = 4 +-- fileFlags = PIO.defaultFileFlags +fileFlags = + PIO.OpenFileFlags { + PIO.append = False, + PIO.exclusive = False, + PIO.noctty = False, +-- PIO.nonBlock = False, + PIO.nonBlock = True, + -- In nonblocking mode opening for read will always succeed and + -- opening for write must happen second. + PIO.trunc = False + } +-- NOTE: +-- "The only open file status flags that can be meaningfully applied +-- to a pipe or FIFO are O_NONBLOCK and O_ASYNC. " + + mkTransport :: IO Transport mkTransport = do uid <- randomIO :: IO Word64 @@ -92,14 +111,17 @@ mkTransport = do -- OPTION (1) -- Here we protect from blocking other threads by running on a separate (OS) thread: --- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags --- fd <- takeMVar mv +-- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing fileFlags + mv <- onOSThread$ tryUntilNoIOErr $ + PIO.openFd filename PIO.WriteOnly Nothing fileFlags + fd <- takeMVar mv - -- OPTION (2) --- fd <- PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags + -- OPTION (2) / (3) +-- spinTillThere filename - -- OPTION (3) - fd <- PIO.openFd filename PIO.WriteOnly Nothing PIO.defaultFileFlags + -- The reader must connect first, the writer here spins with backoff: +-- fd <- PIO.openFd filename PIO.WriteOnly Nothing fileFlags +-- fd <- PIO.openFd filename PIO.ReadWrite Nothing fileFlags dbgprint1$ "GOT WRITING END OPEN ... " return $ @@ -138,22 +160,42 @@ mkTransport = do -- This should only happen on a single process. But it may -- happen on multiple threads so we grab a lock. takeMVar lock + dbgprint2$ " (got lock)" - mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing PIO.defaultFileFlags - fd <- takeMVar mv - let oneread :: Int64 -> IO (BS.ByteString, Int64) - oneread n = do bs <- readit fd (fromIntegral n) - return (bs, BS.length bs) + spinTillThere filename + fd <- PIO.openFd filename PIO.ReadOnly Nothing fileFlags +-- fd <- PIO.openFd filename PIO.ReadWrite Nothing fileFlags +-- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing fileFlags + dbgprint2$ " spawned thread for open..." +-- let dummy 0 acc = acc +-- dummy n acc = dummy (n-1) (1.1 * acc) +-- dbgprint2$ " Silly work: " ++ show (dummy 1000000 0.0001) + +-- fd <- readMVar mv + let -- oneread :: Int64 -> IO (BS.ByteString, Int64) + oneread fd n = +-- handle (\e -> do +-- dbgprint2$ " Got Exception in read call! "++ show e +-- case fromException e of +-- Nothing -> dbgprint2$ "It's NOT an IO exception! " +-- Just e -> dbgprint2$ "It's an IO exception! "++ show (e :: IOException) +-- -- throw (e :: SomeException) +-- return undefined +-- ) $ + do bs <- tryUntilNoIOErr$ + readit fd (fromIntegral n) + return (bs, BS.length bs) dbgprint2$ " Attempt read header..." let spinread :: Int64 -> IO BS.ByteString spinread desired = do #ifdef DEBUG - hPutStr stderr "." --- dbgprint2$ " SPINREAD "++ show desired - BSS.hPutStr stdout (BSS.pack$ " "++show desired) +-- hPutStr stderr "." + dbgprint2$ " SPINREAD "++ show desired +-- BSS.hPutStr stdout (BSS.pack$ " "++show desired) #endif - (bytes,len) <- oneread desired +-- fd <- readMVar mv + (bytes,len) <- oneread fd desired case len of n | n == desired -> return bytes 0 -> do threadDelay (10*1000) @@ -180,9 +222,53 @@ mkTransport = do return [payload] } +spinTillThere :: String -> IO () +spinTillThere filename = + do dbgprint2$ " Spinning till file present: "++ filename + loop 1 + where + maxwait = 10 * 1000 + loop t | t > maxwait = loop maxwait + loop t = do b <- doesFileExist filename + unless b $ do +#ifdef DEBUG + hPutStr stderr "?" +#endif + threadDelay t + loop (2 * t) +-- loop $ round (fromIntegral t * 1.5) + +data InfIO = InfIO (IO (InfIO)) + +-- mkBackoff :: IO (IO ()) + +mkBackoff = + do tref <- newIORef 1 + return$ do t <- readIORef tref + writeIORef tref (min (10 * 1000) (2 * t)) + threadDelay t + +tryUntilNoIOErr :: IO a -> IO a +tryUntilNoIOErr action = mkBackoff >>= loop + where + loop bkoff = + handle (\ (e :: IOException) -> bkoff >> loop bkoff) $ + action + + +-- loop (InfIO bkoff) = +-- handle (\ (e :: IOException) -> +-- do InfIO nxt <- bkoff +-- loop nxt) $ +-- action + + + + + #ifdef DEBUG -dbgprint1 s = BSS.hPutStrLn stderr (BSS.pack s) -dbgprint2 s = BSS.hPutStrLn stdout (BSS.pack s) +dbgprint1 s = do BSS.hPutStrLn stderr (BSS.pack s); hFlush stderr; +dbgprint2 s = do BSS.hPutStrLn stdout (BSS.pack s); hFlush stdout; #else dbgprint1 _ = return () dbgprint2 _ = return () diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index ba0f592c..2a0d485d 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -223,9 +223,13 @@ recvExact sock n = interceptAllExn msg = - Control.Exception.handle $ \ e -> do - BSS.hPutStrLn stderr $ BSS.pack$ "Exception inside "++msg++": "++show e - throw (e :: SomeException) + Control.Exception.handle $ \ e -> + case fromException e of + Just ThreadKilled -> throw e + Nothing -> do + BSS.hPutStrLn stderr $ BSS.pack$ "Exception inside "++msg++": "++show e + throw e +-- throw (e :: SomeException) forkWithExceptions :: (IO () -> IO ThreadId) -> String -> IO () -> IO ThreadId forkWithExceptions forkit descr action = do From b7cc73fa76bab1ffad52e7701967ac16f195e9d8 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 23:41:04 -0500 Subject: [PATCH 0035/2357] Fixed remaining demo. Pipes works for all 5. --- src/Network/Transport/Pipes.hs | 59 +++++++++++++--------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 4bb5827b..afc8e7d3 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -112,9 +112,10 @@ mkTransport = do -- OPTION (1) -- Here we protect from blocking other threads by running on a separate (OS) thread: -- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing fileFlags + dbgprint1$ "About to try opening writing end:" mv <- onOSThread$ tryUntilNoIOErr $ PIO.openFd filename PIO.WriteOnly Nothing fileFlags - fd <- takeMVar mv +-- fd <- takeMVar mv -- OPTION (2) / (3) -- spinTillThere filename @@ -143,7 +144,7 @@ mkTransport = do dbgprint1$ " Final send msg: " ++ show finalmsg -- OPTION 2: Speculative file opening, plus this synchronnization: --- fd <- readMVar mv + fd <- readMVar mv ---------------------------------------- cnt <- PIO.fdWrite fd (fromBS finalmsg) -- inefficient to use String here! unless (fromIntegral cnt == BS.length finalmsg) $ @@ -163,6 +164,7 @@ mkTransport = do dbgprint2$ " (got lock)" spinTillThere filename + -- Opening the file on the reader side should always succeed: fd <- PIO.openFd filename PIO.ReadOnly Nothing fileFlags -- fd <- PIO.openFd filename PIO.ReadWrite Nothing fileFlags -- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing fileFlags @@ -224,47 +226,38 @@ mkTransport = do spinTillThere :: String -> IO () spinTillThere filename = - do dbgprint2$ " Spinning till file present: "++ filename - loop 1 + do dbgprint2$ " Spinning till file present: "++ filename + mkBackoff >>= loop where - maxwait = 10 * 1000 - loop t | t > maxwait = loop maxwait - loop t = do b <- doesFileExist filename - unless b $ do -#ifdef DEBUG - hPutStr stderr "?" -#endif - threadDelay t - loop (2 * t) --- loop $ round (fromIntegral t * 1.5) - -data InfIO = InfIO (IO (InfIO)) + loop bkoff = do b <- doesFileExist filename + unless b $ do bkoff; loop bkoff --- mkBackoff :: IO (IO ()) +mkBackoff :: IO (IO ()) mkBackoff = do tref <- newIORef 1 return$ do t <- readIORef tref - writeIORef tref (min (10 * 1000) (2 * t)) + writeIORef tref (min maxwait (2 * t)) threadDelay t + where + maxwait = 50 * 1000 tryUntilNoIOErr :: IO a -> IO a tryUntilNoIOErr action = mkBackoff >>= loop where loop bkoff = - handle (\ (e :: IOException) -> bkoff >> loop bkoff) $ + handle (\ (e :: IOException) -> + do bkoff + dbgprint2$ " got IO exn: " ++ show e + loop bkoff) $ action - --- loop (InfIO bkoff) = --- handle (\ (e :: IOException) -> --- do InfIO nxt <- bkoff --- loop nxt) $ --- action - - - - +-- Execute an action on its own OS thread. Return an MVar to synchronize on. +onOSThread :: IO a -> IO (MVar a) +onOSThread action = do + mv <- newEmptyMVar + forkOS (action >>= putMVar mv ) + return mv #ifdef DEBUG dbgprint1 s = do BSS.hPutStrLn stderr (BSS.pack s); hFlush stderr; @@ -274,11 +267,3 @@ dbgprint1 _ = return () dbgprint2 _ = return () #endif - --- Execute an action on its own OS thread. Return an MVar to synchronize on. -onOSThread :: IO a -> IO (MVar a) -onOSThread action = do - mv <- newEmptyMVar - forkOS (action >>= putMVar mv ) - return mv --- [2012.02.19] This didn't seem to help. From 7253ab2b7c85d5661572bcf1159dcb469124ace8 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Mon, 20 Feb 2012 23:57:33 -0500 Subject: [PATCH 0036/2357] Cleaned out all unneed comments and debugging printfs in Pipes.hs --- src/Network/Transport/Pipes.hs | 89 +++++----------------------------- 1 file changed, 12 insertions(+), 77 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index afc8e7d3..4c679b0f 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -63,7 +63,6 @@ fileFlags = PIO.append = False, PIO.exclusive = False, PIO.noctty = False, --- PIO.nonBlock = False, PIO.nonBlock = True, -- In nonblocking mode opening for read will always succeed and -- opening for write must happen second. @@ -81,11 +80,8 @@ mkTransport = do let filename = "/tmp/pipe_"++show uid createNamedPipe filename $ unionFileModes ownerReadMode ownerWriteMode - dbgprint1$ " Created pipe at location: "++ filename - return Transport { newConnectionWith = \ _ -> do - dbgprint1$ " Creating new connection" return (mkSourceAddr filename, mkTargetEnd filename lock) , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" @@ -104,26 +100,11 @@ mkTransport = do mkSourceEnd :: String -> IO SourceEnd mkSourceEnd filename = do -- Initiate but do not block on file opening: - -- Note: Linux fifo semantics are NOT to block on open-RW, but this is not Posix standard. - -- - - -- All THREE of the below options were observed to work on a simple demo: - -- OPTION (1) -- Here we protect from blocking other threads by running on a separate (OS) thread: --- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing fileFlags - dbgprint1$ "About to try opening writing end:" mv <- onOSThread$ tryUntilNoIOErr $ + -- The reader must connect first, the writer here spins with backoff. PIO.openFd filename PIO.WriteOnly Nothing fileFlags --- fd <- takeMVar mv - - -- OPTION (2) / (3) --- spinTillThere filename - - -- The reader must connect first, the writer here spins with backoff: --- fd <- PIO.openFd filename PIO.WriteOnly Nothing fileFlags --- fd <- PIO.openFd filename PIO.ReadWrite Nothing fileFlags - dbgprint1$ "GOT WRITING END OPEN ... " return $ -- Write to the named pipe. If the message is less than @@ -131,9 +112,7 @@ mkTransport = do -- we have to do something more sophisticated. SourceEnd { send = \bss -> do - dbgprint1$ "Sending.. "++ show bss - - -- This may happen on multiple processes/threads: + -- ThreadSafe: This may happen on multiple processes/threads: let msgsize :: Word32 = fromIntegral$ foldl' (\n s -> n + BS.length s) 0 bss when (msgsize > 4096)$ -- TODO, look up PIPE_BUF in foreign code error "Message larger than blocksize written atomically to a named pipe. Unimplemented." @@ -141,98 +120,64 @@ mkTransport = do -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: -- let finalmsg = BS.concat (encode msgsize : bss) let finalmsg = BS.concat ((BS.fromChunks[encode msgsize]) : bss) - dbgprint1$ " Final send msg: " ++ show finalmsg - - -- OPTION 2: Speculative file opening, plus this synchronnization: - fd <- readMVar mv + + fd <- readMVar mv -- Synchronnize with file opening. ---------------------------------------- cnt <- PIO.fdWrite fd (fromBS finalmsg) -- inefficient to use String here! unless (fromIntegral cnt == BS.length finalmsg) $ error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) ---------------------------------------- - return () } mkTargetEnd :: String -> MVar () -> TargetEnd mkTargetEnd filename lock = TargetEnd { receive = do - dbgprint2$ "Begin receive action..." -- This should only happen on a single process. But it may -- happen on multiple threads so we grab a lock. takeMVar lock - dbgprint2$ " (got lock)" spinTillThere filename -- Opening the file on the reader side should always succeed: fd <- PIO.openFd filename PIO.ReadOnly Nothing fileFlags --- fd <- PIO.openFd filename PIO.ReadWrite Nothing fileFlags --- mv <- onOSThread$ PIO.openFd filename PIO.ReadWrite Nothing fileFlags - dbgprint2$ " spawned thread for open..." --- let dummy 0 acc = acc --- dummy n acc = dummy (n-1) (1.1 * acc) --- dbgprint2$ " Silly work: " ++ show (dummy 1000000 0.0001) --- fd <- readMVar mv - let -- oneread :: Int64 -> IO (BS.ByteString, Int64) - oneread fd n = --- handle (\e -> do --- dbgprint2$ " Got Exception in read call! "++ show e --- case fromException e of --- Nothing -> dbgprint2$ "It's NOT an IO exception! " --- Just e -> dbgprint2$ "It's an IO exception! "++ show (e :: IOException) --- -- throw (e :: SomeException) --- return undefined --- ) $ + let oneread fd n = do bs <- tryUntilNoIOErr$ readit fd (fromIntegral n) return (bs, BS.length bs) - dbgprint2$ " Attempt read header..." let spinread :: Int64 -> IO BS.ByteString spinread desired = do -#ifdef DEBUG --- hPutStr stderr "." - dbgprint2$ " SPINREAD "++ show desired --- BSS.hPutStr stdout (BSS.pack$ " "++show desired) -#endif --- fd <- readMVar mv - (bytes,len) <- oneread fd desired - case len of - n | n == desired -> return bytes + + bs <- tryUntilNoIOErr$ + readit fd (fromIntegral desired) + + case BS.length bs of + n | n == desired -> return bs 0 -> do threadDelay (10*1000) spinread desired l -> error$ "Inclomplete read expected either 0 bytes or complete msg ("++ show desired ++" bytes) got "++ show l ++ " bytes" hdr <- spinread sizeof_header - dbgprint2$ " Got header "++ show hdr ++ ", next attempt to read payload:" #ifdef CEREAL let decoded = decode (BSS.concat$ BS.toChunks hdr) --- dbgprint2$ " DECODING HDR, bytes "++ show (BS.length hdr) ++ ": "++show hdr --- evaluate decoded --- dbgprint2$ " DONE DECODING HDR" #else let decoded = decode hdr #endif payload <- case decoded of Left err -> error$ "ERROR: "++ err Right size -> spinread (fromIntegral (size::Word32)) - dbgprint2$ " Got payload "++ show payload - putMVar lock () return [payload] } spinTillThere :: String -> IO () -spinTillThere filename = - do dbgprint2$ " Spinning till file present: "++ filename - mkBackoff >>= loop +spinTillThere filename = mkBackoff >>= loop where loop bkoff = do b <- doesFileExist filename unless b $ do bkoff; loop bkoff - mkBackoff :: IO (IO ()) mkBackoff = do tref <- newIORef 1 @@ -248,7 +193,6 @@ tryUntilNoIOErr action = mkBackoff >>= loop loop bkoff = handle (\ (e :: IOException) -> do bkoff - dbgprint2$ " got IO exn: " ++ show e loop bkoff) $ action @@ -258,12 +202,3 @@ onOSThread action = do mv <- newEmptyMVar forkOS (action >>= putMVar mv ) return mv - -#ifdef DEBUG -dbgprint1 s = do BSS.hPutStrLn stderr (BSS.pack s); hFlush stderr; -dbgprint2 s = do BSS.hPutStrLn stdout (BSS.pack s); hFlush stdout; -#else -dbgprint1 _ = return () -dbgprint2 _ = return () -#endif - From 2dd9e5ae4b154c65f326cfb852972ed1fec3e805 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Tue, 21 Feb 2012 00:06:37 -0500 Subject: [PATCH 0037/2357] Added a close routine for Pipes that removes the named pipe from disk --- src/Network/Transport/Pipes.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 4c679b0f..4aea06a9 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -33,7 +33,7 @@ import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), openFile, hClose, hPutStrLn, hPutStr, stderr, stdout, hFlush) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) import System.Posix.Types (Fd) -import System.Directory (doesFileExist) +import System.Directory (doesFileExist, removeFile) -- define DEBUG -- define USE_UNIX_BYTESTRING @@ -86,9 +86,7 @@ mkTransport = do mkTargetEnd filename lock) , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" , deserialize = \bs -> return$ mkSourceAddr (BS.unpack bs) - , closeTransport = do --- removeFile filename - return () + , closeTransport = removeFile filename } where mkSourceAddr :: String -> SourceAddr @@ -128,6 +126,7 @@ mkTransport = do error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) ---------------------------------------- return () + , closeSourceEnd = error "Pipes.hs: closeSourceEnd not yet implemented" } mkTargetEnd :: String -> MVar () -> TargetEnd @@ -170,6 +169,7 @@ mkTransport = do Right size -> spinread (fromIntegral (size::Word32)) putMVar lock () return [payload] + , closeTargetEnd = error "Pipes.hs: closeTargetEnd not yet implemented" } spinTillThere :: String -> IO () From dce608d08e3850605eddbc47c3eb5c98fb39f2f5 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Tue, 21 Feb 2012 00:19:51 -0500 Subject: [PATCH 0038/2357] Minor, add warning at the top of Pipes about it being unfinished --- src/Network/Transport/Pipes.hs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 4aea06a9..a410c37c 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -1,5 +1,15 @@ {-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables, PackageImports #-} +---------------------------------------------------------------------------------------------------- + +-- WARNING -- This transport is not yet completed. TODO: + +-- * Add support for messages greater than 4096 bytes. +-- * debug ODD problem with CEREAL below +-- * switch to unix-bytestring after that package is updated for 7.4.1 + +---------------------------------------------------------------------------------------------------- + module Network.Transport.Pipes ( mkTransport ) where @@ -57,7 +67,6 @@ readit fd n = do (s,_) <- PIO.fdRead fd n -- The msg header consists of just a length field represented as a Word32 sizeof_header = 4 --- fileFlags = PIO.defaultFileFlags fileFlags = PIO.OpenFileFlags { PIO.append = False, From a7fa1270bd980a3092522b3232397199064498b2 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Tue, 21 Feb 2012 16:54:25 -0500 Subject: [PATCH 0039/2357] Fixed the pipes transport so it opens the file only once on the reader side. I'm observing a 72 us latency for PingTCP (vs. 28us for the equivalent C program). I'm observing a 99 us latency for PingPipes, and it goes up to 100% CPU. That's with the *String* based Pipes, however! --- src/Network/Transport/Pipes.hs | 46 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index a410c37c..70919123 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -41,16 +41,19 @@ import Network.Transport import System.Random (randomIO) import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), openFile, hClose, hPutStrLn, hPutStr, stderr, stdout, hFlush) +import System.IO.Error (ioeGetHandle) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) import System.Posix.Types (Fd) import System.Directory (doesFileExist, removeFile) +import System.Mem (performGC) -- define DEBUG -- define USE_UNIX_BYTESTRING #ifdef USE_UNIX_BYTESTRING import qualified "unix-bytestring" System.Posix.IO.ByteString as PIO -import System.Posix.IO as PIO (openFd, defaultFileFlags, OpenMode(ReadWrite, WriteOnly)) +import System.Posix.IO as PIO (openFd, closeFd, defaultFileFlags, + OpenMode(ReadWrite, WriteOnly)) (fromS,toS) = (BS.pack, BS.unpack) (fromBS,toBS) = (id,id) readit fd n = PIO.fdRead fd n @@ -91,8 +94,14 @@ mkTransport = do return Transport { newConnectionWith = \ _ -> do + -- Here we protect from blocking other threads by running on a separate (OS) thread: + -- Opening the file on the reader side should always succeed: + mv <- onOSThread$ tryUntilNoIOErr $ +-- spinTillThere filename + PIO.openFd filename PIO.ReadOnly Nothing fileFlags + return (mkSourceAddr filename, - mkTargetEnd filename lock) + mkTargetEnd mv lock) , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" , deserialize = \bs -> return$ mkSourceAddr (BS.unpack bs) , closeTransport = removeFile filename @@ -107,11 +116,9 @@ mkTransport = do mkSourceEnd :: String -> IO SourceEnd mkSourceEnd filename = do -- Initiate but do not block on file opening: - - -- Here we protect from blocking other threads by running on a separate (OS) thread: mv <- onOSThread$ tryUntilNoIOErr $ -- The reader must connect first, the writer here spins with backoff. - PIO.openFd filename PIO.WriteOnly Nothing fileFlags + PIO.openFd filename PIO.WriteOnly Nothing fileFlags return $ -- Write to the named pipe. If the message is less than @@ -128,31 +135,26 @@ mkTransport = do -- let finalmsg = BS.concat (encode msgsize : bss) let finalmsg = BS.concat ((BS.fromChunks[encode msgsize]) : bss) - fd <- readMVar mv -- Synchronnize with file opening. + fd <- readMVar mv -- Synchronize with file opening. ---------------------------------------- cnt <- PIO.fdWrite fd (fromBS finalmsg) -- inefficient to use String here! unless (fromIntegral cnt == BS.length finalmsg) $ error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) ---------------------------------------- return () - , closeSourceEnd = error "Pipes.hs: closeSourceEnd not yet implemented" + , closeSourceEnd = do + fd <- readMVar mv + PIO.closeFd fd } - mkTargetEnd :: String -> MVar () -> TargetEnd - mkTargetEnd filename lock = TargetEnd + mkTargetEnd :: MVar Fd -> MVar () -> TargetEnd + mkTargetEnd mv lock = TargetEnd { receive = do + fd <- readMVar mv -- Make sure Fd is there before + -- This should only happen on a single process. But it may -- happen on multiple threads so we grab a lock. takeMVar lock - - spinTillThere filename - -- Opening the file on the reader side should always succeed: - fd <- PIO.openFd filename PIO.ReadOnly Nothing fileFlags - - let oneread fd n = - do bs <- tryUntilNoIOErr$ - readit fd (fromIntegral n) - return (bs, BS.length bs) let spinread :: Int64 -> IO BS.ByteString spinread desired = do @@ -178,7 +180,9 @@ mkTransport = do Right size -> spinread (fromIntegral (size::Word32)) putMVar lock () return [payload] - , closeTargetEnd = error "Pipes.hs: closeTargetEnd not yet implemented" + , closeTargetEnd = do + fd <- readMVar mv + PIO.closeFd fd } spinTillThere :: String -> IO () @@ -202,6 +206,10 @@ tryUntilNoIOErr action = mkBackoff >>= loop loop bkoff = handle (\ (e :: IOException) -> do bkoff +-- BSS.hPutStr stderr$ BSS.pack$ " got IO err: " ++ show e + -- case ioeGetHandle e of + -- Nothing -> BSS.hPutStrLn stderr$ BSS.pack$ " no hndl io err." + -- Just x -> BSS.hPutStrLn stderr$ BSS.pack$ " HNDL on io err!" ++ show x loop bkoff) $ action From 1f5379d4665d5ef7d46f8658d6f5abd66e308221 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Wed, 22 Feb 2012 15:39:22 -0500 Subject: [PATCH 0040/2357] Got the Pipes backend working with unix-bytestring and normal unix. --- network-transport.cabal | 4 +-- src/Network/Transport/Pipes.hs | 54 +++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 6f7efde6..0a20be9b 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -16,8 +16,8 @@ Library containers >= 0.4, network >= 2.3, safe >= 0.3, - unix >= 2.5.0.0, random, cereal --- , unix-bytestring + unix >= 2.5.0.0, random, cereal, directory, + unix-bytestring >= 0.3.5.3 Exposed-modules: Network.Transport, Network.Transport.MVar, Network.Transport.TCP, diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs index 70919123..00bd60f0 100644 --- a/src/Network/Transport/Pipes.hs +++ b/src/Network/Transport/Pipes.hs @@ -43,27 +43,29 @@ import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), openFile, hClose, hPutStrLn, hPutStr, stderr, stdout, hFlush) import System.IO.Error (ioeGetHandle) import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) -import System.Posix.Types (Fd) +import System.Posix.Types (Fd, ByteCount) import System.Directory (doesFileExist, removeFile) import System.Mem (performGC) --- define DEBUG --- define USE_UNIX_BYTESTRING - +#define USE_UNIX_BYTESTRING #ifdef USE_UNIX_BYTESTRING import qualified "unix-bytestring" System.Posix.IO.ByteString as PIO -import System.Posix.IO as PIO (openFd, closeFd, defaultFileFlags, - OpenMode(ReadWrite, WriteOnly)) -(fromS,toS) = (BS.pack, BS.unpack) -(fromBS,toBS) = (id,id) +-- import qualified "unix-bytestring" System.Posix.IO.ByteString.Lazy as PIO +import System.Posix.IO as PIO (openFd, closeFd, -- append, exclusive, noctty, nonBlock, trunc, + OpenFileFlags(..), OpenMode(ReadOnly, WriteOnly)) +-- (fromS,toS) = (BS.pack, BS.unpack) +(fromS,toS) = (BSS.pack, BSS.unpack) +fromBS = id readit fd n = PIO.fdRead fd n #else import qualified System.Posix.IO as PIO (toS,fromS) = (id,id) -(fromBS,toBS) = (BS.unpack, BS.pack) +fromBS = BSS.unpack readit fd n = do (s,_) <- PIO.fdRead fd n - return (BS.pack s) + return (BSS.pack s) #endif +-- readit :: Fd -> Int -> IO BS.ByteString +readit :: Fd -> ByteCount -> IO BSS.ByteString ---------------------------------------------------------------------------------------------------- @@ -125,21 +127,30 @@ mkTransport = do -- PIPE_BUF (4KB on linux) then this should be atomic, otherwise -- we have to do something more sophisticated. SourceEnd - { send = \bss -> do + { send = \bsls -> do -- ThreadSafe: This may happen on multiple processes/threads: - let msgsize :: Word32 = fromIntegral$ foldl' (\n s -> n + BS.length s) 0 bss + let msgsize :: Word32 = fromIntegral$ foldl' (\n s -> n + BS.length s) 0 bsls when (msgsize > 4096)$ -- TODO, look up PIPE_BUF in foreign code error "Message larger than blocksize written atomically to a named pipe. Unimplemented." -- Otherwise it's just a simple write: -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: + + let hdrbss :: BSS.ByteString +#ifdef CEREAL + hdrbss = encode msgsize +#else +-- hdrbss = BS.fromChunks[encode msgsize] + [hdrbss] = BS.toChunks (encode msgsize) +#endif + -- let finalmsg = BS.concat (encode msgsize : bss) - let finalmsg = BS.concat ((BS.fromChunks[encode msgsize]) : bss) + let finalmsg = BSS.concat (hdrbss : concatMap BS.toChunks bsls) fd <- readMVar mv -- Synchronize with file opening. ---------------------------------------- cnt <- PIO.fdWrite fd (fromBS finalmsg) -- inefficient to use String here! - unless (fromIntegral cnt == BS.length finalmsg) $ - error$ "Failed to write message in one go, length: "++ show (BS.length finalmsg) + unless (fromIntegral cnt == BSS.length finalmsg) $ + error$ "Failed to write message in one go, length: "++ show (BSS.length finalmsg) ---------------------------------------- return () , closeSourceEnd = do @@ -156,13 +167,13 @@ mkTransport = do -- happen on multiple threads so we grab a lock. takeMVar lock - let spinread :: Int64 -> IO BS.ByteString + let spinread :: Int -> IO BSS.ByteString spinread desired = do bs <- tryUntilNoIOErr$ readit fd (fromIntegral desired) - case BS.length bs of + case BSS.length bs of n | n == desired -> return bs 0 -> do threadDelay (10*1000) spinread desired @@ -170,16 +181,19 @@ mkTransport = do show desired ++" bytes) got "++ show l ++ " bytes" hdr <- spinread sizeof_header + let -- decoded :: BSS.ByteString #ifdef CEREAL - let decoded = decode (BSS.concat$ BS.toChunks hdr) +-- decoded = decode (BSS.concat$ BS.toChunks hdr) + decoded = decode hdr #else - let decoded = decode hdr +-- decoded = decode hdr + decoded = decode (BS.fromChunks [hdr]) #endif payload <- case decoded of Left err -> error$ "ERROR: "++ err Right size -> spinread (fromIntegral (size::Word32)) putMVar lock () - return [payload] + return [BS.fromChunks [payload]] -- How terribly listy. , closeTargetEnd = do fd <- readMVar mv PIO.closeFd fd From 442486747274dd153ab655dadd3796413b64d0fe Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Wed, 22 Feb 2012 15:49:21 -0500 Subject: [PATCH 0041/2357] Factored pipes backend into its own package. One reason for this is that it introduces some additional dependencies, and we always want to minimize dependencies. --- network-transport.cabal | 6 +- src/Network/Transport/Pipes.hs | 235 --------------------------------- 2 files changed, 2 insertions(+), 239 deletions(-) delete mode 100644 src/Network/Transport/Pipes.hs diff --git a/network-transport.cabal b/network-transport.cabal index 0a20be9b..c9118ea6 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -16,12 +16,10 @@ Library containers >= 0.4, network >= 2.3, safe >= 0.3, - unix >= 2.5.0.0, random, cereal, directory, - unix-bytestring >= 0.3.5.3 + unix >= 2.5.0.0 Exposed-modules: Network.Transport, Network.Transport.MVar, - Network.Transport.TCP, - Network.Transport.Pipes + Network.Transport.TCP Extensions: BangPatterns ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Network/Transport/Pipes.hs b/src/Network/Transport/Pipes.hs deleted file mode 100644 index 00bd60f0..00000000 --- a/src/Network/Transport/Pipes.hs +++ /dev/null @@ -1,235 +0,0 @@ -{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables, PackageImports #-} - ----------------------------------------------------------------------------------------------------- - --- WARNING -- This transport is not yet completed. TODO: - --- * Add support for messages greater than 4096 bytes. --- * debug ODD problem with CEREAL below --- * switch to unix-bytestring after that package is updated for 7.4.1 - ----------------------------------------------------------------------------------------------------- - -module Network.Transport.Pipes - ( mkTransport - ) where - -import Control.Monad (when, unless) -import Control.Exception (evaluate, throw, handle, fromException, - SomeException, IOException) -import Control.Concurrent.MVar -import Control.Concurrent (threadDelay, forkOS) -import Data.IntMap (IntMap) --- import Data.ByteString.Char8 (ByteString) -import Data.Word -import Data.Int -import Data.IORef -import qualified Data.IntMap as IntMap -import qualified Data.ByteString.Char8 as BSS -import qualified Data.ByteString.Lazy.Char8 as BS - --- For some STRANGE reason this is not working with Data.Binary [2012.02.20]: -#define CEREAL -#ifdef CEREAL -import Data.Serialize (encode,decode) -- Uses strict BS -#else -import Data.Binary (encode,decode) -- Uses lazy BS -#endif - -import Data.List (foldl') -import Network.Transport -import System.Random (randomIO) -import System.IO (IOMode(ReadMode,AppendMode,WriteMode,ReadWriteMode), - openFile, hClose, hPutStrLn, hPutStr, stderr, stdout, hFlush) -import System.IO.Error (ioeGetHandle) -import System.Posix.Files (createNamedPipe, unionFileModes, ownerReadMode, ownerWriteMode) -import System.Posix.Types (Fd, ByteCount) -import System.Directory (doesFileExist, removeFile) -import System.Mem (performGC) - -#define USE_UNIX_BYTESTRING -#ifdef USE_UNIX_BYTESTRING -import qualified "unix-bytestring" System.Posix.IO.ByteString as PIO --- import qualified "unix-bytestring" System.Posix.IO.ByteString.Lazy as PIO -import System.Posix.IO as PIO (openFd, closeFd, -- append, exclusive, noctty, nonBlock, trunc, - OpenFileFlags(..), OpenMode(ReadOnly, WriteOnly)) --- (fromS,toS) = (BS.pack, BS.unpack) -(fromS,toS) = (BSS.pack, BSS.unpack) -fromBS = id -readit fd n = PIO.fdRead fd n -#else -import qualified System.Posix.IO as PIO -(toS,fromS) = (id,id) -fromBS = BSS.unpack -readit fd n = do (s,_) <- PIO.fdRead fd n - return (BSS.pack s) -#endif --- readit :: Fd -> Int -> IO BS.ByteString -readit :: Fd -> ByteCount -> IO BSS.ByteString - ----------------------------------------------------------------------------------------------------- - --- The msg header consists of just a length field represented as a Word32 -sizeof_header = 4 - -fileFlags = - PIO.OpenFileFlags { - PIO.append = False, - PIO.exclusive = False, - PIO.noctty = False, - PIO.nonBlock = True, - -- In nonblocking mode opening for read will always succeed and - -- opening for write must happen second. - PIO.trunc = False - } --- NOTE: --- "The only open file status flags that can be meaningfully applied --- to a pipe or FIFO are O_NONBLOCK and O_ASYNC. " - - -mkTransport :: IO Transport -mkTransport = do - uid <- randomIO :: IO Word64 - lock <- newMVar () - let filename = "/tmp/pipe_"++show uid - createNamedPipe filename $ unionFileModes ownerReadMode ownerWriteMode - - return Transport - { newConnectionWith = \ _ -> do - -- Here we protect from blocking other threads by running on a separate (OS) thread: - -- Opening the file on the reader side should always succeed: - mv <- onOSThread$ tryUntilNoIOErr $ --- spinTillThere filename - PIO.openFd filename PIO.ReadOnly Nothing fileFlags - - return (mkSourceAddr filename, - mkTargetEnd mv lock) - , newMulticastWith = error "Pipes.hs: newMulticastWith not implemented yet" - , deserialize = \bs -> return$ mkSourceAddr (BS.unpack bs) - , closeTransport = removeFile filename - } - where - mkSourceAddr :: String -> SourceAddr - mkSourceAddr filename = SourceAddr - { connectWith = \_ -> mkSourceEnd filename - , serialize = BS.pack filename - } - - mkSourceEnd :: String -> IO SourceEnd - mkSourceEnd filename = do - -- Initiate but do not block on file opening: - mv <- onOSThread$ tryUntilNoIOErr $ - -- The reader must connect first, the writer here spins with backoff. - PIO.openFd filename PIO.WriteOnly Nothing fileFlags - - return $ - -- Write to the named pipe. If the message is less than - -- PIPE_BUF (4KB on linux) then this should be atomic, otherwise - -- we have to do something more sophisticated. - SourceEnd - { send = \bsls -> do - -- ThreadSafe: This may happen on multiple processes/threads: - let msgsize :: Word32 = fromIntegral$ foldl' (\n s -> n + BS.length s) 0 bsls - when (msgsize > 4096)$ -- TODO, look up PIPE_BUF in foreign code - error "Message larger than blocksize written atomically to a named pipe. Unimplemented." - -- Otherwise it's just a simple write: - -- We append the length as a header. TODO - REMOVE EXTRA COPY HERE: - - let hdrbss :: BSS.ByteString -#ifdef CEREAL - hdrbss = encode msgsize -#else --- hdrbss = BS.fromChunks[encode msgsize] - [hdrbss] = BS.toChunks (encode msgsize) -#endif - --- let finalmsg = BS.concat (encode msgsize : bss) - let finalmsg = BSS.concat (hdrbss : concatMap BS.toChunks bsls) - - fd <- readMVar mv -- Synchronize with file opening. - ---------------------------------------- - cnt <- PIO.fdWrite fd (fromBS finalmsg) -- inefficient to use String here! - unless (fromIntegral cnt == BSS.length finalmsg) $ - error$ "Failed to write message in one go, length: "++ show (BSS.length finalmsg) - ---------------------------------------- - return () - , closeSourceEnd = do - fd <- readMVar mv - PIO.closeFd fd - } - - mkTargetEnd :: MVar Fd -> MVar () -> TargetEnd - mkTargetEnd mv lock = TargetEnd - { receive = do - fd <- readMVar mv -- Make sure Fd is there before - - -- This should only happen on a single process. But it may - -- happen on multiple threads so we grab a lock. - takeMVar lock - - let spinread :: Int -> IO BSS.ByteString - spinread desired = do - - bs <- tryUntilNoIOErr$ - readit fd (fromIntegral desired) - - case BSS.length bs of - n | n == desired -> return bs - 0 -> do threadDelay (10*1000) - spinread desired - l -> error$ "Inclomplete read expected either 0 bytes or complete msg ("++ - show desired ++" bytes) got "++ show l ++ " bytes" - - hdr <- spinread sizeof_header - let -- decoded :: BSS.ByteString -#ifdef CEREAL --- decoded = decode (BSS.concat$ BS.toChunks hdr) - decoded = decode hdr -#else --- decoded = decode hdr - decoded = decode (BS.fromChunks [hdr]) -#endif - payload <- case decoded of - Left err -> error$ "ERROR: "++ err - Right size -> spinread (fromIntegral (size::Word32)) - putMVar lock () - return [BS.fromChunks [payload]] -- How terribly listy. - , closeTargetEnd = do - fd <- readMVar mv - PIO.closeFd fd - } - -spinTillThere :: String -> IO () -spinTillThere filename = mkBackoff >>= loop - where - loop bkoff = do b <- doesFileExist filename - unless b $ do bkoff; loop bkoff - -mkBackoff :: IO (IO ()) -mkBackoff = - do tref <- newIORef 1 - return$ do t <- readIORef tref - writeIORef tref (min maxwait (2 * t)) - threadDelay t - where - maxwait = 50 * 1000 - -tryUntilNoIOErr :: IO a -> IO a -tryUntilNoIOErr action = mkBackoff >>= loop - where - loop bkoff = - handle (\ (e :: IOException) -> - do bkoff --- BSS.hPutStr stderr$ BSS.pack$ " got IO err: " ++ show e - -- case ioeGetHandle e of - -- Nothing -> BSS.hPutStrLn stderr$ BSS.pack$ " no hndl io err." - -- Just x -> BSS.hPutStrLn stderr$ BSS.pack$ " HNDL on io err!" ++ show x - loop bkoff) $ - action - --- Execute an action on its own OS thread. Return an MVar to synchronize on. -onOSThread :: IO a -> IO (MVar a) -onOSThread action = do - mv <- newEmptyMVar - forkOS (action >>= putMVar mv ) - return mv From f0479b3aeaebe2ead3275870fefe02612c3ad0f2 Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Wed, 22 Feb 2012 16:43:56 -0500 Subject: [PATCH 0042/2357] strictified, switched back to cereal in preparation for using conduits --- network-transport.cabal | 3 ++ src/Network/Transport.hs | 2 +- src/Network/Transport/MVar.hs | 13 ++++--- src/Network/Transport/TCP.hs | 72 ++++++++++++++++++++++------------- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 168d6e28..dc808e15 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -13,8 +13,11 @@ Library Build-Depends: base >= 3 && < 5, binary >= 0.5, bytestring >= 0.9, + cereal >= 0.3, + conduit >= 0.2, containers >= 0.4, network >= 2.3, + network-conduit >= 0.2, safe >= 0.3 Exposed-modules: Network.Transport, Network.Transport.MVar, diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index bdc0b5fa..6f0efd8c 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -12,7 +12,7 @@ module Network.Transport , newMulticast ) where -import Data.ByteString.Lazy.Char8 (ByteString) +import Data.ByteString (ByteString) -- | The `Hints` and `SourceHints` provide hints to the underlying transport -- about the kind of connection that is required. This might include details diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs index 801575a6..2f57fff3 100644 --- a/src/Network/Transport/MVar.hs +++ b/src/Network/Transport/MVar.hs @@ -6,10 +6,11 @@ module Network.Transport.MVar import Control.Concurrent.MVar import Data.IntMap (IntMap) -import Data.ByteString.Lazy.Char8 (ByteString) +import Data.ByteString (ByteString) +import Data.Serialize import qualified Data.IntMap as IntMap -import qualified Data.ByteString.Lazy.Char8 as BS +import qualified Data.ByteString as BS import Network.Transport @@ -29,15 +30,15 @@ mkTransport = do return (mkSourceAddr channels sourceAddr, mkTargetEnd receiveChan) , newMulticastWith = undefined , deserialize = \bs -> - case BS.readInt bs of - Nothing -> error "dummyBackend.deserializeSourceEnd: cannot parse" - Just (n,_) -> Just . mkSourceAddr channels $ n + either (error "dummyBackend.deserializeSourceEnd: cannot parse") + (Just . mkSourceAddr channels) + (decode bs) } where mkSourceAddr :: Chans -> Int -> SourceAddr mkSourceAddr channels addr = SourceAddr { connectWith = \_ -> mkSourceEnd channels addr - , serialize = BS.pack (show addr) + , serialize = encode addr } mkSourceEnd :: Chans -> Int -> IO SourceEnd diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 37c007fd..66b8843f 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -5,26 +5,41 @@ module Network.Transport.TCP import Network.Transport +import Control.Applicative + import Control.Concurrent (forkIO, ThreadId, killThread) import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Monad (forever, forM_) -import Data.ByteString.Lazy.Char8 (ByteString) + +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS + +import Data.Conduit +import qualified Data.Conduit.Binary as CB +import Data.Conduit.Network + import Data.IntMap (IntMap) +import qualified Data.IntMap as IntMap + import Data.Int +import Data.Serialize import Data.Word + import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket , SocketType (Stream), SocketOption (ReuseAddr) , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import qualified Network.Socket as N +import qualified Network.Socket.ByteString as NBS + +import Text.Printf + import Safe import qualified Data.Binary as B -import qualified Data.ByteString.Lazy.Char8 as BS -import qualified Data.IntMap as IntMap -import qualified Network.Socket as N -import qualified Network.Socket.ByteString.Lazy as NBS + type ChanId = Int type Chans = MVar (ChanId, IntMap (Chan [ByteString], [(ThreadId, Socket)])) @@ -65,7 +80,8 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do return (mkSourceAddr host service chanId, mkTargetEnd chans chanId chan) , newMulticastWith = error "newMulticastWith: not defined" , deserialize = {-# SCC "deserialize" #-} \bs -> - let (host, service, chanId) = B.decode bs in + let (host, service, chanId) = + either (error $ printf "deserialize: %s") id $ decode bs in Just $ mkSourceAddr host service chanId , closeTransport = {-# SCC "closeTransport" #-} do -- Kill the transport channel process @@ -82,28 +98,30 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr mkSourceAddr host service chanId = SourceAddr { connectWith = {-# SCC "connectWith" #-} \_ -> mkSourceEnd host service chanId - , serialize = {-# SCC "serialize" #-} B.encode (host, service, chanId) + , serialize = {-# SCC "serialize" #-} encode (host, service, chanId) } mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd mkSourceEnd host service chanId = withSocketsDo $ do + let err = error $ printf "mkSourceEnd: %s" serverAddrs <- getAddrInfo Nothing (Just host) (Just service) let serverAddr = case serverAddrs of - [] -> error "mkSourceEnd: getAddrInfo returned []" + [] -> err "getAddrInfo returned []" as -> head as sock <- socket (addrFamily serverAddr) Stream defaultProtocol setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) - NBS.sendAll sock $ B.encode (fromIntegral chanId :: Int64) +-- let sink = sinkSocket sock :: Sink ByteString IO () + NBS.sendAll sock $ encode (fromIntegral chanId :: Int64) return SourceEnd { send = {-# SCC "send" #-} \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int64 if size < 255 then - NBS.sendAll sock (B.encode (fromIntegral size :: Word8)) + NBS.sendAll sock (encode (fromIntegral size :: Word8)) else do - NBS.sendAll sock (B.encode (255 :: Word8)) - NBS.sendAll sock (B.encode size) + NBS.sendAll sock (encode (255 :: Word8)) + NBS.sendAll sock (encode size) mapM_ (NBS.sendAll sock) bss , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } @@ -128,19 +146,20 @@ mkTargetEnd chans chanId chan = TargetEnd -- for some reason, an error is raised. procConnections :: Chans -> Socket -> IO () procConnections chans sock = forever $ do + let err = error $ printf "procConnections: %s" (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId bss <- recvExact clientSock 8 case bss of - [] -> error "procConnections: inbound chanId aborted" + [] -> err "inbound chanId aborted" bss -> do let bs = BS.concat bss - let chanId = fromIntegral (B.decode bs :: Int64) + let chanId = either err fromIntegral (decode bs :: Either String Int64) (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of Nothing -> do putMVar chans (chanId', chanMap) - error "procConnections: cannot find chanId" + err "cannot find chanId" Just (chan, socks) -> do threadId <- forkIO $ procMessages chans chanId chan clientSock let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap @@ -159,29 +178,30 @@ procMessages chans chanId chan sock = do case sizeBSs of [] -> closeSocket _ -> do - let size = fromIntegral (B.decode . BS.concat $ sizeBSs :: Word8) - if size == 255 - then do + case decode . BS.concat $ sizeBSs :: Either String Word8 of + Right 255 -> do sizeBSs' <- recvExact sock 8 case sizeBSs' of [] -> closeSocket - _ -> procMessage (B.decode . BS.concat $ sizeBSs' :: Int64) - else procMessage size + _ -> procMessage (decode . BS.concat $ sizeBSs') + esize -> procMessage (fromIntegral <$> esize) where + err = error $ printf "procMessages: %s" closeSocket :: IO () closeSocket = do (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of Nothing -> do putMVar chans (chanId', chanMap) - error "procMessages: chanId not found." + err "chanId not found." Just (chan, socks) -> do let socks' = filter ((/= sock) . snd) socks let chanMap' = IntMap.insert chanId (chan, socks') chanMap putMVar chans (chanId', chanMap') sClose sock - procMessage :: Int64 -> IO () - procMessage size = do + procMessage :: Either String Int64 -> IO () + procMessage (Left msg) = err msg + procMessage (Right size) = do bss <- recvExact sock size case bss of [] -> closeSocket @@ -202,13 +222,13 @@ procMessages chans chanId chan sock = do -- indicates success. This hasn't been implemented, since a Transport `receive` -- represents an atomic receipt of a message. recvExact :: Socket -> Int64 -> IO [ByteString] -recvExact sock n = go [] sock n +recvExact sock n = go [] sock (fromIntegral n) where - go :: [ByteString] -> Socket -> Int64 -> IO [ByteString] + go :: [ByteString] -> Socket -> Int -> IO [ByteString] go bss _ 0 = return (reverse bss) go bss sock n = do bs <- NBS.recv sock n if BS.null bs then return [] - else go (bs:bss) sock (n - BS.length bs) + else go (bs:bss) sock (n - (fromIntegral $ BS.length bs)) From 3b76e64f987e3e1fa102d1d4110704bb619706ab Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Wed, 22 Feb 2012 17:43:14 -0500 Subject: [PATCH 0043/2357] efficient implementation of recvExact using bytestring internals --- src/Network/Transport/TCP.hs | 61 +++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 66b8843f..e68c770d 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -14,6 +14,7 @@ import Control.Monad (forever, forM_) import Data.ByteString (ByteString) import qualified Data.ByteString as BS +import Data.ByteString.Internal import Data.Conduit import qualified Data.Conduit.Binary as CB @@ -26,6 +27,9 @@ import Data.Int import Data.Serialize import Data.Word +import Foreign.ForeignPtr (withForeignPtr) +import Foreign.Ptr (Ptr, plusPtr) + import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket , SocketType (Stream), SocketOption (ReuseAddr) @@ -151,9 +155,8 @@ procConnections chans sock = forever $ do -- decode the first message to find the correct chanId bss <- recvExact clientSock 8 case bss of - [] -> err "inbound chanId aborted" - bss -> do - let bs = BS.concat bss + Left _ -> err "inbound chanId aborted" + Right bs -> do let chanId = either err fromIntegral (decode bs :: Either String Int64) (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of @@ -174,16 +177,16 @@ procConnections chans sock = forever $ do -- socket has closed. procMessages :: Chans -> ChanId -> Chan [ByteString] -> Socket -> IO () procMessages chans chanId chan sock = do - sizeBSs <- recvExact sock 1 - case sizeBSs of - [] -> closeSocket - _ -> do - case decode . BS.concat $ sizeBSs :: Either String Word8 of + esizeBS <- recvExact sock 1 + case esizeBS of + Left _ -> closeSocket + Right sizeBS -> do + case decode sizeBS :: Either String Word8 of Right 255 -> do - sizeBSs' <- recvExact sock 8 - case sizeBSs' of - [] -> closeSocket - _ -> procMessage (decode . BS.concat $ sizeBSs') + esizeBS' <- recvExact sock 8 + case esizeBS' of + Left _ -> closeSocket + Right sizeBS' -> procMessage $ decode sizeBS' esize -> procMessage (fromIntegral <$> esize) where err = error $ printf "procMessages: %s" @@ -202,11 +205,11 @@ procMessages chans chanId chan sock = do procMessage :: Either String Int64 -> IO () procMessage (Left msg) = err msg procMessage (Right size) = do - bss <- recvExact sock size - case bss of - [] -> closeSocket - _ -> do - writeChan chan bss + ebs <- recvExact sock size + case ebs of + Left _ -> closeSocket + Right bs -> do + writeChan chan [bs] procMessages chans chanId chan sock -- | The result of `recvExact sock n` is a `[ByteString]` whose concatenation @@ -221,14 +224,20 @@ procMessages chans chanId chan sock = do -- a partial retrieval since the socket was closed, and `return Right bs` -- indicates success. This hasn't been implemented, since a Transport `receive` -- represents an atomic receipt of a message. -recvExact :: Socket -> Int64 -> IO [ByteString] -recvExact sock n = go [] sock (fromIntegral n) +recvExact :: Socket -> Int64 -> IO (Either ByteString ByteString) +recvExact sock l = do + res <- createAndTrim (fromIntegral l) (go 0) + case BS.length res of + n | n == (fromIntegral l) -> return $ Right res + _ -> return $ Left res where - go :: [ByteString] -> Socket -> Int -> IO [ByteString] - go bss _ 0 = return (reverse bss) - go bss sock n = do - bs <- NBS.recv sock n - if BS.null bs - then return [] - else go (bs:bss) sock (n - (fromIntegral $ BS.length bs)) + go :: Int -> Ptr Word8 -> IO Int + go n ptr | n == (fromIntegral l) = return n + go n ptr = do + (p, off, len) <- toForeignPtr <$> NBS.recv sock n + if len == 0 + then return n + else withForeignPtr p $ \p -> do + memcpy (ptr `plusPtr` n) (p `plusPtr` off) (fromIntegral len) + go (n+len) ptr From a0c6aad0ee78e01fde2c5a8dca018c34f8be87f6 Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Wed, 22 Feb 2012 17:54:40 -0500 Subject: [PATCH 0044/2357] use vectored IO rather than mapM to send size header with message --- src/Network/Transport/TCP.hs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index e68c770d..2fae9707 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -115,18 +115,13 @@ mkSourceEnd host service chanId = withSocketsDo $ do sock <- socket (addrFamily serverAddr) Stream defaultProtocol setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) --- let sink = sinkSocket sock :: Sink ByteString IO () NBS.sendAll sock $ encode (fromIntegral chanId :: Int64) return SourceEnd { send = {-# SCC "send" #-} \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int64 - if size < 255 - then - NBS.sendAll sock (encode (fromIntegral size :: Word8)) - else do - NBS.sendAll sock (encode (255 :: Word8)) - NBS.sendAll sock (encode size) - mapM_ (NBS.sendAll sock) bss + sizeStr | size < 255 = encode (fromIntegral size :: Word8) + | otherwise = BS.cons 255 (encode size) + NBS.sendMany sock (sizeStr:bss) , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } From 75ae7de508834e20f6479a1a5a44670379a00165 Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Wed, 22 Feb 2012 19:45:09 -0500 Subject: [PATCH 0045/2357] eta-expanded error helpers --- src/Network/Transport/TCP.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 2fae9707..89e005db 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -85,7 +85,8 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do , newMulticastWith = error "newMulticastWith: not defined" , deserialize = {-# SCC "deserialize" #-} \bs -> let (host, service, chanId) = - either (error $ printf "deserialize: %s") id $ decode bs in + either (\m -> error $ printf "deserialize: %s" m) + id $ decode bs in Just $ mkSourceAddr host service chanId , closeTransport = {-# SCC "closeTransport" #-} do -- Kill the transport channel process @@ -107,7 +108,7 @@ mkSourceAddr host service chanId = SourceAddr mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd mkSourceEnd host service chanId = withSocketsDo $ do - let err = error $ printf "mkSourceEnd: %s" + let err m = error $ printf "mkSourceEnd: %s" m serverAddrs <- getAddrInfo Nothing (Just host) (Just service) let serverAddr = case serverAddrs of [] -> err "getAddrInfo returned []" @@ -145,7 +146,7 @@ mkTargetEnd chans chanId chan = TargetEnd -- for some reason, an error is raised. procConnections :: Chans -> Socket -> IO () procConnections chans sock = forever $ do - let err = error $ printf "procConnections: %s" + let err m = error $ printf "procConnections: %s" m (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId bss <- recvExact clientSock 8 @@ -184,7 +185,7 @@ procMessages chans chanId chan sock = do Right sizeBS' -> procMessage $ decode sizeBS' esize -> procMessage (fromIntegral <$> esize) where - err = error $ printf "procMessages: %s" + err m = error $ printf "procMessages: %s" m closeSocket :: IO () closeSocket = do (chanId', chanMap) <- takeMVar chans From bf28e8ccb77843dccf50b568e944d3ac7c19df4e Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Wed, 22 Feb 2012 20:17:29 -0500 Subject: [PATCH 0046/2357] fixed bugs in PingTransport benchmark and TCP --- src/Network/Transport/TCP.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 89e005db..52f0592e 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -225,12 +225,14 @@ recvExact sock l = do res <- createAndTrim (fromIntegral l) (go 0) case BS.length res of n | n == (fromIntegral l) -> return $ Right res - _ -> return $ Left res + n -> do + printf "recvExact: expected %dB, got %dB\n" l n + return $ Left res where go :: Int -> Ptr Word8 -> IO Int go n ptr | n == (fromIntegral l) = return n go n ptr = do - (p, off, len) <- toForeignPtr <$> NBS.recv sock n + (p, off, len) <- toForeignPtr <$> NBS.recv sock (fromIntegral l) if len == 0 then return n else withForeignPtr p $ \p -> do From b852c4e8a502216528e6354de17770f342e81ff5 Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Wed, 22 Feb 2012 21:36:21 -0500 Subject: [PATCH 0047/2357] switched back to Char8 bytestrings; enhanced sendtransport so it's not just sending all 0s --- src/Network/Transport.hs | 2 +- src/Network/Transport/TCP.hs | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 6f0efd8c..f3d1fdea 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -12,7 +12,7 @@ module Network.Transport , newMulticast ) where -import Data.ByteString (ByteString) +import Data.ByteString.Char8 (ByteString) -- | The `Hints` and `SourceHints` provide hints to the underlying transport -- about the kind of connection that is required. This might include details diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 52f0592e..211e8f52 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -12,14 +12,10 @@ import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Monad (forever, forM_) -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as BS import Data.ByteString.Internal -import Data.Conduit -import qualified Data.Conduit.Binary as CB -import Data.Conduit.Network - import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap @@ -121,7 +117,7 @@ mkSourceEnd host service chanId = withSocketsDo $ do { send = {-# SCC "send" #-} \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int64 sizeStr | size < 255 = encode (fromIntegral size :: Word8) - | otherwise = BS.cons 255 (encode size) + | otherwise = BS.cons (toEnum 255) (encode size) NBS.sendMany sock (sizeStr:bss) , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } From 1af7963ca91c86a552c983677061ba1d517d2919 Mon Sep 17 00:00:00 2001 From: "Adam C. Foltzer" Date: Thu, 23 Feb 2012 10:11:19 -0500 Subject: [PATCH 0048/2357] Fixed a major bug in recvExact; the number of bytes we request from recv has to be bounded. Right now it will just ask for the min of the remaining message size and 4096. standardized on Int32s for size fields, since this is the limit for strict bytestring length --- src/Network/Transport/TCP.hs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 211e8f52..344125df 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -19,12 +19,13 @@ import Data.ByteString.Internal import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap -import Data.Int +import Data.Int (Int32) import Data.Serialize -import Data.Word +import Data.Word (Word8) import Foreign.ForeignPtr (withForeignPtr) import Foreign.Ptr (Ptr, plusPtr) +import Foreign.Storable (sizeOf) import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket @@ -112,10 +113,10 @@ mkSourceEnd host service chanId = withSocketsDo $ do sock <- socket (addrFamily serverAddr) Stream defaultProtocol setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) - NBS.sendAll sock $ encode (fromIntegral chanId :: Int64) + NBS.sendAll sock $ encode (fromIntegral chanId :: Int32) return SourceEnd { send = {-# SCC "send" #-} \bss -> do - let size = fromIntegral (sum . map BS.length $ bss) :: Int64 + let size = fromIntegral (sum . map BS.length $ bss) :: Int32 sizeStr | size < 255 = encode (fromIntegral size :: Word8) | otherwise = BS.cons (toEnum 255) (encode size) NBS.sendMany sock (sizeStr:bss) @@ -145,11 +146,11 @@ procConnections chans sock = forever $ do let err m = error $ printf "procConnections: %s" m (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId - bss <- recvExact clientSock 8 + bss <- recvExact clientSock (fromIntegral $ (sizeOf (0 :: Int32))) case bss of Left _ -> err "inbound chanId aborted" Right bs -> do - let chanId = either err fromIntegral (decode bs :: Either String Int64) + let chanId = either err fromIntegral (decode bs :: Either String Int32) (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of Nothing -> do @@ -175,7 +176,7 @@ procMessages chans chanId chan sock = do Right sizeBS -> do case decode sizeBS :: Either String Word8 of Right 255 -> do - esizeBS' <- recvExact sock 8 + esizeBS' <- recvExact sock (fromIntegral $ sizeOf (0 :: Int32)) case esizeBS' of Left _ -> closeSocket Right sizeBS' -> procMessage $ decode sizeBS' @@ -194,7 +195,7 @@ procMessages chans chanId chan sock = do let chanMap' = IntMap.insert chanId (chan, socks') chanMap putMVar chans (chanId', chanMap') sClose sock - procMessage :: Either String Int64 -> IO () + procMessage :: Either String Int32 -> IO () procMessage (Left msg) = err msg procMessage (Right size) = do ebs <- recvExact sock size @@ -216,19 +217,20 @@ procMessages chans chanId chan sock = do -- a partial retrieval since the socket was closed, and `return Right bs` -- indicates success. This hasn't been implemented, since a Transport `receive` -- represents an atomic receipt of a message. -recvExact :: Socket -> Int64 -> IO (Either ByteString ByteString) +recvExact :: Socket -> Int32 -> IO (Either ByteString ByteString) recvExact sock l = do res <- createAndTrim (fromIntegral l) (go 0) case BS.length res of n | n == (fromIntegral l) -> return $ Right res n -> do - printf "recvExact: expected %dB, got %dB\n" l n +-- printf "recvExact: expected %dB, got %dB\n" l n return $ Left res where go :: Int -> Ptr Word8 -> IO Int go n ptr | n == (fromIntegral l) = return n go n ptr = do - (p, off, len) <- toForeignPtr <$> NBS.recv sock (fromIntegral l) + (p, off, len) <- toForeignPtr <$> NBS.recv sock (min (fromIntegral l-n) 4096) +-- printf "recvExact: received %d/%d\n" (n+len) l if len == 0 then return n else withForeignPtr p $ \p -> do From 3972b56baac513849b7b1a613b7a7c520021719d Mon Sep 17 00:00:00 2001 From: Adam Foltzer Date: Thu, 23 Feb 2012 14:51:41 -0500 Subject: [PATCH 0049/2357] added switch between lazy and strict --- network-transport.cabal | 2 - src/Network/Transport.hs | 6 +++ src/Network/Transport/MVar.hs | 21 ++++++++-- src/Network/Transport/TCP.hs | 74 ++++++++++++++++++++++++----------- 4 files changed, 75 insertions(+), 28 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index dc808e15..0d87a81f 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -14,10 +14,8 @@ Library binary >= 0.5, bytestring >= 0.9, cereal >= 0.3, - conduit >= 0.2, containers >= 0.4, network >= 2.3, - network-conduit >= 0.2, safe >= 0.3 Exposed-modules: Network.Transport, Network.Transport.MVar, diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index f3d1fdea..3cc07cfe 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE CPP #-} + module Network.Transport ( Hints (..) , TargetEnd (..) @@ -12,7 +14,11 @@ module Network.Transport , newMulticast ) where +#ifndef LAZY import Data.ByteString.Char8 (ByteString) +#else +import Data.ByteString.Lazy.Char8 (ByteString) +#endif -- | The `Hints` and `SourceHints` provide hints to the underlying transport -- about the kind of connection that is required. This might include details diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs index 2f57fff3..597ec97e 100644 --- a/src/Network/Transport/MVar.hs +++ b/src/Network/Transport/MVar.hs @@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE CPP #-} module Network.Transport.MVar ( mkTransport @@ -6,14 +7,28 @@ module Network.Transport.MVar import Control.Concurrent.MVar import Data.IntMap (IntMap) -import Data.ByteString (ByteString) -import Data.Serialize +import qualified Data.Serialize as Ser import qualified Data.IntMap as IntMap -import qualified Data.ByteString as BS import Network.Transport +#ifndef LAZY +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as BS +encode = Ser.encode +decode = Ser.decode +#else +import Data.ByteString.Lazy.Char8 (ByteString) +import qualified Data.ByteString.Lazy.Char8 as BS +encode = Ser.encodeLazy +decode = Ser.decodeLazy +#endif +{-# INLINE encode #-} +{-# INLINE decode #-} +encode :: Ser.Serialize a => a -> ByteString +decode :: Ser.Serialize a => ByteString -> Either String a + type Chans = MVar (Int, IntMap (MVar [ByteString])) mkTransport :: IO Transport diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 211e8f52..4f5a56fe 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE CPP #-} + module Network.Transport.TCP ( mkTransport , TCPConfig (..) @@ -12,19 +14,18 @@ import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Monad (forever, forM_) -import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Char8 as BS -import Data.ByteString.Internal +import qualified Data.ByteString.Internal as BSI import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap -import Data.Int -import Data.Serialize +import Data.Int (Int32) +import qualified Data.Serialize as Ser import Data.Word import Foreign.ForeignPtr (withForeignPtr) import Foreign.Ptr (Ptr, plusPtr) +import Foreign.Storable (sizeOf) import Network.Socket ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket @@ -32,14 +33,30 @@ import Network.Socket , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) import qualified Network.Socket as N -import qualified Network.Socket.ByteString as NBS import Text.Printf import Safe -import qualified Data.Binary as B - +#ifndef LAZY +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as BS +import qualified Network.Socket.ByteString as NBS +sendNBS = NBS.sendMany +encode = Ser.encode +decode = Ser.decode +#else +import Data.ByteString.Lazy.Char8 (ByteString) +import qualified Data.ByteString.Lazy.Char8 as BS +import qualified Network.Socket.ByteString.Lazy as NBS +sendNBS sock bs = NBS.sendAll sock $! head bs +encode = Ser.encodeLazy +decode = Ser.decodeLazy +#endif +{-# INLINE encode #-} +{-# INLINE decode #-} +encode :: Ser.Serialize a => a -> ByteString +decode :: Ser.Serialize a => ByteString -> Either String a type ChanId = Int type Chans = MVar (ChanId, IntMap (Chan [ByteString], [(ThreadId, Socket)])) @@ -112,13 +129,13 @@ mkSourceEnd host service chanId = withSocketsDo $ do sock <- socket (addrFamily serverAddr) Stream defaultProtocol setSocketOption sock ReuseAddr 1 N.connect sock (addrAddress serverAddr) - NBS.sendAll sock $ encode (fromIntegral chanId :: Int64) + NBS.sendAll sock $ encode (fromIntegral chanId :: Int32) return SourceEnd { send = {-# SCC "send" #-} \bss -> do - let size = fromIntegral (sum . map BS.length $ bss) :: Int64 + let size = fromIntegral (sum . map BS.length $ bss) :: Int32 sizeStr | size < 255 = encode (fromIntegral size :: Word8) | otherwise = BS.cons (toEnum 255) (encode size) - NBS.sendMany sock (sizeStr:bss) + sendNBS sock (sizeStr:bss) , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } @@ -145,11 +162,11 @@ procConnections chans sock = forever $ do let err m = error $ printf "procConnections: %s" m (clientSock, _clientAddr) <- accept sock -- decode the first message to find the correct chanId - bss <- recvExact clientSock 8 + bss <- recvExact clientSock (fromIntegral $ sizeOf (0 :: Int32)) case bss of Left _ -> err "inbound chanId aborted" Right bs -> do - let chanId = either err fromIntegral (decode bs :: Either String Int64) + let chanId = either err fromIntegral (decode bs :: Either String Int32) (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of Nothing -> do @@ -175,7 +192,7 @@ procMessages chans chanId chan sock = do Right sizeBS -> do case decode sizeBS :: Either String Word8 of Right 255 -> do - esizeBS' <- recvExact sock 8 + esizeBS' <- recvExact sock (fromIntegral $ sizeOf (0 :: Int32)) case esizeBS' of Left _ -> closeSocket Right sizeBS' -> procMessage $ decode sizeBS' @@ -194,7 +211,7 @@ procMessages chans chanId chan sock = do let chanMap' = IntMap.insert chanId (chan, socks') chanMap putMVar chans (chanId', chanMap') sClose sock - procMessage :: Either String Int64 -> IO () + procMessage :: Either String Int32 -> IO () procMessage (Left msg) = err msg procMessage (Right size) = do ebs <- recvExact sock size @@ -216,22 +233,33 @@ procMessages chans chanId chan sock = do -- a partial retrieval since the socket was closed, and `return Right bs` -- indicates success. This hasn't been implemented, since a Transport `receive` -- represents an atomic receipt of a message. -recvExact :: Socket -> Int64 -> IO (Either ByteString ByteString) +#ifndef LAZY +recvExact :: Socket -> Int32 -> IO (Either ByteString ByteString) recvExact sock l = do - res <- createAndTrim (fromIntegral l) (go 0) + res <- BSI.createAndTrim (fromIntegral l) (go 0) case BS.length res of n | n == (fromIntegral l) -> return $ Right res - n -> do - printf "recvExact: expected %dB, got %dB\n" l n - return $ Left res + n -> return $ Left res where go :: Int -> Ptr Word8 -> IO Int go n ptr | n == (fromIntegral l) = return n go n ptr = do - (p, off, len) <- toForeignPtr <$> NBS.recv sock (fromIntegral l) + (p, off, len) <- BSI.toForeignPtr <$> NBS.recv sock (min (fromIntegral l-n) 4096) if len == 0 then return n else withForeignPtr p $ \p -> do - memcpy (ptr `plusPtr` n) (p `plusPtr` off) (fromIntegral len) + BSI.memcpy (ptr `plusPtr` n) (p `plusPtr` off) (fromIntegral len) go (n+len) ptr - +#else +recvExact :: Socket -> Int32 -> IO (Either ByteString ByteString) +recvExact sock n = + go [] sock n + where + go :: [ByteString] -> Socket -> Int32 -> IO (Either ByteString ByteString) + go bss _ 0 = return (Right $ BS.concat (reverse bss)) + go bss sock n = do + bs <- NBS.recv sock (min (fromIntegral n) 4096) + if BS.null bs + then return (Left $ BS.concat (reverse bss)) + else go (bs:bss) sock (n - (fromIntegral $ BS.length bs)) +#endif \ No newline at end of file From d11dfd76677d764f13b511b3a64bc0bb7b5af8db Mon Sep 17 00:00:00 2001 From: Adam Foltzer Date: Thu, 23 Feb 2012 15:40:26 -0500 Subject: [PATCH 0050/2357] tweaked docs and Send benchmark --- src/Network/Transport/TCP.hs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index b8b419da..ed9a4c3d 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -49,7 +49,7 @@ decode = Ser.decode import Data.ByteString.Lazy.Char8 (ByteString) import qualified Data.ByteString.Lazy.Char8 as BS import qualified Network.Socket.ByteString.Lazy as NBS -sendNBS sock bs = NBS.sendAll sock $! head bs +sendNBS sock = NBS.sendAll sock . BS.concat encode = Ser.encodeLazy decode = Ser.decodeLazy #endif @@ -133,7 +133,7 @@ mkSourceEnd host service chanId = withSocketsDo $ do return SourceEnd { send = {-# SCC "send" #-} \bss -> do let size = fromIntegral (sum . map BS.length $ bss) :: Int32 - sizeStr | size < 255 = encode (fromIntegral size :: Word8) + sizeStr | size < 255 = BS.singleton (toEnum $ fromEnum size) | otherwise = BS.cons (toEnum 255) (encode size) sendNBS sock (sizeStr:bss) , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock @@ -179,7 +179,7 @@ procConnections chans sock = forever $ do -- | This function first extracts a header of type Word8, which determines -- the size of the ByteString that follows. If this size is 0, this indicates --- that the ByteString is large, so the next value is an Int64, which +-- that the ByteString is large, so the next value is an Int32, which -- determines the size of the ByteString that follows. The ByteString is then -- extracted from the socket, and then written to the Chan only when -- complete. If either of the first header size is null this indicates the @@ -221,18 +221,13 @@ procMessages chans chanId chan sock = do writeChan chan [bs] procMessages chans chanId chan sock --- | The result of `recvExact sock n` is a `[ByteString]` whose concatenation --- is of length `n`, received from `sock`. No more bytes than necessary are --- read from the socket. --- NB: This uses Network.Socket.ByteString.recv, which may *discard* --- superfluous input depending on the socket type. Also note that --- if `recv` returns an empty `ByteString` then this means that the socket --- was closed: in this case, we return an empty list. --- NB: It may be appropriate to change the return type to --- IO (Either ByteString ByteString), where `return Left bs` indicates --- a partial retrieval since the socket was closed, and `return Right bs` --- indicates success. This hasn't been implemented, since a Transport `receive` --- represents an atomic receipt of a message. +-- | The normal result of `recvExact sock n` is `Right ByteString` +-- whose string of length `n`, received from `sock`. If fewer than `n` +-- bytes are read from `sock` before it closes, the result is `Left +-- ByteString` whose string is those bytes that were received. No more +-- bytes than necessary are read from the socket. NB: This uses +-- Network.Socket.ByteString.recv, which may *discard* superfluous +-- input depending on the socket type. #ifndef LAZY recvExact :: Socket -> Int32 -> IO (Either ByteString ByteString) recvExact sock l = do From 5b42ef3cc68623dd274cb69cb2f10441598f80cd Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Fri, 24 Feb 2012 10:44:41 -0500 Subject: [PATCH 0051/2357] Cleaned up the benchmark setup. Now all benchmarks can run either a single trial (no criterion) or multiple trials with criterion. Added some notes on the performance I'm observing. The TCP transport is adding an unfortunate amount of latency. --- network-transport.cabal | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/network-transport.cabal b/network-transport.cabal index a78273b1..faf07b31 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,5 +1,10 @@ Name: network-transport -Version: 0.0.1 +Version: 0.0.1.1 + +-- Version History: +-- 0.0.1 - Initial +-- 0.0.1.1 - Bump for Adam's change to strict BS. + Description: Network transport interface License: BSD3 License-file: LICENSE From 3b9053f6468e94af4f19822afa7a04edfdb77182 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Sat, 25 Feb 2012 04:50:44 -0500 Subject: [PATCH 0052/2357] Cleaned up examples and benchmarks. Found strange bug. Tweaked Process.hs for strict bytestrings. The bug is that DemoTransport.hs will hang when trying to "killThread" (Pipes backend, demo3 and demo4), but ONLY under GHC 7.0.4 and 7.2.1. Under GHC 6.12.3 and GHC 7.4.1 it works fine! At first I thought this may be an issue with non-allocating threads not being preempted by the runtime system (and therefore not servicing the ThreadKilled asynchronous exception). But it's hard to explain that pattern of outcomes on different GHC versions. --- network-transport.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index faf07b31..f5081be8 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -16,13 +16,13 @@ Cabal-Version: >=1.2 Library Build-Depends: base >= 3 && < 5, - binary >= 0.5, +-- binary >= 0.5, bytestring >= 0.9, cereal >= 0.3, containers >= 0.4, network >= 2.3, safe >= 0.3, - unix >= 2.5.0.0 + unix >= 2.4 Exposed-modules: Network.Transport, Network.Transport.MVar, Network.Transport.TCP From 71d7b1dfbf40667fae86f56913f5f8f77cf0d92b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 13 Apr 2012 16:14:35 +0100 Subject: [PATCH 0053/2357] Start comparing the transport against base tests Modified the TCP transport not to use serialization for lengths, but since the ping messages are only short this has no effect on the tests. The transport is still significnatly slower though. Not yet sure what's causing this. --- src/Network/Transport/TCP.hs | 53 ++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index ba33661d..facfaf0c 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -39,6 +39,9 @@ import Text.Printf import Safe import System.IO (stderr, hPutStrLn) +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) #ifndef LAZY import Data.ByteString.Char8 (ByteString) @@ -60,6 +63,9 @@ decode = Ser.decodeLazy encode :: Ser.Serialize a => a -> ByteString decode :: Ser.Serialize a => ByteString -> Either String a +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + type ChanId = Int type Chans = MVar (ChanId, IntMap (Chan [ByteString], [(ThreadId, Socket)])) @@ -134,11 +140,12 @@ mkSourceEnd host service chanId = withSocketsDo $ do N.connect sock (addrAddress serverAddr) NBS.sendAll sock $ encode (fromIntegral chanId :: Int32) return SourceEnd - { send = {-# SCC "send" #-} \bss -> do - let size = fromIntegral (sum . map BS.length $ bss) :: Int32 - sizeStr | size < 255 = BS.singleton (toEnum $ fromEnum size) - | otherwise = BS.cons (toEnum 255) (encode size) - sendNBS sock (sizeStr:bss) + { send = {-# SCC "send" #-} \bss -> + let size = fromIntegral (sum . map BS.length $ bss) :: Int32 in + if size < 255 + then sendNBS sock (BS.singleton (toEnum $ fromEnum size) : bss) + else do size' <- encodeLength size + sendNBS sock (BS.singleton (toEnum 255) : size' : bss) , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } @@ -193,15 +200,15 @@ procMessages chans chanId chan sock = do esizeBS <- recvExact sock 1 case esizeBS of Left _ -> closeSocket - Right sizeBS -> do - case decode sizeBS :: Either String Word8 of - Right 255 -> do - esizeBS' <- recvExact sock (fromIntegral $ sizeOf (0 :: Int32)) - case esizeBS' of - Left _ -> closeSocket - Right sizeBS' -> procMessage $ decode sizeBS' - esize@(Right _) -> procMessage (fromIntegral <$> esize) - Left msg -> error msg + Right sizeBS -> + let size = toEnum . fromEnum $ BS.index sizeBS 0 in + if size < 255 + then procMessage size + else do esizeBS' <- recvExact sock (fromIntegral $ sizeOf (0 :: Int32)) + case esizeBS' of + Left _ -> closeSocket + Right sizeBS' -> do sizeDec <- decodeLength sizeBS' + procMessage sizeDec where err m = error $ printf "procMessages: %s" m closeSocket :: IO () @@ -216,9 +223,8 @@ procMessages chans chanId chan sock = do let chanMap' = IntMap.insert chanId (chan, socks') chanMap putMVar chans (chanId', chanMap') sClose sock - procMessage :: Either String Int32 -> IO () - procMessage (Left msg) = err msg - procMessage (Right size) = do + procMessage :: Int32 -> IO () + procMessage size = do ebs <- recvExact sock size case ebs of Left _ -> closeSocket @@ -289,3 +295,16 @@ forkWithExceptions forkit descr action = do throwTo parent (e::SomeException) ) +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) From 3eb2458df6b6a15e6c872206679cb7a6cec47c56 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 16 Apr 2012 12:45:30 +0100 Subject: [PATCH 0054/2357] Use MVar rather than Chan in Transport.TCP --- src/Network/Transport/TCP.hs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index facfaf0c..d819f748 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -9,7 +9,6 @@ import Network.Transport import Control.Applicative import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) -import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Exception (SomeException, IOException, AsyncException(ThreadKilled), fromException, throwTo, throw, catch, handle) @@ -67,7 +66,7 @@ foreign import ccall unsafe "htonl" htonl :: CInt -> CInt foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt type ChanId = Int -type Chans = MVar (ChanId, IntMap (Chan [ByteString], [(ThreadId, Socket)])) +type Chans = MVar (ChanId, IntMap (MVar [ByteString], [(ThreadId, Socket)])) -- | This deals with several different configuration properties: -- * Buffer size, specified in Hints @@ -101,7 +100,7 @@ mkTransport (TCPConfig _hints host service) = withSocketsDo $ do return Transport { newConnectionWith = {-# SCC "newConnectionWith" #-}\_ -> do (chanId, chanMap) <- takeMVar chans - chan <- newChan + chan <- newEmptyMVar putMVar chans (chanId + 1, IntMap.insert chanId (chan, []) chanMap) return (mkSourceAddr host service chanId, mkTargetEnd chans chanId chan) , newMulticastWith = error "newMulticastWith: not defined" @@ -149,10 +148,10 @@ mkSourceEnd host service chanId = withSocketsDo $ do , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock } -mkTargetEnd :: Chans -> ChanId -> Chan [ByteString] -> TargetEnd +mkTargetEnd :: Chans -> ChanId -> MVar [ByteString] -> TargetEnd mkTargetEnd chans chanId chan = TargetEnd - { -- for now we will implement this as a Chan - receive = {-# SCC "receive" #-} readChan chan + { -- for now we will implement this as an MVar + receive = {-# SCC "receive" #-} takeMVar chan , closeTargetEnd = {-# SCC "closeTargetEnd" #-} do (chanId', chanMap) <- takeMVar chans case IntMap.lookup chanId chanMap of @@ -195,7 +194,7 @@ procConnections chans sock = forever $ do -- extracted from the socket, and then written to the Chan only when -- complete. If either of the first header size is null this indicates the -- socket has closed. -procMessages :: Chans -> ChanId -> Chan [ByteString] -> Socket -> IO () +procMessages :: Chans -> ChanId -> MVar [ByteString] -> Socket -> IO () procMessages chans chanId chan sock = do esizeBS <- recvExact sock 1 case esizeBS of @@ -229,7 +228,7 @@ procMessages chans chanId chan sock = do case ebs of Left _ -> closeSocket Right bs -> do - writeChan chan [bs] + putMVar chan [bs] procMessages chans chanId chan sock -- | The normal result of `recvExact sock n` is `Right ByteString` From 0d586fb6e0c9371ab5b79e11ca61c94166e0133e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 16 May 2012 11:02:52 +0100 Subject: [PATCH 0055/2357] Replace network-transport with network-transport-2 --- network-transport.cabal | 96 +- src/Network/Transport.hs | 289 +++-- src/Network/Transport/Chan.hs | 150 +++ src/Network/Transport/Internal.hs | 93 ++ src/Network/Transport/Internal/TCP.hs | 102 ++ src/Network/Transport/MVar.hs | 75 -- src/Network/Transport/TCP.hs | 1415 +++++++++++++++++++------ src/Network/Transport/Util.hs | 24 + tests/TestAuxiliary.hs | 82 ++ tests/TestInMemory.hs | 8 + tests/TestMulticast.hs | 72 ++ tests/TestMulticastInMemory.hs | 7 + tests/TestTCP.hs | 487 +++++++++ tests/TestTransport.hs | 728 +++++++++++++ tests/Traced.hs | 191 ++++ tests/chat/ChatClient.hs | 107 ++ tests/chat/ChatServer.hs | 28 + tests/sumeuler/SumEulerMaster.hs | 44 + tests/sumeuler/SumEulerWorker.hs | 52 + tests/sumeuler/sumeuler.sh | 20 + 20 files changed, 3567 insertions(+), 503 deletions(-) create mode 100644 src/Network/Transport/Chan.hs create mode 100644 src/Network/Transport/Internal.hs create mode 100644 src/Network/Transport/Internal/TCP.hs delete mode 100644 src/Network/Transport/MVar.hs create mode 100644 src/Network/Transport/Util.hs create mode 100644 tests/TestAuxiliary.hs create mode 100644 tests/TestInMemory.hs create mode 100644 tests/TestMulticast.hs create mode 100644 tests/TestMulticastInMemory.hs create mode 100644 tests/TestTCP.hs create mode 100644 tests/TestTransport.hs create mode 100644 tests/Traced.hs create mode 100644 tests/chat/ChatClient.hs create mode 100644 tests/chat/ChatServer.hs create mode 100644 tests/sumeuler/SumEulerMaster.hs create mode 100644 tests/sumeuler/SumEulerWorker.hs create mode 100755 tests/sumeuler/sumeuler.sh diff --git a/network-transport.cabal b/network-transport.cabal index f5081be8..6ec22aa4 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,31 +1,71 @@ -Name: network-transport -Version: 0.0.1.1 +Name: network-transport +Version: 0.1.0 +Description: Network Transport +Author: Duncan Coutts, Nicolas Wu, Edsko de Vries +Maintainer: dcoutts@well-typed.com +License: BSD3 +License-file: LICENSE +Synopsis: Network abstraction layer +Category: Network +Homepage: http://github.com/haskell-distributed +Build-Type: Simple +Cabal-Version: >=1.9.2 --- Version History: --- 0.0.1 - Initial --- 0.0.1.1 - Bump for Adam's change to strict BS. +Library + Build-Depends: base >= 4 && < 5, + bytestring, + containers, + data-accessor, + network, + mtl, + transformers + Exposed-modules: Network.Transport, + Network.Transport.Chan, + Network.Transport.TCP, + Network.Transport.Util + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable + ghc-options: -Wall -fno-warn-unused-do-bind + HS-Source-Dirs: src -Description: Network transport interface -License: BSD3 -License-file: LICENSE -Author: Duncan Coutts, Nicolas Wu -Maintainer: duncan@well-typed.com -Homepage: http://github.com/haskell-distributed/ -Build-Type: Simple -Cabal-Version: >=1.2 +Test-Suite TestTCP + Type: exitcode-stdio-1.0 + Main-Is: TestTCP.hs + Build-Depends: base >= 4, + bytestring, + containers, + data-accessor, + network, + mtl, + transformers, + ansi-terminal + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + ghc-options: -Wall -fno-warn-unused-do-bind + HS-Source-Dirs: tests src -Library - Build-Depends: base >= 3 && < 5, --- binary >= 0.5, - bytestring >= 0.9, - cereal >= 0.3, - containers >= 0.4, - network >= 2.3, - safe >= 0.3, - unix >= 2.4 - Exposed-modules: Network.Transport, - Network.Transport.MVar, - Network.Transport.TCP - Extensions: BangPatterns - ghc-options: -Wall - HS-Source-Dirs: src +Test-Suite TestMulticastInMemory + Type: exitcode-stdio-1.0 + Main-Is: TestMulticastInMemory.hs + Build-Depends: base >= 4, + bytestring, + containers, + data-accessor, + mtl, + transformers, + ansi-terminal + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + ghc-options: -Wall -fno-warn-unused-do-bind + HS-Source-Dirs: tests src + +Test-Suite TestInMemory + Type: exitcode-stdio-1.0 + Main-Is: TestInMemory.hs + Build-Depends: base >= 4, + bytestring, + containers, + data-accessor, + mtl, + transformers, + ansi-terminal + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + ghc-options: -Wall -fno-warn-unused-do-bind + HS-Source-Dirs: tests src diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 3cc07cfe..fd556f28 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -1,109 +1,192 @@ -{-# LANGUAGE CPP #-} - -module Network.Transport - ( Hints (..) - , TargetEnd (..) - , SourceAddr (..) - , SourceEnd (..) - , SourceHints (..) - , Transport (..) - , connect - , defaultHints - , defaultSourceHints - , newConnection - , newMulticast - ) where - -#ifndef LAZY -import Data.ByteString.Char8 (ByteString) -#else -import Data.ByteString.Lazy.Char8 (ByteString) -#endif - --- | The `Hints` and `SourceHints` provide hints to the underlying transport --- about the kind of connection that is required. This might include details --- such as whether the connection is eager or buffered, and the buffer size. -data Hints = Hints -data SourceHints = SourceHints - --- | A `Transport` encapsulates the functions required to establish many-to-one --- and one-to-many connections between clients and servers. --- The `newConnectionWith` function creates a `TargetEnd` that listens to --- messages sent using the corresponding `SourceAddr`. This connection is --- established using a `Hints` value, which provides information about the --- connection topology. --- Each `SourceAddr` can be serialised into a `ByteString`, and the `deserialize` --- function converts this back into a `SourceAddr`. --- Note that these connections provide reliable and ordered messages. -data Transport = Transport - { newConnectionWith :: Hints -> IO (SourceAddr, TargetEnd) - , newMulticastWith :: Hints -> IO (MulticastSourceEnd, MulticastTargetAddr) - , deserialize :: ByteString -> Maybe SourceAddr - , closeTransport :: IO () +-- | Network Transport +module Network.Transport ( -- * Types + Transport(..) + , EndPoint(..) + , Connection(..) + , Event(..) + , ConnectionId + , Reliability(..) + , MulticastGroup(..) + , EndPointAddress(..) + , MulticastAddress(..) + -- * Error codes + , TransportError(..) + , NewEndPointErrorCode(..) + , ConnectErrorCode(..) + , NewMulticastGroupErrorCode(..) + , ResolveMulticastGroupErrorCode(..) + , SendErrorCode(..) + , EventErrorCode(..) + ) where + +import Data.ByteString (ByteString) +import Control.Exception (Exception) +import Data.Typeable (Typeable) + +-------------------------------------------------------------------------------- +-- Main API -- +-------------------------------------------------------------------------------- + +-- | To create a network abstraction layer, use one of the +-- @Network.Transport.*@ packages. +data Transport = Transport { + -- | Create a new end point (heavyweight operation) + newEndPoint :: IO (Either (TransportError NewEndPointErrorCode) EndPoint) + -- | Shutdown the transport completely + , closeTransport :: IO () } --- | This is a convenience function that creates a new connection on a transport --- using the default hints. -newConnection :: Transport -> IO (SourceAddr, TargetEnd) -newConnection transport = newConnectionWith transport defaultHints - -newMulticast :: Transport -> IO (MulticastSourceEnd, MulticastTargetAddr) -newMulticast transport = newMulticastWith transport defaultHints - --- | The default `Hints` for establishing a new transport connection. -defaultHints :: Hints -defaultHints = Hints - --- | A `SourceAddr` is an address that corresponds to a listening `TargetEnd` --- initially created using `newConnection`. A `SourceAddr` can be shared between --- clients by using `serialize`, and passing the resulting `ByteString`. --- Given a `SourceAddr`, the `connectWith` function creates a `SourceEnd` which --- can be used to send messages. -data SourceAddr = SourceAddr - { connectWith :: SourceHints -> IO SourceEnd - , serialize :: ByteString +-- | Network endpoint. +data EndPoint = EndPoint { + -- | Endpoints have a single shared receive queue. + receive :: IO Event + -- | EndPointAddress of the endpoint. + , address :: EndPointAddress + -- | Create a new lightweight connection. + , connect :: EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) + -- | Create a new multicast group. + , newMulticastGroup :: IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) + -- | Resolve an address to a multicast group. + , resolveMulticastGroup :: MulticastAddress -> IO (Either (TransportError ResolveMulticastGroupErrorCode) MulticastGroup) + -- | Close the endpoint + , closeEndPoint :: IO () + } + +-- | Lightweight connection to an endpoint. +data Connection = Connection { + -- | Send a message on this connection. + send :: [ByteString] -> IO (Either (TransportError SendErrorCode) ()) + -- | Close the connection. + , close :: IO () } --- | This is a convenience function that connects with a given `SourceAddr` using --- the default hints for sending. -connect :: SourceAddr -> IO SourceEnd -connect sourceAddr = connectWith sourceAddr defaultSourceHints - --- | The default `SourceHints` for establishing a `SourceEnd`. -defaultSourceHints :: SourceHints -defaultSourceHints = SourceHints - --- | A `SourceEnd` provides a `send` function that allows vectored messages --- to be sent to the corresponding `TargetEnd`. --- The `close` function closes the connection between this source and the target --- end. Connections between other sources the target end remain unaffected -data SourceEnd = SourceEnd - { send :: [ByteString] -> IO () - , closeSourceEnd :: IO () +-- | Event on an endpoint. +data Event = + -- | Received a message + Received ConnectionId [ByteString] + -- | Connection closed + | ConnectionClosed ConnectionId + -- | Connection opened + | ConnectionOpened ConnectionId Reliability EndPointAddress + -- | Received multicast + | ReceivedMulticast MulticastAddress [ByteString] + -- | The endpoint got closed (manually, by a call to closeEndPoint or closeTransport) + | EndPointClosed + -- | An error occurred + | ErrorEvent (TransportError EventErrorCode) + deriving Show + +-- | Connection IDs enable receivers to distinguish one connection from another. +type ConnectionId = Int + +-- | Reliability guarantees of a connection. +data Reliability = + ReliableOrdered + | ReliableUnordered + | Unreliable + deriving Show + +-- | Multicast group. +data MulticastGroup = MulticastGroup { + -- | EndPointAddress of the multicast group. + multicastAddress :: MulticastAddress + -- | Delete the multicast group completely. + , deleteMulticastGroup :: IO () + -- | Maximum message size that we can send to this group. + , maxMsgSize :: Maybe Int + -- | Send a message to the group. + , multicastSend :: [ByteString] -> IO () + -- | Subscribe to the given multicast group (to start receiving messages from the group). + , multicastSubscribe :: IO () + -- | Unsubscribe from the given multicast group (to stop receiving messages from the group). + , multicastUnsubscribe :: IO () + -- | Close the group (that is, indicate you no longer wish to send to the group). + , multicastClose :: IO () } --- | A `TargetEnd` provides a `receive` function that allows messages --- to be received from the corresponding `SourceEnd`s. --- The `closeTargetEnd` function closes all connections to this target, --- and all new connections will be refused. -data TargetEnd = TargetEnd - { receive :: IO [ByteString] - , closeTargetEnd :: IO () - } - -newtype MulticastSourceEnd = MulticastSourceEnd - { multicastSource :: ByteString -> IO () - } - -newtype MulticastTargetAddr = MulticastTargetAddr - { multicastConnect :: IO MulticastTargetEnd - } - -newtype MulticastTargetEnd = MulticastTargetEnd - { multicastReceive :: IO ByteString - } - --- TODO: Other SourceEnds that might be of use: --- data UnorderedSourceEnd -- reliable, unordered --- data UnreliableSourceEnd -- unreliable, unordered - +-- | EndPointAddress of an endpoint. +newtype EndPointAddress = EndPointAddress { endPointAddressToByteString :: ByteString } + deriving (Show, Eq, Ord) + +-- | EndPointAddress of a multicast group. +newtype MulticastAddress = MulticastAddress { multicastAddressToByteString :: ByteString } + deriving (Show, Eq, Ord) + +-------------------------------------------------------------------------------- +-- Error codes -- +-------------------------------------------------------------------------------- + +data TransportError error = TransportError error String + deriving (Show, Typeable) + +-- | Although the functions in the transport API never throw TransportErrors +-- (but return them explicitly), application code may want to turn these into +-- exceptions. +instance (Typeable err, Show err) => Exception (TransportError err) + +-- | Errors during the creation of an endpoint +data NewEndPointErrorCode = + -- | Not enough resources + -- (i.e., this could be a temporary local problem) + NewEndPointInsufficientResources + -- | Failed for some other reason + -- (i.e., there is probably no point trying again) + | NewEndPointFailed + deriving (Show, Typeable) + +-- | Connection failure +data ConnectErrorCode = + -- | Could not resolve the address + -- (i.e., this could be a temporary remote problem) + ConnectNotFound + -- | Insufficient resources (for instance, no more sockets available) + -- (i.e., this could be a temporary local problem) + | ConnectInsufficientResources + -- | Failed for other reasons (including syntax error) + -- (i.e., there is probably no point trying again). + | ConnectFailed + deriving (Show, Typeable) + +-- | Failure during the creation of a new multicast group +data NewMulticastGroupErrorCode = + -- | Insufficient resources + -- (i.e., this could be a temporary problem) + NewMulticastGroupInsufficientResources + -- | Failed for some other reason + -- (i.e., there is probably no point trying again) + | NewMulticastGroupFailed + -- | Not all transport implementations support multicast + | NewMulticastGroupUnsupported + deriving (Show, Typeable) + +-- | Failure during the resolution of a multicast group +data ResolveMulticastGroupErrorCode = + -- | Multicast group not found + -- (i.e., this could be a temporary problem) + ResolveMulticastGroupNotFound + -- | Failed for some other reason (including syntax error) + -- (i.e., there is probably no point trying again) + | ResolveMulticastGroupFailed + -- | Not all transport implementations support multicast + | ResolveMulticastGroupUnsupported + deriving (Show, Typeable) + +-- | Failure during sending a message +data SendErrorCode = + -- | Could not send this message + -- (but another attempt might succeed) + SendUnreachable + -- | Send failed for some other reason + -- (and retrying probably won't help) + | SendFailed + deriving (Show, Typeable) + +-- | Error codes used when reporting errors to endpoints (through receive) +data EventErrorCode = + -- | Failure of the entire endpoint + EventEndPointFailed + -- | Transport-wide fatal error + | EventTransportFailed + -- | Connection to a remote endpoint was lost + | EventConnectionLost EndPointAddress [ConnectionId] + deriving Show diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs new file mode 100644 index 00000000..6d9ec331 --- /dev/null +++ b/src/Network/Transport/Chan.hs @@ -0,0 +1,150 @@ +-- | In-memory implementation of the Transport API. +module Network.Transport.Chan (createTransport) where + +import Network.Transport +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) +import Control.Applicative ((<$>)) +import Control.Category ((>>>)) +import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_, readMVar) +import Control.Exception (throw) +import Control.Monad (forM_, when) +import Data.Map (Map) +import qualified Data.Map as Map (empty, insert, size, delete, findWithDefault) +import Data.Set (Set) +import qualified Data.Set as Set (empty, elems, insert, delete) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as BSC (pack) +import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) +import qualified Data.Accessor.Container as DAC (mapMaybe) + +-- Global state: next available "address", mapping from addresses to channels and next available connection +data TransportState = State { _channels :: Map EndPointAddress (Chan Event) + , _nextConnectionId :: Map EndPointAddress ConnectionId + , _multigroups :: Map MulticastAddress (MVar (Set EndPointAddress)) + } + +-- | Create a new Transport. +-- +-- Only a single transport should be created per Haskell process +-- (threads can, and should, create their own endpoints though). +createTransport :: IO Transport +createTransport = do + state <- newMVar State { _channels = Map.empty + , _nextConnectionId = Map.empty + , _multigroups = Map.empty + } + return Transport { newEndPoint = apiNewEndPoint state + , closeTransport = throw (userError "closeEndPoint not implemented") + } + +-- | Create a new end point +apiNewEndPoint :: MVar TransportState -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) +apiNewEndPoint state = do + chan <- newChan + addr <- modifyMVar state $ \st -> do + let addr = EndPointAddress . BSC.pack . show . Map.size $ st ^. channels + return ((channelAt addr ^= chan) . (nextConnectionIdAt addr ^= 1) $ st, addr) + return . Right $ EndPoint { receive = readChan chan + , address = addr + , connect = apiConnect addr state + , closeEndPoint = throw (userError "closeEndPoint not implemented") + , newMulticastGroup = apiNewMulticastGroup state addr + , resolveMulticastGroup = apiResolveMulticastGroup state addr + } + +-- | Create a new connection +apiConnect :: EndPointAddress -> MVar TransportState -> EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) +apiConnect myAddress state theirAddress _ = do + (chan, conn) <- modifyMVar state $ \st -> do + let chan = st ^. channelAt theirAddress + let conn = st ^. nextConnectionIdAt theirAddress + return (nextConnectionIdAt theirAddress ^: (+ 1) $ st, (chan, conn)) + writeChan chan $ ConnectionOpened conn ReliableOrdered myAddress + connAlive <- newMVar True + return . Right $ Connection { send = apiSend chan conn connAlive + , close = apiClose chan conn connAlive + } + +-- | Send a message over a connection +apiSend :: Chan Event -> ConnectionId -> MVar Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) +apiSend chan conn connAlive msg = + modifyMVar connAlive $ \alive -> do + if alive + then do writeChan chan (Received conn msg) + return (alive, Right ()) + else do return (alive, Left (TransportError SendFailed "Connection closed")) + +-- | Close a connection +apiClose :: Chan Event -> ConnectionId -> MVar Bool -> IO () +apiClose chan conn connAlive = + modifyMVar_ connAlive $ \alive -> do + when alive . writeChan chan $ ConnectionClosed conn + return False + +-- | Create a new multicast group +apiNewMulticastGroup :: MVar TransportState -> EndPointAddress -> IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) +apiNewMulticastGroup state ourAddress = do + group <- newMVar Set.empty + groupAddr <- modifyMVar state $ \st -> do + let addr = MulticastAddress . BSC.pack . show . Map.size $ st ^. multigroups + return (multigroupAt addr ^= group $ st, addr) + return . Right $ createMulticastGroup state ourAddress groupAddr group + +-- | Construct a multicast group +-- +-- When the group is deleted some endpoints may still receive messages, but +-- subsequent calls to resolveMulticastGroup will fail. This mimicks the fact +-- that some multicast messages may still be in transit when the group is +-- deleted. +createMulticastGroup :: MVar TransportState -> EndPointAddress -> MulticastAddress -> MVar (Set EndPointAddress) -> MulticastGroup +createMulticastGroup state ourAddress groupAddress group = + MulticastGroup { multicastAddress = groupAddress + , deleteMulticastGroup = modifyMVar_ state $ return . (multigroups ^: Map.delete groupAddress) + , maxMsgSize = Nothing + , multicastSend = \payload -> do + cs <- (^. channels) <$> readMVar state + es <- readMVar group + forM_ (Set.elems es) $ \ep -> do + let ch = (cs ^. at ep "Invalid endpoint") + writeChan ch (ReceivedMulticast groupAddress payload) + , multicastSubscribe = modifyMVar_ group $ return . (Set.insert ourAddress) + , multicastUnsubscribe = modifyMVar_ group $ return . (Set.delete ourAddress) + , multicastClose = return () + } + +-- | Resolve a multicast group +apiResolveMulticastGroup :: MVar TransportState + -> EndPointAddress + -> MulticastAddress + -> IO (Either (TransportError ResolveMulticastGroupErrorCode) MulticastGroup) +apiResolveMulticastGroup state ourAddress groupAddress = do + group <- (^. (multigroups >>> DAC.mapMaybe groupAddress)) <$> readMVar state + case group of + Nothing -> return . Left $ TransportError ResolveMulticastGroupNotFound ("Group " ++ show groupAddress ++ " not found") + Just mvar -> return . Right $ createMulticastGroup state ourAddress groupAddress mvar + +-------------------------------------------------------------------------------- +-- Lens definitions -- +-------------------------------------------------------------------------------- + +channels :: Accessor TransportState (Map EndPointAddress (Chan Event)) +channels = accessor _channels (\ch st -> st { _channels = ch }) + +nextConnectionId :: Accessor TransportState (Map EndPointAddress ConnectionId) +nextConnectionId = accessor _nextConnectionId (\cid st -> st { _nextConnectionId = cid }) + +multigroups :: Accessor TransportState (Map MulticastAddress (MVar (Set EndPointAddress))) +multigroups = accessor _multigroups (\gs st -> st { _multigroups = gs }) + +at :: Ord k => k -> String -> Accessor (Map k v) v +at k err = accessor (Map.findWithDefault (error err) k) (Map.insert k) + +channelAt :: EndPointAddress -> Accessor TransportState (Chan Event) +channelAt addr = channels >>> at addr "Invalid channel" + +nextConnectionIdAt :: EndPointAddress -> Accessor TransportState ConnectionId +nextConnectionIdAt addr = nextConnectionId >>> at addr "Invalid connection ID" + +multigroupAt :: MulticastAddress -> Accessor TransportState (MVar (Set EndPointAddress)) +multigroupAt addr = multigroups >>> at addr "Invalid multigroup" + diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs new file mode 100644 index 00000000..f79bff77 --- /dev/null +++ b/src/Network/Transport/Internal.hs @@ -0,0 +1,93 @@ +-- | Internal functions +module Network.Transport.Internal ( -- * Encoders/decoders + encodeInt32 + , decodeInt32 + , encodeInt16 + , decodeInt16 + , prependLength + -- * Miscellaneous abstractions + , mapExceptionIO + , tryIO + , tryToEnum + , void + -- * Debugging + , tlog + ) where + +import Prelude hiding (catch) +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..), CShort(..)) +import Foreign.ForeignPtr (withForeignPtr) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS (length) +import qualified Data.ByteString.Internal as BSI (unsafeCreate, toForeignPtr, inlinePerformIO) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Exception (IOException, Exception, catch, try, throw) +--import Control.Concurrent (myThreadId) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt +foreign import ccall unsafe "htons" htons :: CShort -> CShort +foreign import ccall unsafe "ntohs" ntohs :: CShort -> CShort + +-- | Serialize 32-bit to network byte order +encodeInt32 :: Enum a => a -> ByteString +encodeInt32 i32 = + BSI.unsafeCreate 4 $ \p -> + pokeByteOff p 0 (htonl . fromIntegral . fromEnum $ i32) + +-- | Deserialize 32-bit from network byte order +decodeInt32 :: Num a => ByteString -> Maybe a +decodeInt32 bs | BS.length bs /= 4 = Nothing +decodeInt32 bs = Just . BSI.inlinePerformIO $ do + let (fp, _, _) = BSI.toForeignPtr bs + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral . ntohl $ w32) + +-- | Serialize 16-bit to network byte order +encodeInt16 :: Enum a => a -> ByteString +encodeInt16 i16 = + BSI.unsafeCreate 2 $ \p -> + pokeByteOff p 0 (htons . fromIntegral . fromEnum $ i16) + +-- | Deserialize 16-bit from network byte order +decodeInt16 :: Num a => ByteString -> Maybe a +decodeInt16 bs | BS.length bs /= 2 = Nothing +decodeInt16 bs = Just . BSI.inlinePerformIO $ do + let (fp, _, _) = BSI.toForeignPtr bs + withForeignPtr fp $ \p -> do + w16 <- peekByteOff p 0 + return (fromIntegral . ntohs $ w16) + +-- | Prepend a list of bytestrings with their total length +prependLength :: [ByteString] -> [ByteString] +prependLength bss = encodeInt32 (sum . map BS.length $ bss) : bss + +-- | Translate exceptions that arise in IO computations +mapExceptionIO :: (Exception e1, Exception e2) => (e1 -> e2) -> IO a -> IO a +mapExceptionIO f p = catch p (throw . f) + +-- | Like 'try', but lifted and specialized to IOExceptions +tryIO :: MonadIO m => IO a -> m (Either IOException a) +tryIO = liftIO . try + +-- | Logging (for debugging) +tlog :: MonadIO m => String -> m () +tlog _ = return () +{- +tlog msg = liftIO $ do + tid <- myThreadId + putStrLn $ show tid ++ ": " ++ msg +-} + +-- | Not all versions of "base" export 'void' +void :: Monad m => m a -> m () +void p = p >> return () + +-- | Safe version of 'toEnum' +tryToEnum :: (Enum a, Bounded a) => Int -> Maybe a +tryToEnum = go minBound maxBound + where + go :: Enum b => b -> b -> Int -> Maybe b + go lo hi n = if fromEnum lo <= n && n <= fromEnum hi then Just (toEnum n) else Nothing diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs new file mode 100644 index 00000000..59f65d29 --- /dev/null +++ b/src/Network/Transport/Internal/TCP.hs @@ -0,0 +1,102 @@ +-- | Utility functions for TCP sockets +module Network.Transport.Internal.TCP ( forkServer + , recvWithLength + , recvExact + , recvInt32 + ) where + +import Prelude hiding (catch) +import Network.Transport.Internal (decodeInt32) +import qualified Network.Socket as N ( HostName + , ServiceName + , Socket + , SocketType(Stream) + , SocketOption(ReuseAddr) + , getAddrInfo + , defaultHints + , socket + , bindSocket + , listen + , addrFamily + , addrAddress + , defaultProtocol + , setSocketOption + , accept + , sClose + ) +import qualified Network.Socket.ByteString as NBS (recv) +import Control.Concurrent (forkIO, ThreadId) +import Control.Monad (liftM, forever) +import Control.Exception (SomeException, handle, bracketOnError, throw) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS (length, concat, null) +import Data.Int (Int32) + +-- | Start a server at the specified address. +-- +-- This sets up a server socket for the specified host and port. Exceptions +-- thrown during setup are not caught. +-- +-- Once the socket is created we spawn a new thread which repeatedly accepts +-- incoming connections and executes the given request handler. If any +-- exception occurs the thread terminates and calls the terminationHandler. +-- This exception may occur because of a call to 'N.accept', because the thread +-- was explicitly killed, or because of a synchronous exception thrown by the +-- request handler. Typically, you should avoid the last case by catching any +-- relevant exceptions in the request handler. +-- +-- The request handler should spawn threads to handle each individual request +-- or the server will block. Once a thread has been spawned it will be the +-- responsibility of the new thread to close the socket when an exception +-- occurs. +forkServer :: N.HostName -- ^ Host + -> N.ServiceName -- ^ Port + -> Int -- ^ Backlog (maximum number of queued connections) + -> (SomeException -> IO ()) -- ^ Termination handler + -> (N.Socket -> IO ()) -- ^ Request handler + -> IO ThreadId +forkServer host port backlog terminationHandler requestHandler = do + -- Resolve the specified address. By specification, getAddrInfo will never + -- return an empty list (but will throw an exception instead) and will return + -- the "best" address first, whatever that means + addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just host) (Just port) + sock <- N.socket (N.addrFamily addr) N.Stream N.defaultProtocol + -- putStrLn $ "Created server socket " ++ show sock ++ " for address " ++ host ++ ":" ++ port + N.setSocketOption sock N.ReuseAddr 1 + N.bindSocket sock (N.addrAddress addr) + N.listen sock backlog + -- TODO: when is this socket closed? + forkIO . handle terminationHandler . forever $ + bracketOnError (N.accept sock) + (N.sClose . fst) + (requestHandler . fst) + +-- | Read a length and then a payload of that length +recvWithLength :: N.Socket -> IO [ByteString] +recvWithLength sock = recvInt32 sock >>= recvExact sock + +-- | Receive a 32-bit integer +recvInt32 :: Num a => N.Socket -> IO a +recvInt32 sock = do + mi <- liftM (decodeInt32 . BS.concat) $ recvExact sock 4 + case mi of + Nothing -> throw (userError "Invalid integer") + Just i -> return i + +-- | Read an exact number of bytes from a socket +-- +-- Throws an I/O exception if the socket closes before the specified +-- number of bytes could be read +recvExact :: N.Socket -- ^ Socket to read from + -> Int32 -- ^ Number of bytes to read + -> IO [ByteString] +recvExact _ len | len <= 0 = throw (userError "Negative length") +recvExact sock len = go [] len + where + go :: [ByteString] -> Int32 -> IO [ByteString] + go acc 0 = return (reverse acc) + go acc l = do + bs <- NBS.recv sock (fromIntegral l `min` 4096) + if BS.null bs + then throw (userError "Socket closed") + else go (bs : acc) (l - fromIntegral (BS.length bs)) diff --git a/src/Network/Transport/MVar.hs b/src/Network/Transport/MVar.hs deleted file mode 100644 index e80d296b..00000000 --- a/src/Network/Transport/MVar.hs +++ /dev/null @@ -1,75 +0,0 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE CPP #-} - -module Network.Transport.MVar - ( mkTransport - ) where - -import Control.Concurrent.MVar -import Data.IntMap (IntMap) -import qualified Data.Serialize as Ser - -import qualified Data.IntMap as IntMap - -import Network.Transport - -#ifndef LAZY -import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Char8 as BS -encode = Ser.encode -decode = Ser.decode -#else -import Data.ByteString.Lazy.Char8 (ByteString) -import qualified Data.ByteString.Lazy.Char8 as BS -encode = Ser.encodeLazy -decode = Ser.decodeLazy -#endif -{-# INLINE encode #-} -{-# INLINE decode #-} -encode :: Ser.Serialize a => a -> ByteString -decode :: Ser.Serialize a => ByteString -> Either String a - -type Chans = MVar (Int, IntMap (MVar [ByteString])) - -mkTransport :: IO Transport -mkTransport = do - channels <- newMVar (0, IntMap.empty) - return Transport - { newConnectionWith = \_ -> do - (i, m) <- takeMVar channels - receiveChan <- newEmptyMVar - let sourceAddr = i - !i' = i+1 - !m' = IntMap.insert i receiveChan m - putMVar channels (i', m') - return (mkSourceAddr channels sourceAddr, mkTargetEnd receiveChan) - , newMulticastWith = undefined - , deserialize = \bs -> - either (error "dummyBackend.deserializeSourceEnd: cannot parse") - (Just . mkSourceAddr channels) - (decode bs) - , closeTransport = return () - } - where - mkSourceAddr :: Chans -> Int -> SourceAddr - mkSourceAddr channels addr = SourceAddr - { connectWith = \_ -> mkSourceEnd channels addr - , serialize = encode addr - } - - mkSourceEnd :: Chans -> Int -> IO SourceEnd - mkSourceEnd channels addr = do - (_, m) <- readMVar channels - case IntMap.lookup addr m of - Nothing -> fail "dummyBackend.send: bad send address" - Just chan -> return SourceEnd - { send = realSend chan - } - - mkTargetEnd :: MVar [ByteString] -> TargetEnd - mkTargetEnd chan = TargetEnd - { receive = takeMVar chan - } - - realSend :: MVar [ByteString] -> [ByteString] -> IO () - realSend = putMVar diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index d819f748..298c1283 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -1,309 +1,1130 @@ -{-# LANGUAGE CPP #-} - -module Network.Transport.TCP - ( mkTransport - , TCPConfig (..) - ) where +-- | TCP implementation of the transport layer. +-- +-- The TCP implementation guarantees that only a single TCP connection (socket) +-- will be used between endpoints, provided that the addresses specified are +-- canonical. If /A/ connects to /B/ and reports its address as +-- @192.168.0.1:8080@ and /B/ subsequently connects tries to connect to /A/ as +-- @client1.local:http-alt@ then the transport layer will not realize that the +-- TCP connection can be reused. +-- +-- Applications that use the TCP transport should use +-- 'Network.Socket.withSocketsDo' in their main function for Windows +-- compatibility (see "Network.Socket"). +-- +-- TODOs: +-- * Output exception on channel after endpoint is closed +module Network.Transport.TCP ( -- * Main API + createTransport + , -- * TCP specific functionality (exported mostly for testing purposes_ + EndPointId + , encodeEndPointAddress + , decodeEndPointAddress + , ControlHeader(..) + , ConnectionRequestResponse(..) + , firstNonReservedConnectionId + , socketToEndPoint + -- * Design notes + -- $design + ) where +import Prelude hiding (catch) import Network.Transport - -import Control.Applicative +import Network.Transport.Internal.TCP ( forkServer + , recvWithLength + , recvInt32 + ) +import Network.Transport.Internal ( encodeInt32 + , decodeInt32 + , prependLength + , mapExceptionIO + , tryIO + , tryToEnum + , void + ) +import qualified Network.Socket as N ( HostName + , ServiceName + , Socket + , sClose + , getAddrInfo + , socket + , addrFamily + , addrAddress + , SocketType(Stream) + , defaultProtocol + , setSocketOption + , SocketOption(ReuseAddr) + , connect + , sOMAXCONN + , AddrInfo + ) +import Network.Socket.ByteString (sendMany) import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) -import Control.Concurrent.MVar -import Control.Exception (SomeException, IOException, AsyncException(ThreadKilled), - fromException, throwTo, throw, catch, handle) -import Control.Monad (forever, forM_) +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) +import Control.Concurrent.MVar ( MVar + , newMVar + , modifyMVar + , modifyMVar_ + , readMVar + , takeMVar + , putMVar + , newEmptyMVar + , withMVar + ) +import Control.Category ((>>>)) +import Control.Applicative ((<$>)) +import Control.Monad (forM_, when, unless) +import Control.Exception (IOException, SomeException, handle, throw, try, bracketOnError) +import Data.IORef (IORef, newIORef, writeIORef, readIORef) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS (concat) +import qualified Data.ByteString.Char8 as BSC (pack, unpack, split) +import Data.Int (Int32) +import Data.IntMap (IntMap) +import qualified Data.IntMap as IntMap (empty) +import Data.IntSet (IntSet) +import qualified Data.IntSet as IntSet (empty, insert, elems, singleton, null, delete) +import Data.Map (Map) +import qualified Data.Map as Map (empty, elems, size) +import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) +import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) +import System.IO (hPutStrLn, stderr) -import qualified Data.ByteString.Internal as BSI +-- $design +-- +-- [Goals] +-- +-- The TCP transport maps multiple logical connections between /A/ and /B/ (in +-- either direction) to a single TCP connection: +-- +-- > +-------+ +-------+ +-- > | A |==========================| B | +-- > | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | +-- > | Q |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~Q | +-- > | \~~~|~~~~~~~~~~~~~~~~~~~~~~~~~<| | +-- > | |==========================| | +-- > +-------+ +-------+ +-- +-- Ignoring the complications detailed below, the TCP connection is set up is +-- when the first lightweight connection is created (in either direction), and +-- torn down when the last lightweight connection (in either direction) is +-- closed. +-- +-- [Connecting] +-- +-- Let /A/, /B/ be two endpoints without any connections. When /A/ wants to +-- connect to /B/, it locally records that it is trying to connect to /B/ and +-- sends a request to /B/. As part of the request /A/ sends its own endpoint +-- address to /B/ (so that /B/ can reuse the connection in the other direction). +-- +-- When /B/ receives the connection request it first checks if it did not +-- already initiate a connection request to /A/. If not it will acknowledge the +-- connection request by sending 'ConnectionRequestAccepted' to /A/ and record +-- that it has a TCP connection to /A/. +-- +-- The tricky case arises when /A/ sends a connection request to /B/ and /B/ +-- finds that it had already sent a connection request to /A/. In this case /B/ +-- will accept the connection request from /A/ if /A/s endpoint address is +-- smaller (lexicographically) than /B/s, and reject it otherwise. If it rejects +-- it, it sends a 'ConnectionRequestCrossed' message to /A/. (The +-- lexicographical ordering is an arbitrary but convenient way to break the +-- tie.) +-- +-- When it receives a 'ConnectionRequestCrossed' message the /A/ thread that +-- initiated the request just needs to wait until the /A/ thread that is dealing +-- with /B/'s connection request completes. +-- +-- [Disconnecting] +-- +-- The TCP connection is created as soon as the first logical connection from +-- /A/ to /B/ (or /B/ to /A/) is established. At this point a thread (@#@) is +-- spawned that listens for incoming connections from /B/: +-- +-- > +-------+ +-------+ +-- > | A |==========================| B | +-- > | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | +-- > | | | Q | +-- > | #| | | +-- > | |==========================| | +-- > +-------+ +-------+ +-- +-- The question is when the TCP connection can be closed again. Conceptually, +-- we want to do reference counting: when there are no logical connections left +-- between /A/ and /B/ we want to close the socket (possibly after some +-- timeout). +-- +-- However, /A/ and /B/ need to agree that the refcount has reached zero. It +-- might happen that /B/ sends a connection request over the existing socket at +-- the same time that /A/ closes its logical connection to /B/ and closes the +-- socket. This will cause a failure in /B/ (which will have to retry) which is +-- not caused by a network failure, which is unfortunate. (Note that the +-- connection request from /B/ might succeed even if /A/ closes the socket.) +-- +-- Instead, when /A/ is ready to close the socket it sends a 'CloseSocket' +-- request to /B/ and records that its connection to /B/ is closing. If /A/ +-- receives a new connection request from /B/ after having sent the +-- 'CloseSocket' request it simply forgets that it sent a 'CloseSocket' request +-- and increments the reference count of the connection again. +-- +-- When /B/ receives a 'CloseSocket' message and it too is ready to close the +-- connection, it will respond with a reciprocal 'CloseSocket' request to /A/ +-- and then actually close the socket. /A/ meanwhile will not send any more +-- requests to /B/ after having sent a 'CloseSocket' request, and will actually +-- close its end of the socket only when receiving the 'CloseSocket' message +-- from /B/. (Since /A/ recorded that its connection to /B/ is in closing state +-- after sending a 'CloseSocket' request to /B/, it knows not to reciprocate /B/ +-- reciprocal 'CloseSocket' message.) +-- +-- If there is a concurrent thread in /A/ waiting to connect to /B/ after /A/ +-- has sent a 'CloseSocket' request then this thread will block until /A/ knows +-- whether to reuse the old socket (if /B/ sends a new connection request +-- instead of acknowledging the 'CloseSocket') or to set up a new socket. -import Data.IntMap (IntMap) -import qualified Data.IntMap as IntMap +-------------------------------------------------------------------------------- +-- Internal datatypes -- +-------------------------------------------------------------------------------- -import Data.Int (Int32) -import qualified Data.Serialize as Ser -import Data.Word (Word8) - -import Foreign.ForeignPtr (withForeignPtr) -import Foreign.Ptr (Ptr, plusPtr) -import Foreign.Storable (sizeOf) - -import Network.Socket - ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket - , SocketType (Stream), SocketOption (ReuseAddr) - , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol - , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) -import qualified Network.Socket as N - -import Text.Printf - -import Safe -import System.IO (stderr, hPutStrLn) -import Foreign.Storable (pokeByteOff, peekByteOff) -import Foreign.C (CInt(..)) -import Foreign.ForeignPtr (withForeignPtr) - -#ifndef LAZY -import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Char8 as BS -import qualified Network.Socket.ByteString as NBS -sendNBS = NBS.sendMany -encode = Ser.encode -decode = Ser.decode -#else -import Data.ByteString.Lazy.Char8 (ByteString) -import qualified Data.ByteString.Lazy.Char8 as BS -import qualified Network.Socket.ByteString.Lazy as NBS -sendNBS sock = NBS.sendAll sock . BS.concat -encode = Ser.encodeLazy -decode = Ser.decodeLazy -#endif -{-# INLINE encode #-} -{-# INLINE decode #-} -encode :: Ser.Serialize a => a -> ByteString -decode :: Ser.Serialize a => ByteString -> Either String a - -foreign import ccall unsafe "htonl" htonl :: CInt -> CInt -foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt - -type ChanId = Int -type Chans = MVar (ChanId, IntMap (MVar [ByteString], [(ThreadId, Socket)])) - --- | This deals with several different configuration properties: --- * Buffer size, specified in Hints --- * LAN/WAN, since we can inspect the addresses --- Note that HostName could be an IP address, and ServiceName could be --- a port number -data TCPConfig = TCPConfig Hints HostName ServiceName - --- | This creates a TCP connection between a server and a number of --- clients. Behind the scenes, the server hostname is passed as the SourceAddr --- and when a connection is made, messages sent down the SourceEnd go --- via a socket made for the client that connected. --- Messages are all queued using an unbounded Chan. -mkTransport :: TCPConfig -> IO Transport -mkTransport (TCPConfig _hints host service) = withSocketsDo $ do - chans <- newMVar (0, IntMap.empty) - serverAddrs <- getAddrInfo - (Just (N.defaultHints { addrFlags = [AI_PASSIVE] })) - Nothing - (Just service) - let serverAddr = case serverAddrs of - [] -> error "mkTransport: getAddrInfo returned []" - as -> head as - sock <- socket (addrFamily serverAddr) Stream defaultProtocol - setSocketOption sock ReuseAddr 1 - bindSocket sock (addrAddress serverAddr) - listen sock 5 - threadId <- forkWithExceptions forkIO "Connection Listener" $ - procConnections chans sock - - return Transport - { newConnectionWith = {-# SCC "newConnectionWith" #-}\_ -> do - (chanId, chanMap) <- takeMVar chans - chan <- newEmptyMVar - putMVar chans (chanId + 1, IntMap.insert chanId (chan, []) chanMap) - return (mkSourceAddr host service chanId, mkTargetEnd chans chanId chan) - , newMulticastWith = error "newMulticastWith: not defined" - , deserialize = {-# SCC "deserialize" #-} \bs -> - let (host, service, chanId) = - either (\m -> error $ printf "deserialize: %s" m) - id $ decode bs in - Just $ mkSourceAddr host service chanId - , closeTransport = {-# SCC "closeTransport" #-} do - -- Kill the transport channel process - killThread threadId - sClose sock - -- Kill all target end processes - (chanId, chanMap) <- takeMVar chans - forM_ (IntMap.elems chanMap) (\(chan, socks) -> - forM_ socks (\(threadId', sock') -> do - killThread threadId' - sClose sock')) - } +-- We use underscores for fields that we might update (using accessores) +-- +-- All data types follow the same structure: +-- +-- * A top-level data type describing static properties (TCPTransport, +-- LocalEndPoint, RemoteEndPoint) +-- * The 'static' properties include an MVar containing a data structure for +-- the dynamic properties (TransportState, LocalEndPointState, +-- RemoteEndPointState). The state could be invalid/valid/closed,/etc. +-- * For the case of "valid" we use third data structure to give more details +-- about the state (ValidTransportState, ValidLocalEndPointState, +-- ValidRemoteEndPointState). -mkSourceAddr :: HostName -> ServiceName -> ChanId -> SourceAddr -mkSourceAddr host service chanId = SourceAddr - { connectWith = {-# SCC "connectWith" #-} \_ -> mkSourceEnd host service chanId - , serialize = {-# SCC "serialize" #-} encode (host, service, chanId) +data TCPTransport = TCPTransport + { transportHost :: N.HostName + , transportPort :: N.ServiceName + , transportState :: MVar TransportState } -mkSourceEnd :: HostName -> ServiceName -> ChanId -> IO SourceEnd -mkSourceEnd host service chanId = withSocketsDo $ do - let err m = error $ printf "mkSourceEnd: %s" m - serverAddrs <- getAddrInfo Nothing (Just host) (Just service) - let serverAddr = case serverAddrs of - [] -> err "getAddrInfo returned []" - as -> head as - sock <- socket (addrFamily serverAddr) Stream defaultProtocol - setSocketOption sock ReuseAddr 1 - N.connect sock (addrAddress serverAddr) - NBS.sendAll sock $ encode (fromIntegral chanId :: Int32) - return SourceEnd - { send = {-# SCC "send" #-} \bss -> - let size = fromIntegral (sum . map BS.length $ bss) :: Int32 in - if size < 255 - then sendNBS sock (BS.singleton (toEnum $ fromEnum size) : bss) - else do size' <- encodeLength size - sendNBS sock (BS.singleton (toEnum 255) : size' : bss) - , closeSourceEnd = {-# SCC "closeSourceEnd" #-} sClose sock - } +data TransportState = + TransportValid ValidTransportState + | TransportClosed + +data ValidTransportState = ValidTransportState + { _localEndPoints :: Map EndPointAddress LocalEndPoint + } + +data LocalEndPoint = LocalEndPoint + { localAddress :: EndPointAddress + , localChannel :: Chan Event + , localState :: MVar LocalEndPointState + } + +data LocalEndPointState = + LocalEndPointValid ValidLocalEndPointState + | LocalEndPointClosed + +data ValidLocalEndPointState = ValidLocalEndPointState + { _nextConnectionId :: ConnectionId + , _pendingCtrlRequests :: IntMap (MVar [ByteString]) + , _nextCtrlRequestId :: ControlRequestId + , _localConnections :: Map EndPointAddress RemoteEndPoint + , _internalThreads :: [ThreadId] + } + +-- A remote endpoint has incoming and outgoing connections, and when the total +-- number of connections (that is, the 'remoteRefCount') drops to zero we want +-- to close the TCP connection to the endpoint. +-- +-- What we need to avoid, however, is a situation with two concurrent threads +-- where one closes the last (incoming or outgoing) connection, initiating the +-- process of closing the connection, while another requests (but does not yet +-- have) a new connection. +-- +-- We therefore insist that: +-- +-- 1. All operations that change the state of the endpoint (ask for a new +-- connection, close a connection, close the endpoint completely) are +-- serialized (that is, they take the contents of the MVar containing the +-- endpoint state before starting and don't put the updated contents back +-- until they have completed). +-- 2. Writing to ('apiSend') or reading from (in 'handleIncomingMessages') must +-- maintain the invariant that the connection they are writing to/reading +-- from *must* be "included" in the 'remoteRefCount'. +-- 3. Since every endpoint is associated with a single socket, we regard writes +-- that endpoint a state change too (i.e., we take the MVar before the write +-- and put it back after). The reason is that we don't want to "scramble" the +-- output of multiple concurrent writes (either from an explicit 'send' or +-- the writes for control messages). +-- +-- Of course, "serialize" does not mean that we want for the remote endpoint to +-- reply. "Send" takes the mvar, sends to the endpoint (asynchronously), and +-- then puts the mvar back, without waiting for the endpoint to receive the +-- message. Similarly, when requesting a new connection, we take the mvar, +-- tentatively increment the reference count, send the control request, and +-- then put the mvar back. When the remote host responds to the new connection +-- request we might have to do another state change (reduce the refcount) if +-- the connection request was refused but we don't want to increment the ref +-- count only after the remote host acknowledges the request because then a +-- concurrent 'close' might actually attempt to close the socket. +-- +-- Since we don't do concurrent reads from the same socket we don't need to +-- take the lock when reading from the socket. +-- +-- Moreover, we insist on the invariant (INV-CLOSE) that whenever we put an +-- endpoint in closed state we remove that endpoint from localConnections +-- first, so that if a concurrent thread reads the mvar, finds EndPointClosed, +-- and then looks up the endpoint in localConnections it is guaranteed to +-- either find a different remote endpoint, or else none at all. + +data RemoteEndPoint = RemoteEndPoint + { remoteAddress :: EndPointAddress + , remoteState :: MVar RemoteState + } + +data RemoteState = + RemoteEndPointInvalid (TransportError ConnectErrorCode) + | RemoteEndPointValid ValidRemoteEndPointState + | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState + | RemoteEndPointClosed -mkTargetEnd :: Chans -> ChanId -> MVar [ByteString] -> TargetEnd -mkTargetEnd chans chanId chan = TargetEnd - { -- for now we will implement this as an MVar - receive = {-# SCC "receive" #-} takeMVar chan - , closeTargetEnd = {-# SCC "closeTargetEnd" #-} do - (chanId', chanMap) <- takeMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> putMVar chans (chanId', chanMap) - Just (_, socks) -> do - forM_ socks $ \(threadId, sock) -> do - killThread threadId - sClose sock - let chanMap' = IntMap.delete chanId chanMap - putMVar chans (chanId', chanMap') +data ValidRemoteEndPointState = ValidRemoteEndPointState + { _remoteOutgoing :: !Int + , _remoteIncoming :: IntSet + , remoteSocket :: N.Socket + , sendOn :: [ByteString] -> IO () } --- | This function waits for inbound connections. If a connection fails --- for some reason, an error is raised. -procConnections :: Chans -> Socket -> IO () -procConnections chans sock = forever $ do - let err m = error $ printf "procConnections: %s" m - (clientSock, _clientAddr) <- accept sock - -- decode the first message to find the correct chanId - bss <- recvExact clientSock (fromIntegral $ sizeOf (0 :: Int32)) - case bss of - Left _ -> err "inbound chanId aborted" - Right bs -> do - let chanId = either err fromIntegral (decode bs :: Either String Int32) - (chanId', chanMap) <- takeMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> do - putMVar chans (chanId', chanMap) - err "cannot find chanId" - Just (chan, socks) -> do - threadId <- forkWithExceptions forkIO "Message Listener" $ - procMessages chans chanId chan clientSock - let chanMap' = IntMap.insert chanId (chan, (threadId, clientSock):socks) chanMap - putMVar chans (chanId', chanMap') - --- | This function first extracts a header of type Word8, which determines --- the size of the ByteString that follows. If this size is 0, this indicates --- that the ByteString is large, so the next value is an Int32, which --- determines the size of the ByteString that follows. The ByteString is then --- extracted from the socket, and then written to the Chan only when --- complete. If either of the first header size is null this indicates the --- socket has closed. -procMessages :: Chans -> ChanId -> MVar [ByteString] -> Socket -> IO () -procMessages chans chanId chan sock = do - esizeBS <- recvExact sock 1 - case esizeBS of - Left _ -> closeSocket - Right sizeBS -> - let size = toEnum . fromEnum $ BS.index sizeBS 0 in - if size < 255 - then procMessage size - else do esizeBS' <- recvExact sock (fromIntegral $ sizeOf (0 :: Int32)) - case esizeBS' of - Left _ -> closeSocket - Right sizeBS' -> do sizeDec <- decodeLength sizeBS' - procMessage sizeDec - where - err m = error $ printf "procMessages: %s" m - closeSocket :: IO () - closeSocket = do - (chanId', chanMap) <- takeMVar chans - case IntMap.lookup chanId chanMap of - Nothing -> do - putMVar chans (chanId', chanMap) - err "chanId not found." - Just (chan, socks) -> do - let socks' = filter ((/= sock) . snd) socks - let chanMap' = IntMap.insert chanId (chan, socks') chanMap - putMVar chans (chanId', chanMap') - sClose sock - procMessage :: Int32 -> IO () - procMessage size = do - ebs <- recvExact sock size - case ebs of - Left _ -> closeSocket - Right bs -> do - putMVar chan [bs] - procMessages chans chanId chan sock - --- | The normal result of `recvExact sock n` is `Right ByteString` --- whose string of length `n`, received from `sock`. If fewer than `n` --- bytes are read from `sock` before it closes, the result is `Left --- ByteString` whose string is those bytes that were received. No more --- bytes than necessary are read from the socket. NB: This uses --- Network.Socket.ByteString.recv, which may *discard* superfluous --- input depending on the socket type. -#ifndef LAZY -recvExact :: Socket -> Int32 -> IO (Either ByteString ByteString) -recvExact sock l = interceptAllExn "recvExact" $ do - res <- BSI.createAndTrim (fromIntegral l) (go 0) - case BS.length res of - n | n == (fromIntegral l) -> return $ Right res - n -> return $ Left res - where - go :: Int -> Ptr Word8 -> IO Int - go n ptr | n == (fromIntegral l) = return n - go n ptr = do - (p, off, len) <- BSI.toForeignPtr <$> NBS.recv sock (min (fromIntegral l-n) 4096) - if len == 0 - then return n - else withForeignPtr p $ \p -> do - BSI.memcpy (ptr `plusPtr` n) (p `plusPtr` off) (fromIntegral len) - go (n+len) ptr -#else -recvExact :: Socket -> Int32 -> IO (Either ByteString ByteString) -recvExact sock n = - interceptAllExn "recvExact" $ - go [] sock n - where - go :: [ByteString] -> Socket -> Int32 -> IO (Either ByteString ByteString) - go bss _ 0 = return (Right $ BS.concat (reverse bss)) - go bss sock n = do - bs <- NBS.recv sock (min (fromIntegral n) 4096) - if BS.null bs - then return (Left $ BS.concat (reverse bss)) - else go (bs:bss) sock (n - (fromIntegral $ BS.length bs)) -#endif - -interceptAllExn msg = - Control.Exception.handle $ \ e -> - case fromException e of - Just ThreadKilled -> throw e - Nothing -> do - BS.hPutStrLn stderr $ BS.pack$ "Exception inside "++msg++": "++show e - throw e --- throw (e :: SomeException) - -forkWithExceptions :: (IO () -> IO ThreadId) -> String -> IO () -> IO ThreadId -forkWithExceptions forkit descr action = do - parent <- myThreadId - forkit $ - Control.Exception.catch action - (\ e -> do - case fromException e of - Just ThreadKilled -> --- BSS.hPutStrLn stderr $ BSS.pack$ "Note: Child thread killed: "++descr - return () - _ -> do - BS.hPutStrLn stderr $ BS.pack$ "Exception inside child thread "++descr++": "++show e - throwTo parent (e::SomeException) - ) - --- | Encode length (manual for now) -encodeLength :: Int32 -> IO ByteString -encodeLength i32 = - BSI.create 4 $ \p -> - pokeByteOff p 0 (htonl (fromIntegral i32)) - --- | Decode length (manual for now) -decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in - withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 - return (fromIntegral (ntohl w32)) +type EndPointId = Int32 +type ControlRequestId = Int32 +type EndPointPair = (LocalEndPoint, RemoteEndPoint) + +-- | Control headers +data ControlHeader = + RequestConnectionId -- ^ Request a new connection ID from the remote endpoint + | CloseConnection -- ^ Tell the remote endpoint we will no longer be using a connection + | ControlResponse -- ^ Respond to a control request /from/ the remote endpoint + | CloseSocket -- ^ Request to close the connection (see module description) + deriving (Enum, Bounded, Show) + +-- Response sent by /B/ to /A/ when /A/ tries to connect +data ConnectionRequestResponse = + ConnectionRequestAccepted -- ^ /B/ accepts the connection + | ConnectionRequestEndPointInvalid -- ^ /A/ requested an invalid endpoint + | ConnectionRequestCrossed -- ^ /A/s request crossed with a request from /B/ (see protocols) + deriving (Enum, Bounded, Show) + +-------------------------------------------------------------------------------- +-- Top-level functionality -- +-------------------------------------------------------------------------------- + +-- | Create a TCP transport +-- +-- TODOs: deal with hints +createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) +createTransport host port = do + state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty } + let transport = TCPTransport { transportState = state + , transportHost = host + , transportPort = port + } + tryIO $ do + -- For a discussion of the use of N.sOMAXCONN, see + -- http://tangentsoft.net/wskfaq/advanced.html#backlog + -- http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/023/2333/2333s2.html + transportThread <- forkServer host port N.sOMAXCONN (terminationHandler transport) (handleConnectionRequest transport) + return Transport { newEndPoint = apiNewEndPoint transport + , closeTransport = killThread transportThread -- This will invoke the termination handler + } + where + terminationHandler :: TCPTransport -> SomeException -> IO () + terminationHandler transport _ = do + -- TODO: we currently don't make a distinction between endpoint failure and manual closure + mTSt <- modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> return (TransportClosed, Just vst) + TransportClosed -> return (TransportClosed, Nothing) + case mTSt of + Nothing -> + -- Transport already closed + return () + Just tSt -> + mapM_ (apiCloseEndPoint transport) (Map.elems $ tSt ^. localEndPoints) + +-------------------------------------------------------------------------------- +-- API functions -- +-------------------------------------------------------------------------------- + +-- | Create a new endpoint +apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) +apiNewEndPoint transport = try $ do + ourEndPoint <- createLocalEndPoint transport + return EndPoint + { receive = readChan (localChannel ourEndPoint) + , address = localAddress ourEndPoint + , connect = apiConnect ourEndPoint + , closeEndPoint = apiCloseEndPoint transport ourEndPoint + , newMulticastGroup = return . Left $ newMulticastGroupError + , resolveMulticastGroup = return . Left . const resolveMulticastGroupError + } + where + newMulticastGroupError = TransportError NewMulticastGroupUnsupported "TCP does not support multicast" + resolveMulticastGroupError = TransportError ResolveMulticastGroupUnsupported "TCP does not support multicast" + +-- | Connnect to an endpoint +apiConnect :: LocalEndPoint -- ^ Local end point + -> EndPointAddress -- ^ Remote address + -> Reliability -- ^ Reliability (ignored) + -> IO (Either (TransportError ConnectErrorCode) Connection) +apiConnect ourEndPoint theirAddress _ | localAddress ourEndPoint == theirAddress = + connectToSelf ourEndPoint +apiConnect ourEndPoint theirAddress _ = try $ do + (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress + -- connAlive can be an IORef rather than an MVar because it is protected by + -- the remoteState MVar. We don't need the overhead of locking twice. + connAlive <- newIORef True + return $ Connection { send = apiSend theirEndPoint connId connAlive + , close = apiClose theirEndPoint connId connAlive + } + +-- | Close a connection +-- +-- RELY: Remote endpoint must be in 'RemoteEndPointValid' or 'RemoteEndPointClosed' +-- GUARANTEE: If the connection is alive on entry then the remote endpoint will +-- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the state +-- of the remote endpoint will not be changed. +-- +-- TODO: We ignore errors during a close. Is that right? +apiClose :: RemoteEndPoint -> ConnectionId -> IORef Bool -> IO () +apiClose theirEndPoint connId connAlive = void . tryIO $ do + modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> do + alive <- readIORef connAlive + if alive + then do + writeIORef connAlive False + sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] + return (RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ vst) + else + return (RemoteEndPointValid vst) + RemoteEndPointClosed -> do + return st + RemoteEndPointClosing _ _ -> + fail "apiClose RELY violation" + RemoteEndPointInvalid _ -> + fail "apiClose RELY violation" + closeIfUnused theirEndPoint + +-- | Send data across a connection +-- +-- RELY: The remote endpoint must be in state 'RemoteEndPointValid' or 'RemoteEndPointClosed' +-- GUARANTEE: The state of the remote endpoint will not be changed. +apiSend :: RemoteEndPoint -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) +apiSend theirEndPoint connId connAlive payload = do + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> do + alive <- readIORef connAlive + try $ if alive + then mapExceptionIO sendFailed $ sendOn vst (encodeInt32 connId : prependLength payload) + else throw $ TransportError SendFailed "Connection closed" + RemoteEndPointClosed -> do + return (Left $ TransportError SendFailed "Endpoint closed") + RemoteEndPointClosing _ _ -> + error "apiSend RELY violation" + RemoteEndPointInvalid _ -> + error "apiSend RELY violation" + where + sendFailed :: IOException -> TransportError SendErrorCode + sendFailed = TransportError SendFailed . show + +-- | Force-close the endpoint +apiCloseEndPoint :: TCPTransport -> LocalEndPoint -> IO () +apiCloseEndPoint transport ourEndPoint = do + -- Remove the reference from the transport state + removeLocalEndPoint transport ourEndPoint + -- Close the local endpoint + mOurState <- modifyMVar (localState ourEndPoint) $ \st -> + case st of + LocalEndPointValid vst -> + return (LocalEndPointClosed, Just vst) + LocalEndPointClosed -> + return (LocalEndPointClosed, Nothing) + case mOurState of + Nothing -> -- Already closed + return () + Just vst -> do + -- Close all endpoints and kill all threads + forM_ (Map.elems $ vst ^. localConnections) $ tryCloseRemoteSocket + forM_ (vst ^. internalThreads) killThread + -- We send a single message to the endpoint that it is closed. Subsequent + -- calls will block. We could change this so that all subsequent calls to + -- receive return an error, but this would mean checking for some state on + -- every call to receive, which is an unnecessary overhead. + writeChan (localChannel ourEndPoint) EndPointClosed + where + -- Close the remote socket and return the set of all incoming connections + tryCloseRemoteSocket :: RemoteEndPoint -> IO () + tryCloseRemoteSocket theirEndPoint = do + -- We make an attempt to close the connection nicely (by sending a CloseSocket first) + modifyMVar_ (remoteState theirEndPoint) $ \st -> + case st of + RemoteEndPointInvalid _ -> + return st + RemoteEndPointValid conn -> do + -- Try to send a CloseSocket request + tryIO $ sendOn conn [encodeInt32 CloseSocket] + -- .. but even if it fails, close the socket anyway + -- (hence, two separate calls to tryIO) + tryIO $ N.sClose (remoteSocket conn) + return RemoteEndPointClosed + RemoteEndPointClosing _ conn -> do + tryIO $ N.sClose (remoteSocket conn) + return RemoteEndPointClosed + RemoteEndPointClosed -> + return RemoteEndPointClosed + +-- | Special case of 'apiConnect': connect an endpoint to itself +connectToSelf :: LocalEndPoint -> IO (Either (TransportError ConnectErrorCode) Connection) +connectToSelf ourEndPoint = do + -- Here connAlive must an MVar because it is not protected by another lock + connAlive <- newMVar True + -- TODO: catch exception + connId <- getNextConnectionId ourEndPoint + writeChan ourChan (ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint)) + return . Right $ Connection { send = selfSend connAlive connId + , close = selfClose connAlive connId + } + where + ourChan :: Chan Event + ourChan = localChannel ourEndPoint + + selfSend :: MVar Bool -> ConnectionId -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) + selfSend connAlive connId msg = do + modifyMVar connAlive $ \alive -> + if alive + then do + writeChan ourChan (Received connId msg) + return (alive, Right ()) + else + return (alive, Left (TransportError SendFailed "Connection closed")) + + selfClose :: MVar Bool -> ConnectionId -> IO () + selfClose connAlive connId = do + modifyMVar_ connAlive $ \alive -> do + when alive $ writeChan ourChan (ConnectionClosed connId) + return False + +-------------------------------------------------------------------------------- +-- Lower level functionality -- +-------------------------------------------------------------------------------- + +-- | Create a new local endpoint +-- +-- May throw a TransportError NewEndPointErrorCode exception if the transport is closed. +createLocalEndPoint :: TCPTransport -> IO LocalEndPoint +createLocalEndPoint transport = do + chan <- newChan + state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState + { _nextConnectionId = firstNonReservedConnectionId + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 + , _localConnections = Map.empty + , _internalThreads = [] + } + modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> do + let ix = fromIntegral $ Map.size (vst ^. localEndPoints) + let addr = encodeEndPointAddress (transportHost transport) (transportPort transport) ix + let localEndPoint = LocalEndPoint { localAddress = addr + , localChannel = chan + , localState = state + } + return (TransportValid . (localEndPointAt addr ^= Just localEndPoint) $ vst, localEndPoint) + TransportClosed -> + throw (TransportError NewEndPointFailed "Transport closed") + +-- | Request a connection to a remote endpoint +-- +-- This will block until we get a connection ID from the remote endpoint; if +-- the remote endpoint was in 'RemoteEndPointClosing' state then we will +-- additionally block until that is resolved. +-- +-- May throw a TransportError ConnectErrorCode exception. +requestConnectionTo :: LocalEndPoint + -> EndPointAddress + -> IO (RemoteEndPoint, ConnectionId) +requestConnectionTo ourEndPoint theirAddress = go + where + go = do + -- Find the remote endpoint (create it if it doesn't yet exist) + theirEndPoint <- findTheirEndPoint + let theirState = remoteState theirEndPoint + + -- Before we initiate the new connection request we want to make sure that + -- refcount on the endpoint is incremented so that a concurrent thread will + -- not close the connection. Note that if IF we return RemoteEndPointValid + -- here then we can rely on the endpoint remaining in that state. + endPointStateSnapshot <- modifyMVar theirState $ \st -> + case st of + RemoteEndPointValid ep -> + return (RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ ep, st) + _ -> + return (st, st) + + -- From this point on we are guaranteed the refcount is positive, provided + -- that the endpoint was valid. We still need to deal with the case where + -- it was not valid, however, which we didn't want to do while holding the + -- endpoint lock. + -- + -- Although 'endPointStateSnapshot' here refers to a snapshot of the + -- endpoint state, and might have changed in the meantime, these changes + -- won't matter. + case endPointStateSnapshot of + RemoteEndPointInvalid err -> do + throw err + + RemoteEndPointClosing resolved _ -> + -- If the remote endpoint is closing, then we need to block until + -- this is resolved and we then try again + readMVar resolved >> go + + RemoteEndPointClosed -> do + -- EndPointClosed indicates that a concurrent thread was in the + -- process of closing the TCP connection to the remote endpoint when + -- we obtained a reference to it. The remote endpoint will now have + -- been removed from ourState, so we simply try again. + go + + RemoteEndPointValid _ -> do + -- On a failure we decrement the refcount again and return an error. + -- The only state the remote endpoint can be in at this point is + -- valid. As mentioned above, we can rely on the endpoint being in + -- valid state at this point. + let failureHandler :: IOException -> IO b + failureHandler err = do + modifyMVar_ theirState $ \(RemoteEndPointValid ep) -> + return (RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ ep) + -- TODO: should we call closeIfUnused here? + throw $ TransportError ConnectFailed (show err) + + -- Do the actual connection request. This blocks until the remote + -- endpoint replies (but note that we don't hold any locks at this + -- point) + reply <- handle failureHandler $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + case decodeInt32 . BS.concat $ reply of + Nothing -> failureHandler (userError "Invalid integer") + Just cid -> return (theirEndPoint, cid) + + -- If this is a new endpoint, fork a thread to listen for incoming + -- connections. We don't want to do this while we hold the lock, because + -- forkEndPointThread modifies the local state too (to record the thread + -- ID) + findTheirEndPoint :: IO RemoteEndPoint + findTheirEndPoint = do + (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of + LocalEndPointClosed -> + throw (TransportError ConnectFailed "Local endpoint closed") + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Just theirEndPoint -> + return (st, (theirEndPoint, False)) + Nothing -> do + theirState <- newEmptyMVar + let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress + , remoteState = theirState + } + return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) $ vst, (theirEndPoint, True)) + + -- The only way for forkEndPointThread to fail is if the local endpoint + -- gets closed. This error will be caught elsewhere, so we ignore it + -- here. + when isNew . void . forkEndPointThread ourEndPoint $ + setupRemoteEndPoint (ourEndPoint, theirEndPoint) + return theirEndPoint + + ourState :: MVar LocalEndPointState + ourState = localState ourEndPoint + +-- | Set up a remote endpoint +-- +-- RELY: The state of the remote endpoint must be uninitialized. +-- GUARANTEE: Will only change the state to RemoteEndPointValid or +-- RemoteEndPointInvalid. +setupRemoteEndPoint :: EndPointPair -> IO () +setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do + result <- socketToEndPoint (localAddress ourEndPoint) (remoteAddress theirEndPoint) + case result of + Right (sock, ConnectionRequestAccepted) -> do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + } + putMVar theirState (RemoteEndPointValid vst) + handleIncomingMessages (ourEndPoint, theirEndPoint) + Right (sock, ConnectionRequestEndPointInvalid) -> do + -- We remove the endpoint from our local state again because the next + -- call to 'connect' might give a different result. Threads that were + -- waiting on the result of this call to connect will get the + -- RemoteEndPointInvalid; subsequent threads will initiate a new + -- connection requests. + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + putMVar theirState (RemoteEndPointInvalid (invalidAddress "Invalid endpoint")) + N.sClose sock + Right (sock, ConnectionRequestCrossed) -> do + N.sClose sock + Left err -> do + -- See comment above + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + putMVar theirState (RemoteEndPointInvalid err) + where + theirState = remoteState theirEndPoint + invalidAddress = TransportError ConnectNotFound + +-- | Establish a connection to a remote endpoint +socketToEndPoint :: EndPointAddress -- ^ Our address + -> EndPointAddress -- ^ Their address + -> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse)) +socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do + (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of + Nothing -> throw (failed . userError $ "Could not parse") + Just dec -> return dec + addr:_ <- mapExceptionIO invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port) + bracketOnError (createSocket addr) N.sClose $ \sock -> do + mapExceptionIO failed $ N.setSocketOption sock N.ReuseAddr 1 + mapExceptionIO invalidAddress $ N.connect sock (N.addrAddress addr) + response <- mapExceptionIO failed $ do + sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) + recvInt32 sock + case tryToEnum response of + Nothing -> throw (failed . userError $ "Unexpected response") + Just r -> return (sock, r) + where + createSocket :: N.AddrInfo -> IO N.Socket + createSocket addr = mapExceptionIO insufficientResources $ do + sock <- N.socket (N.addrFamily addr) N.Stream N.defaultProtocol + -- putStrLn $ "Created client socket " ++ show sock + return sock + + invalidAddress, insufficientResources, failed :: IOException -> TransportError ConnectErrorCode + invalidAddress = TransportError ConnectNotFound . show + insufficientResources = TransportError ConnectInsufficientResources . show + failed = TransportError ConnectFailed . show + +-- | Remove reference to a remote endpoint from a local endpoint +-- +-- If the local endpoint is closed, do nothing +removeRemoteEndPoint :: EndPointPair -> IO () +removeRemoteEndPoint (ourEndPoint, theirEndPoint) = do + modifyMVar_ (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> + return (LocalEndPointValid . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) $ vst) + LocalEndPointClosed -> + return LocalEndPointClosed + +-- | Remove reference to a local endpoint from the transport state +-- +-- Does nothing if the transport is closed +removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () +removeLocalEndPoint transport ourEndPoint = do + modifyMVar_ (transportState transport) $ \st -> case st of + TransportValid vst -> + return (TransportValid . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) $ vst) + TransportClosed -> + return TransportClosed + +-- | Encode end point address +encodeEndPointAddress :: N.HostName -> N.ServiceName -> EndPointId -> EndPointAddress +encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ + host ++ ":" ++ port ++ ":" ++ show ix + +-- | Decode end point address +decodeEndPointAddress :: EndPointAddress -> Maybe (N.HostName, N.ServiceName, EndPointId) +decodeEndPointAddress (EndPointAddress bs) = case map BSC.unpack $ BSC.split ':' bs of + [host, port, endPointIdStr] -> + case reads endPointIdStr of + [(endPointId, "")] -> Just (host, port, endPointId) + _ -> Nothing + _ -> + Nothing + +-- | Do a (blocking) remote request +-- +-- RELY: Remote endpoint must be in valid state. +-- GUARANTEE: Will not change the state of the remote endpoint. +-- +-- May throw IO (user) exception if the local endpoint is closed or if the send +-- fails. +doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] +doRemoteRequest (ourEndPoint, theirEndPoint) header = do + reply <- newEmptyMVar + reqId <- modifyMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> do + let reqId = vst ^. nextCtrlRequestId + return (LocalEndPointValid . (nextCtrlRequestId ^: (+ 1)) . (pendingCtrlRequestsAt reqId ^= Just reply) $ vst, reqId) + LocalEndPointClosed -> + throw (userError "Local endpoint closed") + withMVar (remoteState theirEndPoint) $ \(RemoteEndPointValid vst) -> + sendOn vst [encodeInt32 header, encodeInt32 reqId] + takeMVar reply + +-- | Check if the remote endpoint is unused, and if so, send a CloseSocket request +closeIfUnused :: RemoteEndPoint -> IO () +closeIfUnused theirEndPoint = modifyMVar_ (remoteState theirEndPoint) $ \st -> + case st of + RemoteEndPointValid vst -> do + if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + then do + resolved <- newEmptyMVar + sendOn vst [encodeInt32 CloseSocket] + return (RemoteEndPointClosing resolved vst) + else + return st + _ -> do + return st + +-- | Fork a new thread and store its ID as part of the transport state +-- +-- If the local end point is closed this function does nothing (no thread is +-- spawned). Returns whether or not a thread was spawned. +forkEndPointThread :: LocalEndPoint -> IO () -> IO Bool +forkEndPointThread ourEndPoint p = + modifyMVar ourState $ \st -> case st of + LocalEndPointValid vst -> do + tid <- forkIO (p >> removeThread) + return (LocalEndPointValid . (internalThreads ^: (tid :)) $ vst, True) + LocalEndPointClosed -> + return (LocalEndPointClosed, False) + where + removeThread :: IO () + removeThread = do + tid <- myThreadId + modifyMVar_ ourState $ \st -> case st of + LocalEndPointValid vst -> + return (LocalEndPointValid . (internalThreads ^: filter (/= tid)) $ vst) + LocalEndPointClosed -> + return LocalEndPointClosed + + ourState :: MVar LocalEndPointState + ourState = localState ourEndPoint + +-------------------------------------------------------------------------------- +-- Incoming requests -- +-------------------------------------------------------------------------------- + +-- | Handle a connection request (that is, a remote endpoint that is trying to +-- establish a TCP connection with us) +-- +-- 'handleConnectionRequest' runs in the context of the transport thread, which +-- can be killed asynchronously by 'closeTransport'. We fork a separate thread +-- as soon as we have located the lcoal endpoint that the remote endpoint is +-- interested in. We cannot fork any sooner because then we have no way of +-- storing the thread ID and hence no way of killing the thread when we take +-- the transport down. We must be careful to close the socket when a (possibly +-- asynchronous, ThreadKilled) exception occurs. (If an exception escapes from +-- handleConnectionRequest the transport will be shut down.) +handleConnectionRequest :: TCPTransport -> N.Socket -> IO () +handleConnectionRequest transport sock = handle tryCloseSocket $ do + ourEndPointId <- recvInt32 sock + theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock + let ourAddress = encodeEndPointAddress (transportHost transport) (transportPort transport) ourEndPointId + ourEndPoint <- withMVar (transportState transport) $ \st -> case st of + TransportValid vst -> + case vst ^. localEndPointAt ourAddress of + Nothing -> do + sendMany sock [encodeInt32 ConnectionRequestEndPointInvalid] + fail "Invalid endpoint" + Just ourEndPoint -> + return ourEndPoint + TransportClosed -> do + fail "Transport closed" + void . forkEndPointThread ourEndPoint $ go ourEndPoint theirAddress + where + go :: LocalEndPoint -> EndPointAddress -> IO () + go ourEndPoint theirAddress = do + mEndPoint <- handle (\e -> invalidEndPoint e >> return Nothing) $ do + (crossed, theirEndPoint) <- modifyMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Nothing -> do + theirState <- newEmptyMVar + let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress + , remoteState = theirState + } + return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) $ vst, (False, theirEndPoint)) + Just theirEndPoint -> + return (st, (localAddress ourEndPoint < theirAddress, theirEndPoint)) + LocalEndPointClosed -> + fail "Local endpoint closed" + if crossed + then do + tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] + tryIO $ N.sClose sock + return Nothing + else do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + } + -- TODO: this putMVar might block if the remote endpoint sends a + -- connection request for a local endpoint that it is already + -- connected to + putMVar (remoteState theirEndPoint) (RemoteEndPointValid vst) + sendMany sock [encodeInt32 ConnectionRequestAccepted] + return (Just theirEndPoint) + -- If we left the scope of the exception handler with a return value of + -- Nothing then the socket is already closed; otherwise, the socket has + -- been recorded as part of the remote endpoint. Either way, we no longer + -- have to worry about closing the socket on receiving an asynchronous + -- exception from this point forward. + case mEndPoint of + Nothing -> + return () + Just theirEndPoint -> + handleIncomingMessages (ourEndPoint, theirEndPoint) + + tryCloseSocket :: IOException -> IO () + tryCloseSocket _ = void . tryIO $ N.sClose sock + + invalidEndPoint :: IOException -> IO () + invalidEndPoint ex = do + tryIO $ sendMany sock [encodeInt32 ConnectionRequestEndPointInvalid] + tryCloseSocket ex + +-- | Handle requests from a remote endpoint. +-- +-- Returns only if the remote party closes the socket or if an error occurs. +-- +-- RELY: The remote endpoint must be in RemoteEndPointValid or +-- RemoteEndPointClosing state. If the latter, then the 'resolved' MVar +-- associated with the closing state must be empty. +-- GUARANTEE: May change the remote endpoint to RemoteEndPointClosed state. +handleIncomingMessages :: EndPointPair -> IO () +handleIncomingMessages (ourEndPoint, theirEndPoint) = do + -- For efficiency sake we get the socket once and for all + sock <- withMVar theirState $ \st -> + case st of + RemoteEndPointValid ep -> + return (remoteSocket ep) + RemoteEndPointClosing _ ep -> + return (remoteSocket ep) + _ -> + error "handleIncomingMessages RELY violation" + + mCleanExit <- tryIO $ go sock + case mCleanExit of + Right () -> return () + Left err -> prematureExit sock err + where + -- Dispatch + go :: N.Socket -> IO () + go sock = do + connId <- recvInt32 sock + if connId >= firstNonReservedConnectionId + then do + readMessage sock connId + go sock + else do + case tryToEnum (fromIntegral connId) of + Just RequestConnectionId -> do + recvInt32 sock >>= createNewConnection + go sock + Just ControlResponse -> do + recvInt32 sock >>= readControlResponse sock + go sock + Just CloseConnection -> do + recvInt32 sock >>= closeConnection + go sock + Just CloseSocket -> do + didClose <- closeSocket sock + unless didClose $ go sock + Nothing -> + -- Invalid control request, exit + hPutStrLn stderr "Warning: invalid control request" + + -- Create a new connection + createNewConnection :: ControlRequestId -> IO () + createNewConnection reqId = do + newId <- getNextConnectionId ourEndPoint + modifyMVar_ theirState $ \st -> do + vst <- case st of + RemoteEndPointValid vst -> + return (remoteIncoming ^: IntSet.insert newId $ vst) + RemoteEndPointClosing resolved vst -> do + -- If the endpoint is in closing state that means we send a + -- CloseSocket request to the remote endpoint. If the remote + -- endpoint replies with the request to create a new connection, it + -- either ignored our request or it sent the request before it got + -- ours. Either way, at this point we simply restore the endpoint + -- to RemoteEndPointValid + putMVar resolved () + return (remoteIncoming ^= IntSet.singleton newId $ vst) + _ -> + error "handleIncomingMessages RELY violation" + sendOn vst ( encodeInt32 ControlResponse + : encodeInt32 reqId + : prependLength [encodeInt32 newId] + ) + -- We add the new connection ID to the list of open connections only once the + -- endpoint has been notified of the new connection (sendOn may fail) + return (RemoteEndPointValid vst) + writeChan ourChannel (ConnectionOpened newId ReliableOrdered theirAddr) + + -- Read a control response + readControlResponse :: N.Socket -> ControlRequestId -> IO () + readControlResponse sock reqId = do + response <- recvWithLength sock + mmvar <- modifyMVar ourState $ \st -> case st of + LocalEndPointValid vst -> + return (LocalEndPointValid . (pendingCtrlRequestsAt reqId ^= Nothing) $ vst, vst ^. pendingCtrlRequestsAt reqId) + LocalEndPointClosed -> + fail "Local endpoint closed" + case mmvar of + Nothing -> do + hPutStrLn stderr $ "Warning: Invalid request ID" + return () -- Invalid request ID. TODO: We just ignore it? + Just mvar -> + putMVar mvar response + + -- Close a connection + closeConnection :: ConnectionId -> IO () + closeConnection cid = do + -- TODO: we should check that this connection is in fact open + writeChan ourChannel (ConnectionClosed cid) + modifyMVar_ theirState $ \(RemoteEndPointValid vst) -> + return (RemoteEndPointValid . (remoteIncoming ^: IntSet.delete cid) $ vst) + closeIfUnused theirEndPoint + + -- Close the socket (if we don't have any outgoing connections) + closeSocket :: N.Socket -> IO Bool + closeSocket sock = do + -- We need to check if we can close the socket (that is, if we don't have + -- any outgoing connections), and once we are sure that we can put the + -- endpoint in Closed state. However, by INV-CLOSE we can only put the + -- endpoint in Closed state once we remove the endpoint from our local + -- connections. But we can do that only once we are sure that we can + -- close the endpoint. Catch-22. We can resolve the catch-22 by locking + -- /both/ our local state and the endpoint state, but at the cost of + -- introducing a double lock and all the associated perils, or by putting + -- the remote endpoint in Closing state first. We opt for the latter. + canClose <- modifyMVar theirState $ \st -> + case st of + RemoteEndPointValid vst -> do + -- We regard a CloseSocket message as an (optimized) way for the + -- remote endpoint to indicate that all its connections to us are + -- now properly closed + forM_ (IntSet.elems $ vst ^. remoteIncoming) $ \cid -> + writeChan ourChannel (ConnectionClosed cid) + let vst' = remoteIncoming ^= IntSet.empty $ vst + -- Check if we agree that the connection should be closed + if vst' ^. remoteOutgoing == 0 + then do + -- Attempt to reply (but don't insist) + tryIO $ sendOn vst' [encodeInt32 CloseSocket] + resolved <- newEmptyMVar + return (RemoteEndPointClosing resolved vst', Just resolved) + else + return (RemoteEndPointValid vst', Nothing) + RemoteEndPointClosing resolved _ -> + return (st, Just resolved) + _ -> + error "handleIncomingConnections RELY violation" + + case canClose of + Nothing -> + return False + Just resolved -> do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + modifyMVar_ theirState $ return . const RemoteEndPointClosed + N.sClose sock + putMVar resolved () + return True + + -- Read a message and output it on the endPoint's channel + readMessage :: N.Socket -> ConnectionId -> IO () + readMessage sock connId = recvWithLength sock >>= writeChan ourChannel . Received connId + + -- Arguments + ourChannel = localChannel ourEndPoint + ourState = localState ourEndPoint + theirState = remoteState theirEndPoint + theirAddr = remoteAddress theirEndPoint + + -- Deal with a premature exit + prematureExit :: N.Socket -> IOException -> IO () + prematureExit sock err = do + N.sClose sock + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + mUnclosedConnections <- modifyMVar theirState $ \st -> + case st of + RemoteEndPointInvalid _ -> + error "handleIncomingMessages RELY violation" + RemoteEndPointValid vst -> + return (RemoteEndPointClosed, Just $ vst ^. remoteIncoming) + RemoteEndPointClosing _ _ -> + return (RemoteEndPointClosed, Nothing) + RemoteEndPointClosed -> + return (st, Nothing) + + -- We send the connection lost message even if unclosedConnections is the + -- empty set, because *outgoing* connections will be broken too now + case mUnclosedConnections of + Nothing -> + return () + Just unclosedConnections -> writeChan ourChannel . ErrorEvent $ + TransportError (EventConnectionLost (remoteAddress theirEndPoint) (IntSet.elems unclosedConnections)) (show err) + +-- | Get the next connection ID +-- +-- Throws an IO exception when the endpoint is closed. +getNextConnectionId :: LocalEndPoint -> IO ConnectionId +getNextConnectionId ourEndpoint = + modifyMVar (localState ourEndpoint) $ \st -> case st of + LocalEndPointValid vst -> do + let connId = vst ^. nextConnectionId + return (LocalEndPointValid . (nextConnectionId ^= connId + 1) $ vst, connId) + LocalEndPointClosed -> + fail "Local endpoint closed" + +-------------------------------------------------------------------------------- +-- Constants -- +-------------------------------------------------------------------------------- + +-- | We reserve a bunch of connection IDs for control messages +firstNonReservedConnectionId :: ConnectionId +firstNonReservedConnectionId = 1024 + +-------------------------------------------------------------------------------- +-- Accessor definitions -- +-------------------------------------------------------------------------------- + +localEndPoints :: Accessor ValidTransportState (Map EndPointAddress LocalEndPoint) +localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es }) + +pendingCtrlRequests :: Accessor ValidLocalEndPointState (IntMap (MVar [ByteString])) +pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) + +nextCtrlRequestId :: Accessor ValidLocalEndPointState ControlRequestId +nextCtrlRequestId = accessor _nextCtrlRequestId (\cid st -> st { _nextCtrlRequestId = cid }) + +nextConnectionId :: Accessor ValidLocalEndPointState ConnectionId +nextConnectionId = accessor _nextConnectionId (\cix st -> st { _nextConnectionId = cix }) + +localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) +localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) + +internalThreads :: Accessor ValidLocalEndPointState [ThreadId] +internalThreads = accessor _internalThreads (\ts st -> st { _internalThreads = ts }) + +localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) +localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr + +pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidLocalEndPointState (Maybe (MVar [ByteString])) +pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral ix) + +localConnectionTo :: EndPointAddress -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) +localConnectionTo addr = localConnections >>> DAC.mapMaybe addr + +remoteOutgoing :: Accessor ValidRemoteEndPointState Int +remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs }) + +remoteIncoming :: Accessor ValidRemoteEndPointState IntSet +remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) diff --git a/src/Network/Transport/Util.hs b/src/Network/Transport/Util.hs new file mode 100644 index 00000000..d2c0d4ad --- /dev/null +++ b/src/Network/Transport/Util.hs @@ -0,0 +1,24 @@ +-- | Utility functions +-- +-- Note: this module is bound to change even more than the rest of the API :) +module Network.Transport.Util (spawn) where + +import Network.Transport ( Transport + , EndPoint(..) + , EndPointAddress + , newEndPoint + ) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) + +-- | Fork a new thread, create a new end point on that thread, and run the specified IO operation on that thread. +-- +-- Returns the address of the new end point. +spawn :: Transport -> (EndPoint -> IO ()) -> IO EndPointAddress +spawn transport proc = do + addr <- newEmptyMVar + forkIO $ do + Right endpoint <- newEndPoint transport + putMVar addr (address endpoint) + proc endpoint + takeMVar addr diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs new file mode 100644 index 00000000..c31de0e3 --- /dev/null +++ b/tests/TestAuxiliary.hs @@ -0,0 +1,82 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module TestAuxiliary ( -- Running tests + runTest + , runTests + -- Writing tests + , forkTry + ) where + +import Prelude hiding (catch) +import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo) +import Control.Monad (liftM2, unless) +import Control.Exception (SomeException, try, catch) +import System.Timeout (timeout) +import System.IO (stdout, hFlush) +import System.Console.ANSI ( SGR(SetColor, Reset) + , Color(Red, Green) + , ConsoleLayer(Foreground) + , ColorIntensity(Vivid) + , setSGR + ) +import Network.Transport +import Traced (Traceable(..), traceShow) + +-- | Like fork, but throw exceptions in the child thread to the parent +forkTry :: IO () -> IO ThreadId +forkTry p = do + tid <- myThreadId + forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) + +-- | Run the given test, catching timeouts and exceptions +runTest :: String -> IO () -> IO Bool +runTest description test = do + putStr $ "Running " ++ show description ++ ": " + hFlush stdout + done <- try . timeout 10000000 $ test + case done of + Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" + Right Nothing -> failed $ "(timeout)" + Right (Just ()) -> ok + where + failed :: String -> IO Bool + failed err = do + setSGR [SetColor Foreground Vivid Red] + putStr "failed " + setSGR [Reset] + putStrLn err + return False + + ok :: IO Bool + ok = do + setSGR [SetColor Foreground Vivid Green] + putStrLn "ok" + setSGR [Reset] + return True + +-- | Run a bunch of tests and throw an exception if any fails +runTests :: [(String, IO ())] -> IO () +runTests tests = do + success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests + unless success $ fail "Some tests failed" + +-------------------------------------------------------------------------------- +-- traceShow instances -- +-------------------------------------------------------------------------------- + +instance Traceable EndPoint where + trace = const Nothing + +instance Traceable Transport where + trace = const Nothing + +instance Traceable Connection where + trace = const Nothing + +instance Traceable Event where + trace = traceShow + +instance Show err => Traceable (TransportError err) where + trace = traceShow + +instance Traceable EndPointAddress where + trace = traceShow . endPointAddressToByteString diff --git a/tests/TestInMemory.hs b/tests/TestInMemory.hs new file mode 100644 index 00000000..f7b6f70f --- /dev/null +++ b/tests/TestInMemory.hs @@ -0,0 +1,8 @@ +module Main where + +import TestTransport +import Network.Transport.Chan +import Control.Applicative ((<$>)) + +main :: IO () +main = testTransport (Right <$> createTransport) diff --git a/tests/TestMulticast.hs b/tests/TestMulticast.hs new file mode 100644 index 00000000..43eb526d --- /dev/null +++ b/tests/TestMulticast.hs @@ -0,0 +1,72 @@ +module TestMulticast where + +import Network.Transport +import TestAuxiliary (runTests) +import Control.Monad (replicateM, replicateM_, forM_, when) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar, readMVar) +import Data.ByteString (ByteString) +import Data.List (elemIndex) + +-- | Node for the "No confusion" test +noConfusionNode :: Transport -- ^ Transport + -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to + -> [MVar ()] -- ^ I'm ready : others ready + -> Int -- ^ number of pings + -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') + -> MVar () -- ^ I'm done + -> IO () +noConfusionNode transport groups ready numPings msgs done = do + -- Create a new endpoint + Right endpoint <- newEndPoint transport + + -- Create a new multicast group and broadcast its address + Right myGroup <- newMulticastGroup endpoint + putMVar (head groups) (multicastAddress myGroup) + + -- Subscribe to the given multicast groups + addrs <- mapM readMVar (tail groups) + forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr + multicastSubscribe group + + -- Indicate that we're ready and wait for everybody else to be ready + putMVar (head ready) () + mapM_ readMVar (tail ready) + + -- Send messages.. + forkIO . replicateM_ numPings $ multicastSend myGroup [head msgs] + + -- ..while checking that the messages we receive are the right ones + replicateM_ (2 * numPings) $ do + event <- receive endpoint + case event of + ReceivedMulticast addr [msg] -> + let mix = addr `elemIndex` addrs in + case mix of + Nothing -> error "Message from unexpected source" + Just ix -> when (msgs !! (ix + 1) /= msg) $ error "Unexpected message" + _ -> + error "Unexpected event" + + -- Success + putMVar done () + +-- | Test that distinct multicast groups are not confused +testNoConfusion :: Transport -> Int -> IO () +testNoConfusion transport numPings = do + [group1, group2, group3] <- replicateM 3 newEmptyMVar + [readyA, readyB, readyC] <- replicateM 3 newEmptyMVar + [doneA, doneB, doneC] <- replicateM 3 newEmptyMVar + let [msgA, msgB, msgC] = ["A says hi", "B says hi", "C says hi"] + + forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA + forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB + forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC + + mapM_ takeMVar [doneA, doneB, doneC] + +-- | Test multicast +testMulticast :: Transport -> IO () +testMulticast transport = + runTests + [ ("NoConfusion", testNoConfusion transport 10000) ] diff --git a/tests/TestMulticastInMemory.hs b/tests/TestMulticastInMemory.hs new file mode 100644 index 00000000..8494af64 --- /dev/null +++ b/tests/TestMulticastInMemory.hs @@ -0,0 +1,7 @@ +module Main where + +import TestMulticast +import Network.Transport.Chan (createTransport) + +main :: IO () +main = createTransport >>= testMulticast diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs new file mode 100644 index 00000000..a9ed76d1 --- /dev/null +++ b/tests/TestTCP.hs @@ -0,0 +1,487 @@ +{-# LANGUAGE RebindableSyntax #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Main where + +import Prelude hiding (catch, (>>=), (>>), return, fail) +import TestTransport (testTransport) +import TestAuxiliary (forkTry, runTests) +import Network.Transport +import Network.Transport.TCP (createTransport, encodeEndPointAddress) +import Data.Int (Int32) +import Control.Concurrent (threadDelay) +import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, readMVar, isEmptyMVar) +import Control.Monad (replicateM, guard, forM_) +import Control.Applicative ((<$>)) +import Control.Exception (throw) +import Network.Transport.TCP ( ControlHeader(..) + , ConnectionRequestResponse(..) + , socketToEndPoint + ) +import Network.Transport.Internal (encodeInt32, prependLength, tlog, tryIO) +import Network.Transport.Internal.TCP (recvInt32, forkServer, recvWithLength) +import qualified Network.Socket as N ( sClose + , ServiceName + , Socket + , AddrInfo + ) +import Network.Socket.ByteString (sendMany) +import Data.String (fromString) +import Traced + +instance Traceable ControlHeader where + trace = traceShow + +instance Traceable ConnectionRequestResponse where + trace = traceShow + +instance Traceable N.Socket where + trace = const Nothing + +instance Traceable N.AddrInfo where + trace = traceShow + + +-- Test that the server gets a ConnectionClosed message when the client closes +-- the socket without sending an explicit control message to the server first +testEarlyDisconnect :: IO N.ServiceName -> IO () +testEarlyDisconnect nextPort = do + clientAddr <- newEmptyMVar + serverAddr <- newEmptyMVar + serverDone <- newEmptyMVar + + tlog "testEarlyDisconnect" + forkTry $ server serverAddr clientAddr serverDone + forkTry $ client serverAddr clientAddr + + takeMVar serverDone + where + server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () + server serverAddr clientAddr serverDone = do + tlog "Server" + Right transport <- nextPort >>= createTransport "127.0.0.1" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + theirAddr <- readMVar clientAddr + + -- TEST 1: they connect to us, then drop the connection + do + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + ErrorEvent (TransportError (EventConnectionLost addr' [cid']) _) <- receive endpoint + True <- return $ addr' == theirAddr && cid' == cid + + return () + + -- TEST 2: after they dropped their connection to us, we now try to + -- establish a connection to them. This should re-establish the broken + -- TCP connection. + tlog "Trying to connect to client" + Right conn <- connect endpoint theirAddr ReliableOrdered + + -- TEST 3: To test the connection, we do a simple ping test; as before, + -- however, the remote client won't close the connection nicely but just + -- closes the socket + do + Right () <- send conn ["ping"] + + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + Received cid' ["pong"] <- receive endpoint + True <- return $ cid == cid' + + ErrorEvent (TransportError (EventConnectionLost addr' [cid'']) _) <- receive endpoint + True <- return $ addr' == theirAddr && cid'' == cid + + return () + + -- TEST 4: A subsequent send on an already-open connection will now break + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- *Pfew* + putMVar serverDone () + + client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () + client serverAddr clientAddr = do + tlog "Client" + clientPort <- nextPort + let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 + putMVar clientAddr ourAddress + + -- Listen for incoming messages + forkServer "127.0.0.1" clientPort 5 throw $ \sock -> do + -- Initial setup + 0 <- recvInt32 sock :: IO Int + _ <- recvWithLength sock + sendMany sock [encodeInt32 ConnectionRequestAccepted] + + -- Server requests a logical connection + RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) + reqId <- recvInt32 sock :: IO Int + sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + + -- Server sends a message + 10001 <- recvInt32 sock :: IO Int + ["ping"] <- recvWithLength sock + + -- Reply + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] + ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) + 10002 <- recvInt32 sock :: IO Int + [cid] <- recvWithLength sock + sendMany sock (cid : prependLength ["pong"]) + + -- Close the socket + N.sClose sock + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + + -- Request a new connection, but don't wait for the response + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + + -- Close the socket without closing the connection explicitly + -- The server should receive an error event + N.sClose sock + +-- | Test the behaviour of a premature CloseSocket request +testEarlyCloseSocket :: IO N.ServiceName -> IO () +testEarlyCloseSocket nextPort = do + clientAddr <- newEmptyMVar + serverAddr <- newEmptyMVar + serverDone <- newEmptyMVar + + tlog "testEarlyDisconnect" + forkTry $ server serverAddr clientAddr serverDone + forkTry $ client serverAddr clientAddr + + takeMVar serverDone + where + server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () + server serverAddr clientAddr serverDone = do + tlog "Server" + Right transport <- nextPort >>= createTransport "127.0.0.1" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + theirAddr <- readMVar clientAddr + + -- TEST 1: they connect to us, then send a CloseSocket. Since we don't + -- have any outgoing connections, this means we will agree to close the + -- socket + do + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + ConnectionClosed cid' <- receive endpoint + True <- return $ cid' == cid + + return () + + -- TEST 2: after they dropped their connection to us, we now try to + -- establish a connection to them. This should re-establish the broken + -- TCP connection. + tlog "Trying to connect to client" + Right conn <- connect endpoint theirAddr ReliableOrdered + + -- TEST 3: To test the connection, we do a simple ping test; as before, + -- however, the remote client won't close the connection nicely but just + -- sends a CloseSocket -- except that now we *do* have outgoing + -- connections, so we won't agree and hence will receive an error when + -- the socket gets closed + do + Right () <- send conn ["ping"] + + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + Received cid' ["pong"] <- receive endpoint + True <- return $ cid' == cid + + ConnectionClosed cid'' <- receive endpoint + True <- return $ cid'' == cid + + ErrorEvent (TransportError (EventConnectionLost addr' []) _) <- receive endpoint + True <- return $ addr' == theirAddr + + return () + + -- TEST 4: A subsequent send on an already-open connection will now break + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- *Pfew* + putMVar serverDone () + + client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () + client serverAddr clientAddr = do + tlog "Client" + clientPort <- nextPort + let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 + putMVar clientAddr ourAddress + + -- Listen for incoming messages + forkServer "127.0.0.1" clientPort 5 throw $ \sock -> do + -- Initial setup + 0 <- recvInt32 sock :: IO Int + _ <- recvWithLength sock + sendMany sock [encodeInt32 ConnectionRequestAccepted] + + -- Server requests a logical connection + RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) + reqId <- recvInt32 sock :: IO Int + sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + + -- Server sends a message + 10001 <- recvInt32 sock :: IO Int + ["ping"] <- recvWithLength sock + + -- Reply + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] + ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) + 10002 <- recvInt32 sock :: IO Int + [cid] <- recvWithLength sock + sendMany sock (cid : prependLength ["pong"]) + + -- Send a CloseSocket even though there are still connections *in both + -- directions* + sendMany sock [encodeInt32 CloseSocket] + N.sClose sock + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + + -- Request a new connection, but don't wait for the response + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + + -- Send a CloseSocket without sending a closeconnecton + -- The server should still receive a ConnectionClosed message + sendMany sock [encodeInt32 CloseSocket] + N.sClose sock + +-- | Test the creation of a transport with an invalid address +testInvalidAddress :: IO N.ServiceName -> IO () +testInvalidAddress nextPort = do + Left _ <- nextPort >>= createTransport "invalidHostName" + return () + +-- | Test connecting to invalid or non-existing endpoints +testInvalidConnect :: IO N.ServiceName -> IO () +testInvalidConnect nextPort = do + port <- nextPort + Right transport <- createTransport "127.0.0.1" port + Right endpoint <- newEndPoint transport + + -- Syntax error in the endpoint address + Left (TransportError ConnectFailed _) <- + connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered + + -- Syntax connect, but invalid hostname (TCP address lookup failure) + Left (TransportError ConnectNotFound _) <- + connect endpoint (encodeEndPointAddress "invalidHost" "port" 0) ReliableOrdered + + -- TCP address correct, but nobody home at that address + Left (TransportError ConnectNotFound _) <- + connect endpoint (encodeEndPointAddress "127.0.0.1" "9000" 0) ReliableOrdered + + -- Valid TCP address but invalid endpoint number + Left (TransportError ConnectNotFound _) <- + connect endpoint (encodeEndPointAddress "127.0.0.1" port 1) ReliableOrdered + + return () + +-- | Test that an endpoint can ignore CloseSocket requests (in "reality" this +-- would happen when the endpoint sends a new connection request before +-- receiving an (already underway) CloseSocket request) +testIgnoreCloseSocket :: IO N.ServiceName -> IO () +testIgnoreCloseSocket nextPort = do + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + Right transport <- nextPort >>= createTransport "127.0.0.1" + + forkTry $ server transport serverAddr + forkTry $ client transport serverAddr clientDone + + takeMVar clientDone + + where + server :: Transport -> MVar EndPointAddress -> IO () + server transport serverAddr = do + tlog "Server" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Wait for the client to connect and disconnect + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint + + -- At this point the server will have sent a CloseSocket request to the + -- client, which however ignores it, instead it requests and closes + -- another connection + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint + + + tlog "Server waiting.." + + client :: Transport -> MVar EndPointAddress -> MVar () -> IO () + client transport serverAddr clientDone = do + tlog "Client" + Right endpoint <- newEndPoint transport + let ourAddress = address endpoint + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + + -- Request a new connection + tlog "Requesting connection" + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + response <- replicateM 4 $ recvInt32 sock :: IO [Int32] + + -- Close the connection again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] + + -- Server will now send a CloseSocket request as its refcount reached 0 + tlog "Waiting for CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + + -- But we ignore it and request another connection + tlog "Ignoring it, requesting another connection" + let reqId' = 1 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] + response' <- replicateM 4 $ recvInt32 sock :: IO [Int32] + + -- Close it again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response' !! 3)] + + -- We now get a CloseSocket again, and this time we heed it + tlog "Waiting for second CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + + tlog "Closing socket" + sendMany sock [encodeInt32 CloseSocket] + N.sClose sock + + putMVar clientDone () + +-- | Like 'testIgnoreSocket', but now the server requests a connection after the +-- client closed their connection. In the meantime, the server will have sent a +-- CloseSocket request to the client, and must block until the client responds. +testBlockAfterCloseSocket :: IO N.ServiceName -> IO () +testBlockAfterCloseSocket nextPort = do + serverAddr <- newEmptyMVar + clientAddr <- newEmptyMVar + clientDone <- newEmptyMVar + port <- nextPort + Right transport <- createTransport "127.0.0.1" port + + forkTry $ server transport serverAddr clientAddr + forkTry $ client transport serverAddr clientAddr clientDone + + takeMVar clientDone + + where + server :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> IO () + server transport serverAddr clientAddr = do + tlog "Server" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Wait for the client to connect and disconnect + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint + + -- At this point the server will have sent a CloseSocket request to the + -- client, and must block until the client responds + tlog "Server waiting to connect to the client.." + Right _ <- readMVar clientAddr >>= \addr -> connect endpoint addr ReliableOrdered + + tlog "Server waiting.." + + client :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () + client transport serverAddr clientAddr clientDone = do + tlog "Client" + Right endpoint <- newEndPoint transport + putMVar clientAddr (address endpoint) + let ourAddress = address endpoint + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + + -- Request a new connection + tlog "Requesting connection" + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + response <- replicateM 4 $ recvInt32 sock :: IO [Int32] + + -- Close the connection again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] + + -- Server will now send a CloseSocket request as its refcount reached 0 + tlog "Waiting for CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + + unblocked <- newEmptyMVar + + -- We should not hear from the server until we unblock him by + -- responding to the CloseSocket request (in this case, we + -- respond by sending a ConnectionRequest) + forkTry $ do + recvInt32 sock :: IO Int32 + isEmptyMVar unblocked >>= (guard . not) + putMVar clientDone () + + threadDelay 1000000 + + tlog "Client ignores close socket and sends connection request" + tlog "This should unblock the server" + putMVar unblocked () + let reqId' = 1 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] + +-- | Test what happens when a remote endpoint sends a connection request to our +-- transport for an endpoint it already has a connection to +testUnnecessaryConnect :: IO N.ServiceName -> IO () +testUnnecessaryConnect nextPort = do + clientDone <- newEmptyMVar + serverAddr <- newEmptyMVar + + forkTry $ do + Right transport <- nextPort >>= createTransport "127.0.0.1" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + forkTry $ do + let ourAddress = EndPointAddress "ourAddress" + Right (_, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (_, ConnectionRequestEndPointInvalid) <- readMVar serverAddr >>= socketToEndPoint ourAddress + putMVar clientDone () + + takeMVar clientDone + +main :: IO () +main = do + portMVar <- newEmptyMVar + forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show + let nextPort = takeMVar portMVar + tryIO $ runTests + [ ("EarlyDisconnect", testEarlyDisconnect nextPort) + , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) + , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) + , ("BlockAfterCloseSocket", testBlockAfterCloseSocket nextPort) + , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort) + , ("InvalidAddress", testInvalidAddress nextPort) + , ("InvalidConnect", testInvalidConnect nextPort) + ] + testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs new file mode 100644 index 00000000..efbd12f5 --- /dev/null +++ b/tests/TestTransport.hs @@ -0,0 +1,728 @@ +{-# LANGUAGE RebindableSyntax #-} +module TestTransport where + +import Prelude hiding (catch, (>>=), (>>), return, fail) +import TestAuxiliary (forkTry, runTests) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) +import Control.Monad (replicateM, replicateM_, when, guard, forM_) +import Control.Monad.Error () +import Network.Transport +import Network.Transport.Internal (tlog) +import Network.Transport.Util (spawn) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack) +import Data.Map (Map) +import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) +import Data.String (fromString) +import Traced + +-- | Server that echoes messages straight back to the origin endpoint. +echoServer :: EndPoint -> IO () +echoServer endpoint = do + go Map.empty + where + go :: Map ConnectionId Connection -> IO () + go cs = do + event <- receive endpoint + case event of + ConnectionOpened cid rel addr -> do + tlog $ "Opened new conncetion " ++ show cid + Right conn <- connect endpoint addr rel + go (Map.insert cid conn cs) + Received cid payload -> do + send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload + go cs + ConnectionClosed cid -> do + tlog $ "Close connection " ++ show cid + close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) + go (Map.delete cid cs) + ReceivedMulticast _ _ -> + -- Ignore + go cs + ErrorEvent _ -> + fail (show event) + EndPointClosed -> + return () + +-- | Ping client used in a few tests +ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () +ping endpoint server numPings msg = do + -- Open connection to the server + tlog "Connect to echo server" + Right conn <- connect endpoint server ReliableOrdered + + -- Wait for the server to open reply connection + tlog "Wait for ConnectionOpened message" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings and wait for reply + tlog "Send ping and wait for reply" + replicateM_ numPings $ do + send conn [msg] + Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg + return () + + -- Close the connection + tlog "Close the connection" + close conn + + -- Wait for the server to close its connection to us + tlog "Wait for ConnectionClosed message" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + -- Done + tlog "Ping client done" + +-- | Basic ping test +testPingPong :: Transport -> Int -> IO () +testPingPong transport numPings = do + tlog "Starting ping pong test" + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + tlog "Ping client" + Right endpoint <- newEndPoint transport + ping endpoint server numPings "ping" + putMVar result () + + takeMVar result + +-- | Test that endpoints don't get confused +testEndPoints :: Transport -> Int -> IO () +testEndPoints transport numPings = do + server <- spawn transport echoServer + dones <- replicateM 2 newEmptyMVar + + forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do + let name' :: ByteString + name' = pack [name] + Right endpoint <- newEndPoint transport + tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) + ping endpoint server numPings name' + putMVar done () + + forM_ dones takeMVar + +-- Test that connections don't get confused +testConnections :: Transport -> Int -> IO () +testConnections transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ replicateM_ numPings $ send conn1 ["pingA"] + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ numPings $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (2 * numPings) + + takeMVar result + +-- | Test that closing one connection does not close the other +testCloseOneConnection :: Transport -> Int -> IO () +testCloseOneConnection transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ do + replicateM_ numPings $ send conn1 ["pingA"] + close conn1 + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (3 * numPings) + + takeMVar result + +-- | Test that if A connects to B and B connects to A, B can still send to A after +-- A closes its connection to B (for instance, in the TCP transport, the socket pair +-- connecting A and B should not yet be closed). +testCloseOneDirection :: Transport -> Int -> IO () +testCloseOneDirection transport numPings = do + addrA <- newEmptyMVar + addrB <- newEmptyMVar + doneA <- newEmptyMVar + doneB <- newEmptyMVar + + -- A + forkTry $ do + tlog "A" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrA (address endpoint) + + -- Connect to B + tlog "Connect to B" + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered + + -- Wait for B to connect to us + tlog "Wait for B" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings to B + tlog "Send pings to B" + replicateM_ numPings $ send conn ["ping"] + + -- Close our connection to B + tlog "Close connection" + close conn + + -- Wait for B's pongs + tlog "Wait for pongs from B" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for B to close it's connection to us + tlog "Wait for B to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Done + tlog "Done" + putMVar doneA () + + -- B + forkTry $ do + tlog "B" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrB (address endpoint) + + -- Wait for A to connect + tlog "Wait for A to connect" + ConnectionOpened cid _ _ <- receive endpoint + + -- Connect to A + tlog "Connect to A" + Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered + + -- Wait for A's pings + tlog "Wait for pings from A" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for A to close it's connection to us + tlog "Wait for A to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Send pongs to A + tlog "Send pongs to A" + replicateM_ numPings $ send conn ["pong"] + + -- Close our connection to A + tlog "Close connection to A" + close conn + + -- Done + tlog "Done" + putMVar doneB () + + mapM_ takeMVar [doneA, doneB] + +-- | Collect a given number of events and order them by connection ID +collect :: EndPoint -> Int -> IO [(ConnectionId, [[ByteString]])] +collect endPoint numEvents = go numEvents Map.empty Map.empty + where + -- TODO: for more serious use of this function we'd need to make these arguments strict + go 0 open closed = if Map.null open + then return . Map.toList . Map.map reverse $ closed + else fail "Open connections" + go n open closed = do + event <- receive endPoint + case event of + ConnectionOpened cid _ _ -> + go (n - 1) (Map.insert cid [] open) closed + ConnectionClosed cid -> + let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in + go (n - 1) (Map.delete cid open) (Map.insert cid list closed) + Received cid msg -> + go (n - 1) (Map.adjust (msg :) cid open) closed + ReceivedMulticast _ _ -> + fail "Unexpected multicast" + ErrorEvent _ -> + fail "Unexpected error" + EndPointClosed -> + fail "Unexpected endpoint closure" + +-- | Open connection, close it, then reopen it +-- (In the TCP transport this means the socket will be closed, then reopened) +-- +-- Note that B cannot expect to receive all of A's messages on the first connection +-- before receiving the messages on the second connection. What might (and sometimes +-- does) happen is that finishes sending all of its messages on the first connection +-- (in the TCP transport, the first socket pair) while B is behind on reading _from_ +-- this connection (socket pair) -- the messages are "in transit" on the network +-- (these tests are done on localhost, so there are in some OS buffer). Then when +-- A opens the second connection (socket pair) B will spawn a new thread for this +-- connection, and hence might start interleaving messages from the first and second +-- connection. +-- +-- This is correct behaviour, however: the transport API guarantees reliability and +-- ordering _per connection_, but not _across_ connections. +testCloseReopen :: Transport -> Int -> IO () +testCloseReopen transport numPings = do + addrB <- newEmptyMVar + doneB <- newEmptyMVar + + let numRepeats = 2 :: Int + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + + forM_ [1 .. numRepeats] $ \i -> do + tlog "A connecting" + -- Connect to B + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered + + tlog "A pinging" + -- Say hi + forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] + + tlog "A closing" + -- Disconnect again + close conn + + tlog "A finishing" + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar addrB (address endpoint) + + eventss <- collect endpoint (numRepeats * (numPings + 2)) + + forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do + forM_ (zip [1 .. numPings] events) $ \(j, event) -> do + guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) + + putMVar doneB () + + takeMVar doneB + +-- | Test lots of parallel connection attempts +testParallelConnects :: Transport -> Int -> IO () +testParallelConnects transport numPings = do + server <- spawn transport echoServer + done <- newEmptyMVar + + Right endpoint <- newEndPoint transport + + -- Spawn lots of clients + forM_ [1 .. numPings] $ \i -> forkTry $ do + Right conn <- connect endpoint server ReliableOrdered + send conn [pack $ "ping" ++ show i] + send conn [pack $ "ping" ++ show i] + close conn + + forkTry $ do + eventss <- collect endpoint (numPings * 4) + -- Check that no pings got sent to the wrong connection + forM_ eventss $ \(_, [[ping1], [ping2]]) -> + guard (ping1 == ping2) + putMVar done () + + takeMVar done + +-- | Test that sending on a closed connection gives an error +testSendAfterClose :: Transport -> Int -> IO () +testSendAfterClose transport _ = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + -- We request two lightweight connections + Right conn1 <- connect endpoint server ReliableOrdered + Right conn2 <- connect endpoint server ReliableOrdered + + -- Close the second, but leave the first open; then output on the second + -- connection (i.e., on a closed connection while there is still another + -- connection open) + close conn2 + Left (TransportError SendFailed _) <- send conn2 ["ping2"] + + -- Now close the first connection, and output on it (i.e., output while + -- there are no lightweight connection at all anymore) + close conn1 + Left (TransportError SendFailed _) <- send conn2 ["ping2"] + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that closing the same connection twice has no effect +testCloseTwice :: Transport -> Int -> IO () +testCloseTwice transport _ = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + -- We request two lightweight connections + Right conn1 <- connect endpoint server ReliableOrdered + Right conn2 <- connect endpoint server ReliableOrdered + + -- Close the second one twice + close conn2 + close conn2 + + -- Then send a message on the first and close that too + send conn1 ["ping"] + close conn1 + + -- Verify expected response from the echo server + ConnectionOpened cid1 _ _ <- receive endpoint + ConnectionOpened cid2 _ _ <- receive endpoint + ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 + Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 + ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that we can connect an endpoint to itself +testConnectToSelf :: Transport -> Int -> IO () +testConnectToSelf transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn <- connect endpoint (address endpoint) ReliableOrdered + + tlog "Talk to myself" + + -- One thread to write to the endpoint + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn ["ping"] + + tlog $ "Closing connection" + close conn + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + tlog "Waiting for ConnectionOpened" + ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint + + tlog "Waiting for Received" + replicateM_ numPings $ do + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + return () + + tlog "Waiting for ConnectionClosed" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we can connect an endpoint to itself multiple times +testConnectToSelfTwice :: Transport -> Int -> IO () +testConnectToSelfTwice transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn1 <- connect endpoint (address endpoint) ReliableOrdered + Right conn2 <- connect endpoint (address endpoint) ReliableOrdered + + tlog "Talk to myself" + + -- One thread to write to the endpoint using the first connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn1 ["pingA"] + + tlog $ "Closing connection" + close conn1 + + -- One thread to write to the endpoint using the second connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn2 ["pingB"] + + tlog $ "Closing connection" + close conn2 + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + [(_, events1), (_, events2)] <- collect endpoint (2 * (numPings + 2)) + True <- return $ events1 == replicate numPings ["pingA"] + True <- return $ events2 == replicate numPings ["pingB"] + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test various aspects of 'closeEndPoint' +testCloseEndPoint :: Transport -> Int -> IO () +testCloseEndPoint transport _ = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- First test (see client) + do + theirAddr <- readMVar clientAddr1 + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + return () + + -- Second test + do + theirAddr <- readMVar clientAddr2 + + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + + Right conn <- connect endpoint theirAddr ReliableOrdered + send conn ["pong"] + + ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' + ErrorEvent (TransportError (EventConnectionLost addr' []) _) <- receive endpoint ; True <- return $ addr' == theirAddr + + Left (TransportError SendFailed _) <- send conn ["pong2"] + + return () + + putMVar serverDone () + + -- Client + forkTry $ do + theirAddr <- readMVar serverAddr + + -- First test: close endpoint with one outgoing but no incoming connections + do + Right endpoint <- newEndPoint transport + putMVar clientAddr1 (address endpoint) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint theirAddr ReliableOrdered + closeEndPoint endpoint + EndPointClosed <- receive endpoint + return () + + -- Second test: close endpoint with one outgoing and one incoming connection + do + Right endpoint <- newEndPoint transport + putMVar clientAddr2 (address endpoint) + + Right conn <- connect endpoint theirAddr ReliableOrdered + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' + + -- Close the endpoint + closeEndPoint endpoint + EndPointClosed <- receive endpoint + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered + + return () + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- Test closeTransport +-- +-- This tests many of the same things that testEndPoint does, and some more +testCloseTransport :: IO (Either String Transport) -> IO () +testCloseTransport newTransport = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right transport <- newTransport + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Client sets up first endpoint + theirAddr1 <- readMVar clientAddr1 + ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 + + -- Client sets up second endpoint + theirAddr2 <- readMVar clientAddr2 + + ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 + Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 + + Right conn <- connect endpoint theirAddr2 ReliableOrdered + send conn ["pong"] + + -- Client now closes down its transport. We should receive connection closed messages + -- TODO: this assumes a certain ordering on the messages we receive; that's not guaranteed + ConnectionClosed cid2'' <- receive endpoint ; True <- return $ cid2'' == cid2 + ErrorEvent (TransportError (EventConnectionLost addr'' []) _) <- receive endpoint ; True <- return $ addr'' == theirAddr2 + ConnectionClosed cid1' <- receive endpoint ; True <- return $ cid1' == cid1 + + -- An attempt to send to the endpoint should now fail + Left (TransportError SendFailed _) <- send conn ["pong2"] + + putMVar serverDone () + + -- Client + forkTry $ do + Right transport <- newTransport + theirAddr <- readMVar serverAddr + + -- Set up endpoint with one outgoing but no incoming connections + Right endpoint1 <- newEndPoint transport + putMVar clientAddr1 (address endpoint1) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint1 theirAddr ReliableOrdered + + -- Set up an endpoint with one outgoing and out incoming connection + Right endpoint2 <- newEndPoint transport + putMVar clientAddr2 (address endpoint2) + + Right conn <- connect endpoint2 theirAddr ReliableOrdered + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' + + -- Now shut down the entire transport + closeTransport transport + + -- Both endpoints should report that they have been closed + EndPointClosed <- receive endpoint1 + EndPointClosed <- receive endpoint2 + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect on either endpoint + Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered + Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered + + -- And finally, so should an attempt to create a new endpoint + Left (TransportError NewEndPointFailed _) <- newEndPoint transport + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +testMany :: IO (Either String Transport) -> IO () +testMany newTransport = do + Right masterTransport <- newTransport + Right masterEndPoint <- newEndPoint masterTransport + + replicateM_ 20 $ do + Right transport <- newTransport + replicateM_ 2 $ do + Right endpoint <- newEndPoint transport + Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered + return () + +-- Transport tests +testTransport :: IO (Either String Transport) -> IO () +testTransport newTransport = do + Right transport <- newTransport + runTests + [ ("Many", testMany newTransport) + , ("PingPong", testPingPong transport numPings) + , ("EndPoints", testEndPoints transport numPings) + , ("Connections", testConnections transport numPings) + , ("CloseOneConnection", testCloseOneConnection transport numPings) + , ("CloseOneDirection", testCloseOneDirection transport numPings) + , ("CloseReopen", testCloseReopen transport numPings) + , ("ParallelConnects", testParallelConnects transport numPings) + , ("SendAfterClose", testSendAfterClose transport numPings) + , ("CloseTwice", testCloseTwice transport numPings) + , ("ConnectToSelf", testConnectToSelf transport numPings) + , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) + , ("CloseEndPoint", testCloseEndPoint transport numPings) + , ("CloseTransport", testCloseTransport newTransport) + ] + where + numPings = 10000 :: Int diff --git a/tests/Traced.hs b/tests/Traced.hs new file mode 100644 index 00000000..6f839003 --- /dev/null +++ b/tests/Traced.hs @@ -0,0 +1,191 @@ +-- | Add tracing to the IO monad (see examples). +-- +-- [Usage] +-- +-- > {-# LANGUAGE RebindableSyntax #-} +-- > import Prelude hiding (catch, (>>=), (>>), return, fail) +-- > import Traced +-- +-- [Example] +-- +-- > test1 :: IO Int +-- > test1 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > putStrLn "Hello world" +-- > Right y <- return (Left 2 :: Either Int Int) +-- > return (x + y) +-- +-- outputs +-- +-- > Hello world +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) +-- > Trace: +-- > 0 Left 2 +-- > 1 Left 1 +-- +-- [Guards] +-- +-- Use the following idiom instead of using 'Control.Monad.guard': +-- +-- > test2 :: IO Int +-- > test2 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > True <- return (x == 3) +-- > return x +-- +-- The advantage of this idiom is that it gives you line number information when the guard fails: +-- +-- > *Traced> test2 +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) +-- > Trace: +-- > 0 Left 1 +module Traced ( MonadS(..) + , return + , (>>=) + , (>>) + , fail + , ifThenElse + , Showable(..) + , Traceable(..) + , traceShow + ) where + +import Prelude hiding ((>>=), return, fail, catch, (>>)) +import qualified Prelude +import Control.Exception (catches, Handler(..), SomeException, throw, Exception(..), IOException) +import Control.Applicative ((<$>)) +import Data.Typeable (Typeable) +import Data.Maybe (catMaybes) +import Data.ByteString (ByteString) +import Data.Int (Int32) +import Control.Concurrent.MVar (MVar) + +-------------------------------------------------------------------------------- +-- MonadS class -- +-------------------------------------------------------------------------------- + +-- | Like 'Monad' but bind is only defined for 'Trace'able instances +class MonadS m where + returnS :: a -> m a + bindS :: Traceable a => m a -> (a -> m b) -> m b + failS :: String -> m a + seqS :: m a -> m b -> m b + +-- | Redefinition of 'Prelude.>>=' +(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b +(>>=) = bindS + +-- | Redefinition of 'Prelude.>>' +(>>) :: MonadS m => m a -> m b -> m b +(>>) = seqS + +-- | Redefinition of 'Prelude.return' +return :: MonadS m => a -> m a +return = returnS + +-- | Redefinition of 'Prelude.fail' +fail :: MonadS m => String -> m a +fail = failS + +-------------------------------------------------------------------------------- +-- Trace typeclass (for adding elements to a trace -- +-------------------------------------------------------------------------------- + +data Showable = forall a. Show a => Showable a + +instance Show Showable where + show (Showable x) = show x + +mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable +mapShowable f (Showable x) = f x + +traceShow :: Show a => a -> Maybe Showable +traceShow = Just . Showable + +class Traceable a where + trace :: a -> Maybe Showable + +instance (Traceable a, Traceable b) => Traceable (Either a b) where + trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x + trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y + +instance (Traceable a, Traceable b) => Traceable (a, b) where + trace (x, y) = case (trace x, trace y) of + (Nothing, Nothing) -> Nothing + (Just t1, Nothing) -> traceShow t1 + (Nothing, Just t2) -> traceShow t2 + (Just t1, Just t2) -> traceShow (t1, t2) + +instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where + trace (x, y, z) = case (trace x, trace y, trace z) of + (Nothing, Nothing, Nothing) -> Nothing + (Just t1, Nothing, Nothing) -> traceShow t1 + (Nothing, Just t2, Nothing) -> traceShow t2 + (Just t1, Just t2, Nothing) -> traceShow (t1, t2) + (Nothing, Nothing, Just t3) -> traceShow t3 + (Just t1, Nothing, Just t3) -> traceShow (t1, t3) + (Nothing, Just t2, Just t3) -> traceShow (t2, t3) + (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) + +instance Traceable a => Traceable (Maybe a) where + trace Nothing = traceShow (Nothing :: Maybe ()) + trace (Just x) = mapShowable (Showable . Just) <$> trace x + +instance Traceable a => Traceable [a] where + trace = traceShow . catMaybes . map trace + +instance Traceable () where + trace = const Nothing + +instance Traceable Int where + trace = traceShow + +instance Traceable Int32 where + trace = traceShow + +instance Traceable Bool where + trace = const Nothing + +instance Traceable ByteString where + trace = traceShow + +instance Traceable (MVar a) where + trace = const Nothing + +instance Traceable [Char] where + trace = traceShow + +instance Traceable IOException where + trace = traceShow + +-------------------------------------------------------------------------------- +-- IO instance for MonadS -- +-------------------------------------------------------------------------------- + +data TracedException = TracedException [String] SomeException + deriving Typeable + +instance Exception TracedException + +-- | Add tracing to 'IO' (see examples) +instance MonadS IO where + returnS = Prelude.return + bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) + failS = Prelude.fail + seqS = (Prelude.>>) + +instance Show TracedException where + show (TracedException ts ex) = + show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (reverse ts))) + +traceHandlers :: Traceable a => a -> [Handler b] +traceHandlers a = case trace a of + Nothing -> [ Handler $ \ex -> throw (ex :: SomeException) ] + Just t -> [ Handler $ \(TracedException ts ex) -> throw $ TracedException (show t : ts) ex + , Handler $ \ex -> throw $ TracedException [show t] (ex :: SomeException) + ] + +-- | Definition of 'ifThenElse' for use with RebindableSyntax +ifThenElse :: Bool -> a -> a -> a +ifThenElse True x _ = x +ifThenElse False _ y = y diff --git a/tests/chat/ChatClient.hs b/tests/chat/ChatClient.hs new file mode 100644 index 00000000..527af993 --- /dev/null +++ b/tests/chat/ChatClient.hs @@ -0,0 +1,107 @@ +import System.Environment (getArgs) +import Network.Transport +import Network.Transport.TCP (createTransport) +import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar, newMVar, readMVar, modifyMVar_, modifyMVar) +import Control.Concurrent (forkIO) +import Control.Monad (forever, forM, unless, when) +import qualified Data.ByteString as BS (concat, null) +import qualified Data.ByteString.Char8 as BSC (pack, unpack, getLine) +import Data.Map (Map) +import qualified Data.Map as Map (fromList, elems, insert, member, empty, size, delete, (!)) + +chatClient :: MVar () -> EndPoint -> EndPointAddress -> IO () +chatClient done endpoint serverAddr = do + connect endpoint serverAddr ReliableOrdered + cOut <- getPeers >>= connectToPeers + cIn <- newMVar Map.empty + + -- Listen for incoming messages + forkIO . forever $ do + event <- receive endpoint + case event of + Received _ msg -> + putStrLn . BSC.unpack . BS.concat $ msg + ConnectionOpened cid _ addr -> do + modifyMVar_ cIn $ return . Map.insert cid addr + didAdd <- modifyMVar cOut $ \conns -> + if not (Map.member addr conns) + then do + Right conn <- connect endpoint addr ReliableOrdered + return (Map.insert addr conn conns, True) + else + return (conns, False) + when didAdd $ showNumPeers cOut + ConnectionClosed cid -> do + addr <- modifyMVar cIn $ \conns -> + return (Map.delete cid conns, conns Map.! cid) + modifyMVar_ cOut $ \conns -> do + close (conns Map.! addr) + return (Map.delete addr conns) + showNumPeers cOut + + + +{- + chatState <- newMVar (Map.fromList peerConns) + + -- Thread to listen to incoming messages + forkIO . forever $ do + event <- receive endpoint + case event of + ConnectionOpened _ _ (EndPointAddress addr) -> do + modifyMVar_ chatState $ \peers -> + if not (Map.member addr peers) + then do + Right conn <- connect endpoint (EndPointAddress addr) ReliableOrdered + return (Map.insert addr conn peers) + else + return peers + Received _ msg -> + putStrLn . BSC.unpack . BS.concat $ msg + ConnectionClosed _ -> + return () + +-} + -- Thread to interact with the user + showNumPeers cOut + let go = do + msg <- BSC.getLine + unless (BS.null msg) $ do + readMVar cOut >>= \conns -> forM (Map.elems conns) $ \conn -> send conn [msg] + go + go + putMVar done () + + where + getPeers :: IO [EndPointAddress] + getPeers = do + ConnectionOpened _ _ _ <- receive endpoint + Received _ msg <- receive endpoint + ConnectionClosed _ <- receive endpoint + return . map EndPointAddress . read . BSC.unpack . BS.concat $ msg + + connectToPeers :: [EndPointAddress] -> IO (MVar (Map EndPointAddress Connection)) + connectToPeers addrs = do + conns <- forM addrs $ \addr -> do + Right conn <- connect endpoint addr ReliableOrdered + return (addr, conn) + newMVar (Map.fromList conns) + + showNumPeers :: MVar (Map EndPointAddress Connection) -> IO () + showNumPeers cOut = + readMVar cOut >>= \conns -> putStrLn $ "# " ++ show (Map.size conns) ++ " peers" + + + + +main :: IO () +main = do + host:port:server:_ <- getArgs + Right transport <- createTransport host port + Right endpoint <- newEndPoint transport + clientDone <- newEmptyMVar + + forkIO $ chatClient clientDone endpoint (EndPointAddress . BSC.pack $ server) + + takeMVar clientDone + diff --git a/tests/chat/ChatServer.hs b/tests/chat/ChatServer.hs new file mode 100644 index 00000000..6c63d8b8 --- /dev/null +++ b/tests/chat/ChatServer.hs @@ -0,0 +1,28 @@ +import System.Environment (getArgs) +import Network.Transport +import Network.Transport.TCP (createTransport) +import Control.Monad.State (evalStateT, modify, get) +import Control.Monad (forever) +import Control.Monad.IO.Class (liftIO) +import qualified Data.IntMap as IntMap (empty, insert, delete, elems) +import qualified Data.ByteString.Char8 as BSC (pack) + +main :: IO () +main = do + host:port:_ <- getArgs + Right transport <- createTransport host port + Right endpoint <- newEndPoint transport + + putStrLn $ "Chat server ready at " ++ (show . endPointAddressToByteString . address $ endpoint) + + flip evalStateT IntMap.empty . forever $ do + event <- liftIO $ receive endpoint + case event of + ConnectionOpened cid _ addr -> do + get >>= \clients -> liftIO $ do + Right conn <- connect endpoint addr ReliableOrdered + send conn [BSC.pack . show . IntMap.elems $ clients] + close conn + modify $ IntMap.insert cid (endPointAddressToByteString addr) + ConnectionClosed cid -> + modify $ IntMap.delete cid diff --git a/tests/sumeuler/SumEulerMaster.hs b/tests/sumeuler/SumEulerMaster.hs new file mode 100644 index 00000000..8159a47b --- /dev/null +++ b/tests/sumeuler/SumEulerMaster.hs @@ -0,0 +1,44 @@ +import System.Environment (getArgs) +import Network.Transport +import Network.Transport.TCP (createTransport) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar) +import Control.Monad (forM, forM_, replicateM_) +import qualified Data.ByteString as BS (concat) +import qualified Data.ByteString.Char8 as BSC (pack, unpack) +import Control.Monad.Trans.Writer (execWriterT, tell) +import Control.Monad.IO.Class (liftIO) + +master :: MVar () -> EndPoint -> [String] -> IO () +master done endpoint workers = do + conns <- forM workers $ \worker -> do + Right conn <- connect endpoint (EndPointAddress $ BSC.pack worker) ReliableOrdered + return conn + -- Send out requests + forM_ conns $ \conn -> do + send conn [BSC.pack $ show 5300] + close conn + -- Print all replies + replies <- execWriterT $ replicateM_ (length workers * 3) $ do + event <- liftIO $ receive endpoint + case event of + Received _ msg -> + tell [read . BSC.unpack . BS.concat $ msg] + _ -> + return () + putStrLn $ "Replies: " ++ show (replies :: [Int]) + putMVar done () + +main :: IO () +main = do + host:port:workers <- getArgs + Right transport <- createTransport host port + Right endpoint <- newEndPoint transport + masterDone <- newEmptyMVar + + putStrLn $ "Master using workers " ++ show workers + + forkIO $ master masterDone endpoint workers + + takeMVar masterDone + diff --git a/tests/sumeuler/SumEulerWorker.hs b/tests/sumeuler/SumEulerWorker.hs new file mode 100644 index 00000000..071eb0ab --- /dev/null +++ b/tests/sumeuler/SumEulerWorker.hs @@ -0,0 +1,52 @@ +import System.Environment (getArgs) +import Network.Transport +import Network.Transport.TCP (createTransport) +import qualified Data.ByteString.Char8 as BSC (putStrLn, pack, unpack) +import qualified Data.ByteString as BS (concat) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar) +import System.IO (hFlush, stdout, stderr, hPutStrLn) + +mkList :: Int -> [Int] +mkList n = [1 .. n - 1] + +relPrime :: Int -> Int -> Bool +relPrime x y = gcd x y == 1 + +euler :: Int -> Int +euler n = length (filter (relPrime n) (mkList n)) + +sumEuler :: Int -> Int +sumEuler = sum . (map euler) . mkList + +worker :: String -> MVar () -> EndPoint -> IO () +worker id done endpoint = do + ConnectionOpened _ _ theirAddr <- receive endpoint + Right replyChan <- connect endpoint theirAddr ReliableOrdered + go replyChan + where + go replyChan = do + event <- receive endpoint + case event of + ConnectionClosed _ -> do + close replyChan + putMVar done () + Received _ msg -> do + let i :: Int + i = read . BSC.unpack . BS.concat $ msg + send replyChan [BSC.pack . show $ sumEuler i] + go replyChan + +main :: IO () +main = do + (id:host:port:_) <- getArgs + Right transport <- createTransport host port + Right endpoint <- newEndPoint transport + workerDone <- newEmptyMVar + + BSC.putStrLn (endPointAddressToByteString (address endpoint)) + hFlush stdout + + forkIO $ worker id workerDone endpoint + + takeMVar workerDone diff --git a/tests/sumeuler/sumeuler.sh b/tests/sumeuler/sumeuler.sh new file mode 100755 index 00000000..d5790b0d --- /dev/null +++ b/tests/sumeuler/sumeuler.sh @@ -0,0 +1,20 @@ +#/bin/bash + +rm -f workers +killall -9 SumEulerWorker + +ghc -O2 -i../src -XScopedTypeVariables SumEulerWorker +ghc -O2 -i../src -XScopedTypeVariables SumEulerMaster + +./SumEulerWorker 1 127.0.0.1 8080 >> workers & +./SumEulerWorker 1 127.0.0.1 8081 >> workers & +./SumEulerWorker 1 127.0.0.1 8082 >> workers & +./SumEulerWorker 1 127.0.0.1 8083 >> workers & +./SumEulerWorker 1 127.0.0.1 8084 >> workers & +./SumEulerWorker 1 127.0.0.1 8085 >> workers & +./SumEulerWorker 1 127.0.0.1 8086 >> workers & +./SumEulerWorker 1 127.0.0.1 8087 >> workers & + +echo "Waiting for all workers to be ready" +sleep 1 +cat workers | xargs ./SumEulerMaster 127.0.0.1 8090 From 002bf628ebe80147d10fd10595060e233a2a5db9 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 16 May 2012 14:43:07 +0100 Subject: [PATCH 0056/2357] Fix bug during move from data-lens->data-accessor --- src/Network/Transport/TCP.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 298c1283..2f3648e8 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -399,7 +399,7 @@ apiClose theirEndPoint connId connAlive = void . tryIO $ do then do writeIORef connAlive False sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] - return (RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ vst) + return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ vst) else return (RemoteEndPointValid vst) RemoteEndPointClosed -> do @@ -598,7 +598,7 @@ requestConnectionTo ourEndPoint theirAddress = go let failureHandler :: IOException -> IO b failureHandler err = do modifyMVar_ theirState $ \(RemoteEndPointValid ep) -> - return (RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ ep) + return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ ep) -- TODO: should we call closeIfUnused here? throw $ TransportError ConnectFailed (show err) From 03b1729b47e57d6caadcc51cd2f18efdc07518dd Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 16 May 2012 15:51:34 +0100 Subject: [PATCH 0057/2357] Remove potential race condition --- src/Network/Transport/TCP.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 2f3648e8..c14581d2 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -864,11 +864,11 @@ handleConnectionRequest transport sock = handle tryCloseSocket $ do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } + sendMany sock [encodeInt32 ConnectionRequestAccepted] -- TODO: this putMVar might block if the remote endpoint sends a -- connection request for a local endpoint that it is already -- connected to putMVar (remoteState theirEndPoint) (RemoteEndPointValid vst) - sendMany sock [encodeInt32 ConnectionRequestAccepted] return (Just theirEndPoint) -- If we left the scope of the exception handler with a return value of -- Nothing then the socket is already closed; otherwise, the socket has From 7acf705d40b2823a3b4a64941abaa746cdab904a Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 16 May 2012 16:57:16 +0100 Subject: [PATCH 0058/2357] Relax RELY conditions for send and close --- src/Network/Transport/TCP.hs | 24 ++++++++---- tests/TestAuxiliary.hs | 2 +- tests/TestTransport.hs | 72 +++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index c14581d2..57179503 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -384,7 +384,7 @@ apiConnect ourEndPoint theirAddress _ = try $ do -- | Close a connection -- --- RELY: Remote endpoint must be in 'RemoteEndPointValid' or 'RemoteEndPointClosed' +-- RELY: The endpoint must not be invalid -- GUARANTEE: If the connection is alive on entry then the remote endpoint will -- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the state -- of the remote endpoint will not be changed. @@ -405,14 +405,15 @@ apiClose theirEndPoint connId connAlive = void . tryIO $ do RemoteEndPointClosed -> do return st RemoteEndPointClosing _ _ -> - fail "apiClose RELY violation" + -- See discussion in apiSend why we must allow for this case + return st RemoteEndPointInvalid _ -> fail "apiClose RELY violation" closeIfUnused theirEndPoint -- | Send data across a connection -- --- RELY: The remote endpoint must be in state 'RemoteEndPointValid' or 'RemoteEndPointClosed' +-- RELY: The endpoint must not be in invalid state. -- GUARANTEE: The state of the remote endpoint will not be changed. apiSend :: RemoteEndPoint -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) apiSend theirEndPoint connId connAlive payload = do @@ -423,9 +424,16 @@ apiSend theirEndPoint connId connAlive payload = do then mapExceptionIO sendFailed $ sendOn vst (encodeInt32 connId : prependLength payload) else throw $ TransportError SendFailed "Connection closed" RemoteEndPointClosed -> do - return (Left $ TransportError SendFailed "Endpoint closed") + return (Left $ TransportError SendFailed "Connection closed") RemoteEndPointClosing _ _ -> - error "apiSend RELY violation" + -- The only way for the endpoint to be in closing state, while a + -- connection is still active, is for the application to call 'close' + -- on the connection triggering "garbage collection" of the TCP channel + -- (CloseSocket request to the remote endpoint) which hasn't completed + -- yet. Even if the remote endpoint comes back with "please don't + -- close", this still means that our *outgoing* connection has been + -- closed + return (Left $ TransportError SendFailed "Connection closed") RemoteEndPointInvalid _ -> error "apiSend RELY violation" where @@ -960,9 +968,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do _ -> error "handleIncomingMessages RELY violation" sendOn vst ( encodeInt32 ControlResponse - : encodeInt32 reqId - : prependLength [encodeInt32 newId] - ) + : encodeInt32 reqId + : prependLength [encodeInt32 newId] + ) -- We add the new connection ID to the list of open connections only once the -- endpoint has been notified of the new connection (sendOn may fail) return (RemoteEndPointValid vst) diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index c31de0e3..6c5f7307 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -32,7 +32,7 @@ runTest :: String -> IO () -> IO Bool runTest description test = do putStr $ "Running " ++ show description ++ ": " hFlush stdout - done <- try . timeout 10000000 $ test + done <- try . timeout 20000000 $ test case done of Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" Right Nothing -> failed $ "(timeout)" diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index efbd12f5..26f64537 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -374,7 +374,7 @@ testParallelConnects transport numPings = do -- | Test that sending on a closed connection gives an error testSendAfterClose :: Transport -> Int -> IO () -testSendAfterClose transport _ = do +testSendAfterClose transport numRepeats = do server <- spawn transport echoServer clientDone <- newEmptyMVar @@ -382,19 +382,22 @@ testSendAfterClose transport _ = do Right endpoint <- newEndPoint transport -- We request two lightweight connections - Right conn1 <- connect endpoint server ReliableOrdered - Right conn2 <- connect endpoint server ReliableOrdered - - -- Close the second, but leave the first open; then output on the second - -- connection (i.e., on a closed connection while there is still another - -- connection open) - close conn2 - Left (TransportError SendFailed _) <- send conn2 ["ping2"] + replicateM numRepeats $ do + Right conn1 <- connect endpoint server ReliableOrdered + Right conn2 <- connect endpoint server ReliableOrdered + + -- Close the second, but leave the first open; then output on the second + -- connection (i.e., on a closed connection while there is still another + -- connection open) + close conn2 + Left (TransportError SendFailed _) <- send conn2 ["ping2"] + + -- Now close the first connection, and output on it (i.e., output while + -- there are no lightweight connection at all anymore) + close conn1 + Left (TransportError SendFailed _) <- send conn2 ["ping2"] - -- Now close the first connection, and output on it (i.e., output while - -- there are no lightweight connection at all anymore) - close conn1 - Left (TransportError SendFailed _) <- send conn2 ["ping2"] + return () putMVar clientDone () @@ -402,32 +405,35 @@ testSendAfterClose transport _ = do -- | Test that closing the same connection twice has no effect testCloseTwice :: Transport -> Int -> IO () -testCloseTwice transport _ = do +testCloseTwice transport numRepeats = do server <- spawn transport echoServer clientDone <- newEmptyMVar forkTry $ do Right endpoint <- newEndPoint transport - -- We request two lightweight connections - Right conn1 <- connect endpoint server ReliableOrdered - Right conn2 <- connect endpoint server ReliableOrdered - - -- Close the second one twice - close conn2 - close conn2 - - -- Then send a message on the first and close that too - send conn1 ["ping"] - close conn1 - - -- Verify expected response from the echo server - ConnectionOpened cid1 _ _ <- receive endpoint - ConnectionOpened cid2 _ _ <- receive endpoint - ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 - Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 - ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 + replicateM numRepeats $ do + -- We request two lightweight connections + Right conn1 <- connect endpoint server ReliableOrdered + Right conn2 <- connect endpoint server ReliableOrdered + + -- Close the second one twice + close conn2 + close conn2 + + -- Then send a message on the first and close that twice too + send conn1 ["ping"] + close conn1 + -- Verify expected response from the echo server + ConnectionOpened cid1 _ _ <- receive endpoint + ConnectionOpened cid2 _ _ <- receive endpoint + ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 + Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 + ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 + + return () + putMVar clientDone () takeMVar clientDone @@ -722,7 +728,7 @@ testTransport newTransport = do , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) , ("CloseEndPoint", testCloseEndPoint transport numPings) - , ("CloseTransport", testCloseTransport newTransport) + , ("CloseTransport", testCloseTransport newTransport) ] where numPings = 10000 :: Int From 27d414d480ba4b14473bfc2c26b5becc1880beed Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 17 May 2012 08:53:38 +0100 Subject: [PATCH 0059/2357] Reduce number of repetitions in tests to avoid timeouts on slower machines --- tests/TestTransport.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 26f64537..0f73796c 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -723,8 +723,8 @@ testTransport newTransport = do , ("CloseOneDirection", testCloseOneDirection transport numPings) , ("CloseReopen", testCloseReopen transport numPings) , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport numPings) - , ("CloseTwice", testCloseTwice transport numPings) + , ("SendAfterClose", testSendAfterClose transport 100) + , ("CloseTwice", testCloseTwice transport 100) , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) , ("CloseEndPoint", testCloseEndPoint transport numPings) From 01f6b6388b5a81722b4c09b60739bab7766722a5 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 17 May 2012 11:50:07 +0100 Subject: [PATCH 0060/2357] Manual show instances for addresses --- src/Network/Transport.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index fd556f28..679c346e 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -106,11 +106,17 @@ data MulticastGroup = MulticastGroup { -- | EndPointAddress of an endpoint. newtype EndPointAddress = EndPointAddress { endPointAddressToByteString :: ByteString } - deriving (Show, Eq, Ord) + deriving (Eq, Ord) + +instance Show EndPointAddress where + show = show . endPointAddressToByteString -- | EndPointAddress of a multicast group. newtype MulticastAddress = MulticastAddress { multicastAddressToByteString :: ByteString } - deriving (Show, Eq, Ord) + deriving (Eq, Ord) + +instance Show MulticastAddress where + show = show . multicastAddressToByteString -------------------------------------------------------------------------------- -- Error codes -- From c3fd1f39abca4ac701c5bd4a824914c36338e707 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 17 May 2012 16:33:42 +0100 Subject: [PATCH 0061/2357] Add ConnectHints, reintroduce SendClosed --- network-transport.cabal | 8 +++---- src/Network/Transport.hs | 43 +++++++++++++++++++++++------------ src/Network/Transport/Chan.hs | 15 ++++++++---- src/Network/Transport/TCP.hs | 15 ++++++------ tests/TestTCP.hs | 18 +++++++-------- tests/TestTransport.hs | 15 ++++++++---- 6 files changed, 68 insertions(+), 46 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 6ec22aa4..9fb93841 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -23,7 +23,7 @@ Library Network.Transport.Chan, Network.Transport.TCP, Network.Transport.Util - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src @@ -38,7 +38,7 @@ Test-Suite TestTCP mtl, transformers, ansi-terminal - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -52,7 +52,7 @@ Test-Suite TestMulticastInMemory mtl, transformers, ansi-terminal - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -66,6 +66,6 @@ Test-Suite TestInMemory mtl, transformers, ansi-terminal - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 679c346e..68171ab4 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -9,6 +9,9 @@ module Network.Transport ( -- * Types , MulticastGroup(..) , EndPointAddress(..) , MulticastAddress(..) + -- * Hints + , ConnectHints + , defaultConnectHints -- * Error codes , TransportError(..) , NewEndPointErrorCode(..) @@ -43,7 +46,7 @@ data EndPoint = EndPoint { -- | EndPointAddress of the endpoint. , address :: EndPointAddress -- | Create a new lightweight connection. - , connect :: EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) + , connect :: EndPointAddress -> Reliability -> ConnectHints -> IO (Either (TransportError ConnectErrorCode) Connection) -- | Create a new multicast group. , newMulticastGroup :: IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) -- | Resolve an address to a multicast group. @@ -76,7 +79,7 @@ data Event = | ErrorEvent (TransportError EventErrorCode) deriving Show --- | Connection IDs enable receivers to distinguish one connection from another. +-- | Connection data ConnectHintsIDs enable receivers to distinguish one connection from another. type ConnectionId = Int -- | Reliability guarantees of a connection. @@ -118,10 +121,31 @@ newtype MulticastAddress = MulticastAddress { multicastAddressToByteString :: By instance Show MulticastAddress where show = show . multicastAddressToByteString +-------------------------------------------------------------------------------- +-- Hints -- +-- -- +-- Hints provide transport-generic "suggestions". For now, these are -- +-- placeholders only. -- +-------------------------------------------------------------------------------- + +-- Hints used by 'connect' +data ConnectHints + +-- Default hints for connecting +defaultConnectHints :: ConnectHints +defaultConnectHints = undefined + -------------------------------------------------------------------------------- -- Error codes -- +-- -- +-- Errors should be transport-implementation independent. The deciding factor -- +-- for distinguishing one kind of error from another should be: might -- +-- application code have to take a different action depending on the kind of -- +-- error? -- -------------------------------------------------------------------------------- +-- | Errors returned by Network.Transport API functions consist of an error +-- code and a human readable description of the problem data TransportError error = TransportError error String deriving (Show, Typeable) @@ -133,33 +157,26 @@ instance (Typeable err, Show err) => Exception (TransportError err) -- | Errors during the creation of an endpoint data NewEndPointErrorCode = -- | Not enough resources - -- (i.e., this could be a temporary local problem) NewEndPointInsufficientResources -- | Failed for some other reason - -- (i.e., there is probably no point trying again) | NewEndPointFailed deriving (Show, Typeable) -- | Connection failure data ConnectErrorCode = -- | Could not resolve the address - -- (i.e., this could be a temporary remote problem) ConnectNotFound -- | Insufficient resources (for instance, no more sockets available) - -- (i.e., this could be a temporary local problem) | ConnectInsufficientResources -- | Failed for other reasons (including syntax error) - -- (i.e., there is probably no point trying again). | ConnectFailed deriving (Show, Typeable) -- | Failure during the creation of a new multicast group data NewMulticastGroupErrorCode = -- | Insufficient resources - -- (i.e., this could be a temporary problem) NewMulticastGroupInsufficientResources -- | Failed for some other reason - -- (i.e., there is probably no point trying again) | NewMulticastGroupFailed -- | Not all transport implementations support multicast | NewMulticastGroupUnsupported @@ -168,10 +185,8 @@ data NewMulticastGroupErrorCode = -- | Failure during the resolution of a multicast group data ResolveMulticastGroupErrorCode = -- | Multicast group not found - -- (i.e., this could be a temporary problem) ResolveMulticastGroupNotFound -- | Failed for some other reason (including syntax error) - -- (i.e., there is probably no point trying again) | ResolveMulticastGroupFailed -- | Not all transport implementations support multicast | ResolveMulticastGroupUnsupported @@ -179,11 +194,9 @@ data ResolveMulticastGroupErrorCode = -- | Failure during sending a message data SendErrorCode = - -- | Could not send this message - -- (but another attempt might succeed) - SendUnreachable + -- | Connection was closed + SendClosed -- | Send failed for some other reason - -- (and retrying probably won't help) | SendFailed deriving (Show, Typeable) diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs index 6d9ec331..217f3c52 100644 --- a/src/Network/Transport/Chan.hs +++ b/src/Network/Transport/Chan.hs @@ -53,8 +53,13 @@ apiNewEndPoint state = do } -- | Create a new connection -apiConnect :: EndPointAddress -> MVar TransportState -> EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect myAddress state theirAddress _ = do +apiConnect :: EndPointAddress + -> MVar TransportState + -> EndPointAddress + -> Reliability + -> ConnectHints + -> IO (Either (TransportError ConnectErrorCode) Connection) +apiConnect myAddress state theirAddress _reliability _hints = do (chan, conn) <- modifyMVar state $ \st -> do let chan = st ^. channelAt theirAddress let conn = st ^. nextConnectionIdAt theirAddress @@ -105,10 +110,10 @@ createMulticastGroup state ourAddress groupAddress group = cs <- (^. channels) <$> readMVar state es <- readMVar group forM_ (Set.elems es) $ \ep -> do - let ch = (cs ^. at ep "Invalid endpoint") + let ch = cs ^. at ep "Invalid endpoint" writeChan ch (ReceivedMulticast groupAddress payload) - , multicastSubscribe = modifyMVar_ group $ return . (Set.insert ourAddress) - , multicastUnsubscribe = modifyMVar_ group $ return . (Set.delete ourAddress) + , multicastSubscribe = modifyMVar_ group $ return . Set.insert ourAddress + , multicastUnsubscribe = modifyMVar_ group $ return . Set.delete ourAddress , multicastClose = return () } diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 57179503..b6f09406 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -370,10 +370,11 @@ apiNewEndPoint transport = try $ do apiConnect :: LocalEndPoint -- ^ Local end point -> EndPointAddress -- ^ Remote address -> Reliability -- ^ Reliability (ignored) + -> ConnectHints -- ^ Hints (ignored for now) -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect ourEndPoint theirAddress _ | localAddress ourEndPoint == theirAddress = +apiConnect ourEndPoint theirAddress _reliability _hints | localAddress ourEndPoint == theirAddress = connectToSelf ourEndPoint -apiConnect ourEndPoint theirAddress _ = try $ do +apiConnect ourEndPoint theirAddress _reliability _hints = try $ do (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress -- connAlive can be an IORef rather than an MVar because it is protected by -- the remoteState MVar. We don't need the overhead of locking twice. @@ -422,9 +423,9 @@ apiSend theirEndPoint connId connAlive payload = do alive <- readIORef connAlive try $ if alive then mapExceptionIO sendFailed $ sendOn vst (encodeInt32 connId : prependLength payload) - else throw $ TransportError SendFailed "Connection closed" + else throw $ TransportError SendClosed "Connection closed" RemoteEndPointClosed -> do - return (Left $ TransportError SendFailed "Connection closed") + return (Left $ TransportError SendClosed "Connection lost") RemoteEndPointClosing _ _ -> -- The only way for the endpoint to be in closing state, while a -- connection is still active, is for the application to call 'close' @@ -433,7 +434,7 @@ apiSend theirEndPoint connId connAlive payload = do -- yet. Even if the remote endpoint comes back with "please don't -- close", this still means that our *outgoing* connection has been -- closed - return (Left $ TransportError SendFailed "Connection closed") + return (Left $ TransportError SendClosed "Connection lost") RemoteEndPointInvalid _ -> error "apiSend RELY violation" where @@ -706,9 +707,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do where createSocket :: N.AddrInfo -> IO N.Socket createSocket addr = mapExceptionIO insufficientResources $ do - sock <- N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - -- putStrLn $ "Created client socket " ++ show sock - return sock + N.socket (N.addrFamily addr) N.Stream N.defaultProtocol invalidAddress, insufficientResources, failed :: IOException -> TransportError ConnectErrorCode invalidAddress = TransportError ConnectNotFound . show diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index a9ed76d1..08a01582 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -77,7 +77,7 @@ testEarlyDisconnect nextPort = do -- establish a connection to them. This should re-establish the broken -- TCP connection. tlog "Trying to connect to client" - Right conn <- connect endpoint theirAddr ReliableOrdered + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- TEST 3: To test the connection, we do a simple ping test; as before, -- however, the remote client won't close the connection nicely but just @@ -97,7 +97,7 @@ testEarlyDisconnect nextPort = do return () -- TEST 4: A subsequent send on an already-open connection will now break - Left (TransportError SendFailed _) <- send conn ["ping2"] + Left (TransportError SendClosed _) <- send conn ["ping2"] -- *Pfew* putMVar serverDone () @@ -183,7 +183,7 @@ testEarlyCloseSocket nextPort = do -- establish a connection to them. This should re-establish the broken -- TCP connection. tlog "Trying to connect to client" - Right conn <- connect endpoint theirAddr ReliableOrdered + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- TEST 3: To test the connection, we do a simple ping test; as before, -- however, the remote client won't close the connection nicely but just @@ -208,7 +208,7 @@ testEarlyCloseSocket nextPort = do return () -- TEST 4: A subsequent send on an already-open connection will now break - Left (TransportError SendFailed _) <- send conn ["ping2"] + Left (TransportError SendClosed _) <- send conn ["ping2"] -- *Pfew* putMVar serverDone () @@ -275,19 +275,19 @@ testInvalidConnect nextPort = do -- Syntax error in the endpoint address Left (TransportError ConnectFailed _) <- - connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered + connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered defaultConnectHints -- Syntax connect, but invalid hostname (TCP address lookup failure) Left (TransportError ConnectNotFound _) <- - connect endpoint (encodeEndPointAddress "invalidHost" "port" 0) ReliableOrdered + connect endpoint (encodeEndPointAddress "invalidHost" "port" 0) ReliableOrdered defaultConnectHints -- TCP address correct, but nobody home at that address Left (TransportError ConnectNotFound _) <- - connect endpoint (encodeEndPointAddress "127.0.0.1" "9000" 0) ReliableOrdered + connect endpoint (encodeEndPointAddress "127.0.0.1" "9000" 0) ReliableOrdered defaultConnectHints -- Valid TCP address but invalid endpoint number Left (TransportError ConnectNotFound _) <- - connect endpoint (encodeEndPointAddress "127.0.0.1" port 1) ReliableOrdered + connect endpoint (encodeEndPointAddress "127.0.0.1" port 1) ReliableOrdered defaultConnectHints return () @@ -404,7 +404,7 @@ testBlockAfterCloseSocket nextPort = do -- At this point the server will have sent a CloseSocket request to the -- client, and must block until the client responds tlog "Server waiting to connect to the client.." - Right _ <- readMVar clientAddr >>= \addr -> connect endpoint addr ReliableOrdered + Right _ <- readMVar clientAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints tlog "Server waiting.." diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 0f73796c..b18eed72 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -6,7 +6,8 @@ import TestAuxiliary (forkTry, runTests) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) import Control.Monad (replicateM, replicateM_, when, guard, forM_) import Control.Monad.Error () -import Network.Transport +import Network.Transport hiding (connect) +import qualified Network.Transport as NT import Network.Transport.Internal (tlog) import Network.Transport.Util (spawn) import Data.ByteString (ByteString) @@ -16,6 +17,10 @@ import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust import Data.String (fromString) import Traced +-- | We overload connect to always pass the default hints +connect :: EndPoint -> EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) +connect ep addr rel = NT.connect ep addr rel defaultConnectHints + -- | Server that echoes messages straight back to the origin endpoint. echoServer :: EndPoint -> IO () echoServer endpoint = do @@ -390,12 +395,12 @@ testSendAfterClose transport numRepeats = do -- connection (i.e., on a closed connection while there is still another -- connection open) close conn2 - Left (TransportError SendFailed _) <- send conn2 ["ping2"] + Left (TransportError SendClosed _) <- send conn2 ["ping2"] -- Now close the first connection, and output on it (i.e., output while -- there are no lightweight connection at all anymore) close conn1 - Left (TransportError SendFailed _) <- send conn2 ["ping2"] + Left (TransportError SendClosed _) <- send conn2 ["ping2"] return () @@ -558,7 +563,7 @@ testCloseEndPoint transport _ = do ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' ErrorEvent (TransportError (EventConnectionLost addr' []) _) <- receive endpoint ; True <- return $ addr' == theirAddr - Left (TransportError SendFailed _) <- send conn ["pong2"] + Left (TransportError SendClosed _) <- send conn ["pong2"] return () @@ -596,7 +601,7 @@ testCloseEndPoint transport _ = do EndPointClosed <- receive endpoint -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] + Left (TransportError SendClosed _) <- send conn ["ping2"] -- An attempt to close the already closed connection should just return () <- close conn From baca10b716dcfd0725ef10aa575b3889f8e17fd2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 17 May 2012 17:13:27 +0100 Subject: [PATCH 0062/2357] Update tests to use SendClosed --- tests/TestTransport.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index b18eed72..3932d71d 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -652,7 +652,7 @@ testCloseTransport newTransport = do ConnectionClosed cid1' <- receive endpoint ; True <- return $ cid1' == cid1 -- An attempt to send to the endpoint should now fail - Left (TransportError SendFailed _) <- send conn ["pong2"] + Left (TransportError SendClosed _) <- send conn ["pong2"] putMVar serverDone () @@ -687,7 +687,7 @@ testCloseTransport newTransport = do EndPointClosed <- receive endpoint2 -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] + Left (TransportError SendClosed _) <- send conn ["ping2"] -- An attempt to close the already closed connection should just return () <- close conn From 3954a69b4e6f867b47c059698ebe94fa42657d24 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 18 May 2012 11:51:27 +0100 Subject: [PATCH 0063/2357] Fix testUnnecessaryConnect --- src/Network/Transport/TCP.hs | 7 ++++--- tests/TestAuxiliary.hs | 2 +- tests/TestTCP.hs | 29 +++++++++++++++++++++++++++-- tests/TestTransport.hs | 15 +-------------- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index b6f09406..e5d44a7d 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -872,9 +872,10 @@ handleConnectionRequest transport sock = handle tryCloseSocket $ do , sendOn = sendMany sock } sendMany sock [encodeInt32 ConnectionRequestAccepted] - -- TODO: this putMVar might block if the remote endpoint sends a - -- connection request for a local endpoint that it is already - -- connected to + -- If the remote endpoint (due to a bug) attempts to connect the + -- same local endpoint twice, the sceond attempt wil have been + -- rejected with ConnectionRequestCrossed and so will never get to + -- this point putMVar (remoteState theirEndPoint) (RemoteEndPointValid vst) return (Just theirEndPoint) -- If we left the scope of the exception handler with a return value of diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index 6c5f7307..e090a810 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -32,7 +32,7 @@ runTest :: String -> IO () -> IO Bool runTest description test = do putStr $ "Running " ++ show description ++ ": " hFlush stdout - done <- try . timeout 20000000 $ test + done <- try . timeout 20000000 $ test -- 20 seconds case done of Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" Right Nothing -> failed $ "(timeout)" diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 08a01582..3daf4eb1 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -10,7 +10,7 @@ import Network.Transport.TCP (createTransport, encodeEndPointAddress) import Data.Int (Int32) import Control.Concurrent (threadDelay) import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, readMVar, isEmptyMVar) -import Control.Monad (replicateM, guard, forM_) +import Control.Monad (replicateM, guard, forM_, replicateM_) import Control.Applicative ((<$>)) import Control.Exception (throw) import Network.Transport.TCP ( ControlHeader(..) @@ -27,6 +27,8 @@ import qualified Network.Socket as N ( sClose import Network.Socket.ByteString (sendMany) import Data.String (fromString) import Traced +import GHC.IO.Exception (ioe_errno) +import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) instance Traceable ControlHeader where trace = traceShow @@ -465,11 +467,33 @@ testUnnecessaryConnect nextPort = do forkTry $ do let ourAddress = EndPointAddress "ourAddress" Right (_, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress - Right (_, ConnectionRequestEndPointInvalid) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (_, ConnectionRequestCrossed) <- readMVar serverAddr >>= socketToEndPoint ourAddress putMVar clientDone () takeMVar clientDone +-- | Test that we can create "many" transport instances +testMany :: IO N.ServiceName -> IO () +testMany nextPort = do + Right masterTransport <- nextPort >>= createTransport "127.0.0.1" + Right masterEndPoint <- newEndPoint masterTransport + + replicateM_ 20 $ do + mTransport <- nextPort >>= createTransport "127.0.0.1" + threadDelay 2000000 + case mTransport of + Left ex -> do + putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex) + case (ioe_errno ex) of + Just no | Errno no == eADDRNOTAVAIL -> putStrLn "(ADDRNOTAVAIL)" + _ -> return () + throw ex + Right transport -> + replicateM_ 3 $ do + Right endpoint <- newEndPoint transport + Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered defaultConnectHints + return () + main :: IO () main = do portMVar <- newEmptyMVar @@ -483,5 +507,6 @@ main = do , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort) , ("InvalidAddress", testInvalidAddress nextPort) , ("InvalidConnect", testInvalidConnect nextPort) + , ("TestMany", testMany nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 3932d71d..38c9fea5 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -703,25 +703,12 @@ testCloseTransport newTransport = do mapM_ takeMVar [serverDone, clientDone] -testMany :: IO (Either String Transport) -> IO () -testMany newTransport = do - Right masterTransport <- newTransport - Right masterEndPoint <- newEndPoint masterTransport - - replicateM_ 20 $ do - Right transport <- newTransport - replicateM_ 2 $ do - Right endpoint <- newEndPoint transport - Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered - return () - -- Transport tests testTransport :: IO (Either String Transport) -> IO () testTransport newTransport = do Right transport <- newTransport runTests - [ ("Many", testMany newTransport) - , ("PingPong", testPingPong transport numPings) + [ ("PingPong", testPingPong transport numPings) , ("EndPoints", testEndPoints transport numPings) , ("Connections", testConnections transport numPings) , ("CloseOneConnection", testCloseOneConnection transport numPings) From e47a8228d3b929ec8b16bbb75bf9475c57661e18 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 18 May 2012 12:13:02 +0100 Subject: [PATCH 0064/2357] Remove delay from test --- tests/TestTCP.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 3daf4eb1..762c5a78 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -480,7 +480,6 @@ testMany nextPort = do replicateM_ 20 $ do mTransport <- nextPort >>= createTransport "127.0.0.1" - threadDelay 2000000 case mTransport of Left ex -> do putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex) From 128a6476362c1eac9b850670688578ac67d99499 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 18 May 2012 13:53:15 +0100 Subject: [PATCH 0065/2357] Distinguish between closeTransport and failure Also make forkServer in Internal/TCP.hs more robust --- src/Network/Transport/Internal/TCP.hs | 28 +++++----- src/Network/Transport/TCP.hs | 76 ++++++++++++++++++--------- tests/TestTCP.hs | 21 ++++++-- 3 files changed, 86 insertions(+), 39 deletions(-) diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 59f65d29..68a9c260 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -25,9 +25,9 @@ import qualified Network.Socket as N ( HostName , sClose ) import qualified Network.Socket.ByteString as NBS (recv) -import Control.Concurrent (forkIO, ThreadId) +import Control.Concurrent (ThreadId, forkIOWithUnmask) import Control.Monad (liftM, forever) -import Control.Exception (SomeException, handle, bracketOnError, throw) +import Control.Exception (SomeException, catch, bracketOnError, throw, mask_) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length, concat, null) import Data.Int (Int32) @@ -60,16 +60,20 @@ forkServer host port backlog terminationHandler requestHandler = do -- return an empty list (but will throw an exception instead) and will return -- the "best" address first, whatever that means addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just host) (Just port) - sock <- N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - -- putStrLn $ "Created server socket " ++ show sock ++ " for address " ++ host ++ ":" ++ port - N.setSocketOption sock N.ReuseAddr 1 - N.bindSocket sock (N.addrAddress addr) - N.listen sock backlog - -- TODO: when is this socket closed? - forkIO . handle terminationHandler . forever $ - bracketOnError (N.accept sock) - (N.sClose . fst) - (requestHandler . fst) + bracketOnError (N.socket (N.addrFamily addr) N.Stream N.defaultProtocol) + N.sClose $ \sock -> do + N.setSocketOption sock N.ReuseAddr 1 + N.bindSocket sock (N.addrAddress addr) + N.listen sock backlog + mask_ $ forkIOWithUnmask $ \unmask -> + catch (unmask (forever $ acceptRequest sock)) $ \ex -> do + N.sClose sock + terminationHandler ex + where + acceptRequest :: N.Socket -> IO () + acceptRequest sock = bracketOnError (N.accept sock) + (N.sClose . fst) + (requestHandler . fst) -- | Read a length and then a payload of that length recvWithLength :: N.Socket -> IO [ByteString] diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index e5d44a7d..6ea38dae 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -15,8 +15,9 @@ -- * Output exception on channel after endpoint is closed module Network.Transport.TCP ( -- * Main API createTransport - , -- * TCP specific functionality (exported mostly for testing purposes_ - EndPointId + -- * Internals (exposed for unit tests) + , createTransportExposeInternals + , EndPointId , encodeEndPointAddress , decodeEndPointAddress , ControlHeader(..) @@ -319,6 +320,16 @@ data ConnectionRequestResponse = -- TODOs: deal with hints createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) createTransport host port = do + result <- createTransportExposeInternals host port + case result of + Left err -> + return $ Left err + Right (transport, _) -> + return $ Right transport + +-- | You should probably not use this function (used for unit testing only) +createTransportExposeInternals :: N.HostName -> N.ServiceName -> IO (Either IOException (Transport, ThreadId)) +createTransportExposeInternals host port = do state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty } let transport = TCPTransport { transportState = state , transportHost = host @@ -329,27 +340,45 @@ createTransport host port = do -- http://tangentsoft.net/wskfaq/advanced.html#backlog -- http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/023/2333/2333s2.html transportThread <- forkServer host port N.sOMAXCONN (terminationHandler transport) (handleConnectionRequest transport) - return Transport { newEndPoint = apiNewEndPoint transport - , closeTransport = killThread transportThread -- This will invoke the termination handler - } + return ( Transport { newEndPoint = apiNewEndPoint transport + , closeTransport = apiCloseTransport transport (Just transportThread) [EndPointClosed] + } + , transportThread + ) where terminationHandler :: TCPTransport -> SomeException -> IO () - terminationHandler transport _ = do - -- TODO: we currently don't make a distinction between endpoint failure and manual closure - mTSt <- modifyMVar (transportState transport) $ \st -> case st of - TransportValid vst -> return (TransportClosed, Just vst) - TransportClosed -> return (TransportClosed, Nothing) - case mTSt of - Nothing -> - -- Transport already closed - return () - Just tSt -> - mapM_ (apiCloseEndPoint transport) (Map.elems $ tSt ^. localEndPoints) + terminationHandler transport ex = do + let ev = ErrorEvent (TransportError EventTransportFailed (show ex)) + apiCloseTransport transport Nothing [ev] -------------------------------------------------------------------------------- -- API functions -- -------------------------------------------------------------------------------- +-- | Close the transport +apiCloseTransport :: TCPTransport -> Maybe ThreadId -> [Event] -> IO () +apiCloseTransport transport mTransportThread evs = do + mTSt <- modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> return (TransportClosed, Just vst) + TransportClosed -> return (TransportClosed, Nothing) + case mTSt of + Nothing -> + -- Transport already closed + return () + Just tSt -> do + mapM_ (apiCloseEndPoint transport evs) (Map.elems $ tSt ^. localEndPoints) + case mTransportThread of + Just transportThread -> + -- This will invoke the termination handler, which in turn will call + -- apiCloseTransport again, but at that point the transport is + -- already in closed state so at that point we return immediately + killThread transportThread + Nothing -> + -- This will happen is apiCloseTransport was invoked *because* the + -- transport thread died (in which case we don't need to kill it) + return () + + -- | Create a new endpoint apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) apiNewEndPoint transport = try $ do @@ -358,7 +387,7 @@ apiNewEndPoint transport = try $ do { receive = readChan (localChannel ourEndPoint) , address = localAddress ourEndPoint , connect = apiConnect ourEndPoint - , closeEndPoint = apiCloseEndPoint transport ourEndPoint + , closeEndPoint = apiCloseEndPoint transport [EndPointClosed] ourEndPoint , newMulticastGroup = return . Left $ newMulticastGroupError , resolveMulticastGroup = return . Left . const resolveMulticastGroupError } @@ -442,8 +471,11 @@ apiSend theirEndPoint connId connAlive payload = do sendFailed = TransportError SendFailed . show -- | Force-close the endpoint -apiCloseEndPoint :: TCPTransport -> LocalEndPoint -> IO () -apiCloseEndPoint transport ourEndPoint = do +apiCloseEndPoint :: TCPTransport -- ^ Transport + -> [Event] -- ^ Events to output on the endpoint to indicate closure + -> LocalEndPoint -- ^ Local endpoint + -> IO () +apiCloseEndPoint transport evs ourEndPoint = do -- Remove the reference from the transport state removeLocalEndPoint transport ourEndPoint -- Close the local endpoint @@ -460,11 +492,7 @@ apiCloseEndPoint transport ourEndPoint = do -- Close all endpoints and kill all threads forM_ (Map.elems $ vst ^. localConnections) $ tryCloseRemoteSocket forM_ (vst ^. internalThreads) killThread - -- We send a single message to the endpoint that it is closed. Subsequent - -- calls will block. We could change this so that all subsequent calls to - -- receive return an error, but this would mean checking for some state on - -- every call to receive, which is an unnecessary overhead. - writeChan (localChannel ourEndPoint) EndPointClosed + forM_ evs $ writeChan (localChannel ourEndPoint) where -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO () diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 762c5a78..d7908640 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -6,9 +6,9 @@ import Prelude hiding (catch, (>>=), (>>), return, fail) import TestTransport (testTransport) import TestAuxiliary (forkTry, runTests) import Network.Transport -import Network.Transport.TCP (createTransport, encodeEndPointAddress) +import Network.Transport.TCP (createTransport, createTransportExposeInternals, encodeEndPointAddress) import Data.Int (Int32) -import Control.Concurrent (threadDelay) +import Control.Concurrent (threadDelay, ThreadId, killThread) import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, readMVar, isEmptyMVar) import Control.Monad (replicateM, guard, forM_, replicateM_) import Control.Applicative ((<$>)) @@ -41,7 +41,9 @@ instance Traceable N.Socket where instance Traceable N.AddrInfo where trace = traceShow - + +instance Traceable ThreadId where + trace = const Nothing -- Test that the server gets a ConnectionClosed message when the client closes -- the socket without sending an explicit control message to the server first @@ -493,6 +495,18 @@ testMany nextPort = do Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered defaultConnectHints return () +-- | Test what happens when the transport breaks completely +testBreakTransport :: IO N.ServiceName -> IO () +testBreakTransport nextPort = do + Right (transport, transportThread) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + Right endpoint <- newEndPoint transport + + killThread transportThread -- Uh oh + + ErrorEvent (TransportError EventTransportFailed _) <- receive endpoint + + return () + main :: IO () main = do portMVar <- newEmptyMVar @@ -507,5 +521,6 @@ main = do , ("InvalidAddress", testInvalidAddress nextPort) , ("InvalidConnect", testInvalidConnect nextPort) , ("TestMany", testMany nextPort) + , ("TestBreakTransport", testBreakTransport nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") From b1927ac60ea1f50e591e77a8a44d04d366b6adf3 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 21 May 2012 12:02:04 +0100 Subject: [PATCH 0066/2357] Don't reuse endpoint IDs. More error handling. In particular, catch exception when self-connecting to a closed endpoint. --- src/Network/Transport/TCP.hs | 46 +++++++++++++++++----------- tests/TestTransport.hs | 59 ++++++++++++++++++++++++++++-------- 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 6ea38dae..1f95ab75 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -85,7 +85,7 @@ import qualified Data.IntMap as IntMap (empty) import Data.IntSet (IntSet) import qualified Data.IntSet as IntSet (empty, insert, elems, singleton, null, delete) import Data.Map (Map) -import qualified Data.Map as Map (empty, elems, size) +import qualified Data.Map as Map (empty, elems) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) import System.IO (hPutStrLn, stderr) @@ -209,6 +209,7 @@ data TransportState = data ValidTransportState = ValidTransportState { _localEndPoints :: Map EndPointAddress LocalEndPoint + , _nextEndPointId :: EndPointId } data LocalEndPoint = LocalEndPoint @@ -330,7 +331,10 @@ createTransport host port = do -- | You should probably not use this function (used for unit testing only) createTransportExposeInternals :: N.HostName -> N.ServiceName -> IO (Either IOException (Transport, ThreadId)) createTransportExposeInternals host port = do - state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty } + state <- newMVar . TransportValid $ ValidTransportState + { _localEndPoints = Map.empty + , _nextEndPointId = 0 + } let transport = TCPTransport { transportState = state , transportHost = host , transportPort = port @@ -378,7 +382,6 @@ apiCloseTransport transport mTransportThread evs = do -- transport thread died (in which case we don't need to kill it) return () - -- | Create a new endpoint apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) apiNewEndPoint transport = try $ do @@ -418,8 +421,6 @@ apiConnect ourEndPoint theirAddress _reliability _hints = try $ do -- GUARANTEE: If the connection is alive on entry then the remote endpoint will -- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the state -- of the remote endpoint will not be changed. --- --- TODO: We ignore errors during a close. Is that right? apiClose :: RemoteEndPoint -> ConnectionId -> IORef Bool -> IO () apiClose theirEndPoint connId connAlive = void . tryIO $ do modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of @@ -428,6 +429,7 @@ apiClose theirEndPoint connId connAlive = void . tryIO $ do if alive then do writeIORef connAlive False + -- Possible exception caught top-level sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ vst) else @@ -454,7 +456,7 @@ apiSend theirEndPoint connId connAlive payload = do then mapExceptionIO sendFailed $ sendOn vst (encodeInt32 connId : prependLength payload) else throw $ TransportError SendClosed "Connection closed" RemoteEndPointClosed -> do - return (Left $ TransportError SendClosed "Connection lost") + return . Left $ TransportError SendClosed "Connection lost" RemoteEndPointClosing _ _ -> -- The only way for the endpoint to be in closing state, while a -- connection is still active, is for the application to call 'close' @@ -463,7 +465,7 @@ apiSend theirEndPoint connId connAlive payload = do -- yet. Even if the remote endpoint comes back with "please don't -- close", this still means that our *outgoing* connection has been -- closed - return (Left $ TransportError SendClosed "Connection lost") + return . Left $ TransportError SendClosed "Connection lost" RemoteEndPointInvalid _ -> error "apiSend RELY violation" where @@ -520,12 +522,15 @@ connectToSelf :: LocalEndPoint -> IO (Either (TransportError ConnectErrorCode) C connectToSelf ourEndPoint = do -- Here connAlive must an MVar because it is not protected by another lock connAlive <- newMVar True - -- TODO: catch exception - connId <- getNextConnectionId ourEndPoint - writeChan ourChan (ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint)) - return . Right $ Connection { send = selfSend connAlive connId - , close = selfClose connAlive connId - } + mConnId <- tryIO (getNextConnectionId ourEndPoint) + case mConnId of + Left err -> + return . Left $ TransportError ConnectNotFound (show err) + Right connId -> do + writeChan ourChan (ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint)) + return . Right $ Connection { send = selfSend connAlive connId + , close = selfClose connAlive connId + } where ourChan :: Chan Event ourChan = localChannel ourEndPoint @@ -565,13 +570,13 @@ createLocalEndPoint transport = do } modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> do - let ix = fromIntegral $ Map.size (vst ^. localEndPoints) + let ix = vst ^. nextEndPointId let addr = encodeEndPointAddress (transportHost transport) (transportPort transport) ix let localEndPoint = LocalEndPoint { localAddress = addr , localChannel = chan , localState = state } - return (TransportValid . (localEndPointAt addr ^= Just localEndPoint) $ vst, localEndPoint) + return (TransportValid . (localEndPointAt addr ^= Just localEndPoint) . (nextEndPointId ^= ix + 1) $ vst, localEndPoint) TransportClosed -> throw (TransportError NewEndPointFailed "Transport closed") @@ -644,8 +649,10 @@ requestConnectionTo ourEndPoint theirAddress = go -- point) reply <- handle failureHandler $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId case decodeInt32 . BS.concat $ reply of - Nothing -> failureHandler (userError "Invalid integer") - Just cid -> return (theirEndPoint, cid) + Nothing -> + failureHandler (userError "Invalid integer") + Just cid -> do + return (theirEndPoint, cid) -- If this is a new endpoint, fork a thread to listen for incoming -- connections. We don't want to do this while we hold the lock, because @@ -979,6 +986,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Create a new connection createNewConnection :: ControlRequestId -> IO () createNewConnection reqId = do + -- getNextConnectionId throws an exception if ourEndPoint is closed; but + -- if this endpoint is closed, this thread will soon die anyway newId <- getNextConnectionId ourEndPoint modifyMVar_ theirState $ \st -> do vst <- case st of @@ -1135,6 +1144,9 @@ firstNonReservedConnectionId = 1024 localEndPoints :: Accessor ValidTransportState (Map EndPointAddress LocalEndPoint) localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es }) +nextEndPointId :: Accessor ValidTransportState EndPointId +nextEndPointId = accessor _nextEndPointId (\eid st -> st { _nextEndPointId = eid }) + pendingCtrlRequests :: Accessor ValidLocalEndPointState (IntMap (MVar [ByteString])) pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 38c9fea5..d0e8bf91 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -16,6 +16,7 @@ import Data.Map (Map) import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) import Data.String (fromString) import Traced +import Control.Concurrent (threadDelay) -- | We overload connect to always pass the default hints connect :: EndPoint -> EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) @@ -702,25 +703,57 @@ testCloseTransport newTransport = do putMVar clientDone () mapM_ takeMVar [serverDone, clientDone] + +-- | Remote node attempts to connect to a closed local endpoint +testConnectClosedEndPoint :: Transport -> IO () +testConnectClosedEndPoint transport = do + serverAddr <- newEmptyMVar + serverClosed <- newEmptyMVar + clientDone <- newEmptyMVar + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + closeEndPoint endpoint + putMVar serverClosed () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + readMVar serverClosed + + -- Connect to a remote closed endpoint + Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered + + -- Self-connect to a closed endpoint + closeEndPoint endpoint + Left (TransportError ConnectNotFound _) <- connect endpoint (address endpoint) ReliableOrdered + + putMVar clientDone () + + takeMVar clientDone + -- Transport tests testTransport :: IO (Either String Transport) -> IO () testTransport newTransport = do Right transport <- newTransport runTests - [ ("PingPong", testPingPong transport numPings) - , ("EndPoints", testEndPoints transport numPings) - , ("Connections", testConnections transport numPings) - , ("CloseOneConnection", testCloseOneConnection transport numPings) - , ("CloseOneDirection", testCloseOneDirection transport numPings) - , ("CloseReopen", testCloseReopen transport numPings) - , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport 100) - , ("CloseTwice", testCloseTwice transport 100) - , ("ConnectToSelf", testConnectToSelf transport numPings) - , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) - , ("CloseEndPoint", testCloseEndPoint transport numPings) - , ("CloseTransport", testCloseTransport newTransport) + [ ("PingPong", testPingPong transport numPings) + , ("EndPoints", testEndPoints transport numPings) + , ("Connections", testConnections transport numPings) + , ("CloseOneConnection", testCloseOneConnection transport numPings) + , ("CloseOneDirection", testCloseOneDirection transport numPings) + , ("CloseReopen", testCloseReopen transport numPings) + , ("ParallelConnects", testParallelConnects transport numPings) + , ("SendAfterClose", testSendAfterClose transport 100) + , ("CloseTwice", testCloseTwice transport 100) + , ("ConnectToSelf", testConnectToSelf transport numPings) + , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) + , ("CloseEndPoint", testCloseEndPoint transport numPings) + , ("CloseTransport", testCloseTransport newTransport) + , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) ] where numPings = 10000 :: Int From 8dc50ff4973da3a7de00e126157df85f6198057d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 21 May 2012 13:10:44 +0100 Subject: [PATCH 0067/2357] Error handling --- src/Network/Transport/TCP.hs | 31 +++++++++++++++++++--------- tests/TestTCP.hs | 40 ++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 1f95ab75..7b738ee3 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -74,7 +74,7 @@ import Control.Concurrent.MVar ( MVar import Control.Category ((>>>)) import Control.Applicative ((<$>)) import Control.Monad (forM_, when, unless) -import Control.Exception (IOException, SomeException, handle, throw, try, bracketOnError) +import Control.Exception (IOException, SomeException, handle, throw, try, bracketOnError, mask_) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (concat) @@ -628,8 +628,9 @@ requestConnectionTo ourEndPoint theirAddress = go RemoteEndPointClosed -> do -- EndPointClosed indicates that a concurrent thread was in the -- process of closing the TCP connection to the remote endpoint when - -- we obtained a reference to it. The remote endpoint will now have - -- been removed from ourState, so we simply try again. + -- we obtained a reference to it. By INV-CLOSE we can assume that the + -- remote endpoint will now have been removed from ourState, so we + -- simply try again. go RemoteEndPointValid _ -> do @@ -641,7 +642,6 @@ requestConnectionTo ourEndPoint theirAddress = go failureHandler err = do modifyMVar_ theirState $ \(RemoteEndPointValid ep) -> return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ ep) - -- TODO: should we call closeIfUnused here? throw $ TransportError ConnectFailed (show err) -- Do the actual connection request. This blocks until the remote @@ -827,12 +827,23 @@ closeIfUnused theirEndPoint = modifyMVar_ (remoteState theirEndPoint) $ \st -> -- spawned). Returns whether or not a thread was spawned. forkEndPointThread :: LocalEndPoint -> IO () -> IO Bool forkEndPointThread ourEndPoint p = - modifyMVar ourState $ \st -> case st of - LocalEndPointValid vst -> do - tid <- forkIO (p >> removeThread) - return (LocalEndPointValid . (internalThreads ^: (tid :)) $ vst, True) - LocalEndPointClosed -> - return (LocalEndPointClosed, False) + -- We use an explicit mask_ because we don't want to be interrupted until + -- we have registered the thread. In particular, modifyMVar is not good + -- enough because if we get an asynchronous exception after the fork but + -- before the argument to modifyMVar returns we don't want to simply put + -- the old value of the mvar back. + mask_ $ do + st <- takeMVar ourState + case st of + LocalEndPointValid vst -> do + threadRegistered <- newEmptyMVar + tid <- forkIO (takeMVar threadRegistered >> p >> removeThread) + putMVar ourState $ LocalEndPointValid . (internalThreads ^: (tid :)) $ vst + putMVar threadRegistered () + return True + LocalEndPointClosed -> do + putMVar ourState st + return False where removeThread :: IO () removeThread = do diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index d7908640..7aa0b8dd 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -507,6 +507,41 @@ testBreakTransport nextPort = do return () +-- | Test that a second call to 'connect' might succeed even if the first +-- failed. This is a TCP specific test rather than an endpoint specific test +-- because we must manually create the endpoint address to match an endpoint we +-- have yet to set up +testReconnect :: IO N.ServiceName -> IO () +testReconnect nextPort = do + clientDone <- newEmptyMVar + firstAttempt <- newEmptyMVar + endpointCreated <- newEmptyMVar + port <- nextPort + Right transport <- createTransport "127.0.0.1" port + + -- Server + forkTry $ do + takeMVar firstAttempt + newEndPoint transport + putMVar endpointCreated () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + let theirAddr = encodeEndPointAddress "127.0.0.1" port 1 + + Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + putMVar firstAttempt () + + takeMVar endpointCreated + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + putMVar clientDone () + + takeMVar clientDone + + + main :: IO () main = do portMVar <- newEmptyMVar @@ -520,7 +555,8 @@ main = do , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort) , ("InvalidAddress", testInvalidAddress nextPort) , ("InvalidConnect", testInvalidConnect nextPort) - , ("TestMany", testMany nextPort) - , ("TestBreakTransport", testBreakTransport nextPort) + , ("Many", testMany nextPort) + , ("BreakTransport", testBreakTransport nextPort) + , ("Reconnect", testReconnect nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") From 4ca68e6e7ced7436faa19d77e8313b40b1483af5 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 21 May 2012 13:47:39 +0100 Subject: [PATCH 0068/2357] Error handling --- src/Network/Transport/TCP.hs | 81 ++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 7b738ee3..d05540cb 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -70,6 +70,7 @@ import Control.Concurrent.MVar ( MVar , putMVar , newEmptyMVar , withMVar + , tryPutMVar ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) @@ -691,33 +692,61 @@ requestConnectionTo ourEndPoint theirAddress = go -- RemoteEndPointInvalid. setupRemoteEndPoint :: EndPointPair -> IO () setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do - result <- socketToEndPoint (localAddress ourEndPoint) (remoteAddress theirEndPoint) - case result of - Right (sock, ConnectionRequestAccepted) -> do - let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock - } - putMVar theirState (RemoteEndPointValid vst) - handleIncomingMessages (ourEndPoint, theirEndPoint) - Right (sock, ConnectionRequestEndPointInvalid) -> do - -- We remove the endpoint from our local state again because the next - -- call to 'connect' might give a different result. Threads that were - -- waiting on the result of this call to connect will get the - -- RemoteEndPointInvalid; subsequent threads will initiate a new - -- connection requests. - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - putMVar theirState (RemoteEndPointInvalid (invalidAddress "Invalid endpoint")) - N.sClose sock - Right (sock, ConnectionRequestCrossed) -> do - N.sClose sock - Left err -> do - -- See comment above - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - putMVar theirState (RemoteEndPointInvalid err) + didAccept <- bracketOnError (socketToEndPoint (localAddress ourEndPoint) (remoteAddress theirEndPoint)) + onError $ \result -> do + case result of + Right (sock, ConnectionRequestAccepted) -> do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + } + putMVar theirState (RemoteEndPointValid vst) + return True + Right (sock, ConnectionRequestEndPointInvalid) -> do + -- We remove the endpoint from our local state again because the next + -- call to 'connect' might give a different result. Threads that were + -- waiting on the result of this call to connect will get the + -- RemoteEndPointInvalid; subsequent threads will initiate a new + -- connection requests. + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + putMVar theirState (RemoteEndPointInvalid (invalidAddress "Invalid endpoint")) + N.sClose sock + return False + Right (sock, ConnectionRequestCrossed) -> do + N.sClose sock + return False + Left err -> do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- See comment above + putMVar theirState (RemoteEndPointInvalid err) + return False + + -- If we get to this point without an exception, then + -- * if didAccept is False the socket has already been closed + -- * if didAccept is True, the socket has been stored as part of the remote + -- state so we no longer need to worry about closing it when an + -- asynchronous exception occurs + when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) where + -- If an asynchronous exception occurs while we set up the remote endpoint + -- we need to make sure to close the socket. It is also useful to + -- initialize the remote state (to "invalid") so that concurrent threads + -- that are blocked on reading the remote state are unblocked. It is + -- possible, however, that the exception occurred after we already + -- initialized the remote state, which is why we use tryPutMVar here. + onError :: Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse) -> IO () + onError result = do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + case result of + Left err -> do + tryPutMVar theirState (RemoteEndPointInvalid (TransportError ConnectFailed (show err))) + return () + Right (sock, _) -> do + tryPutMVar theirState (RemoteEndPointInvalid (TransportError ConnectFailed "setupRemoteEndPoint failed")) + tryIO $ N.sClose sock + return () + theirState = remoteState theirEndPoint invalidAddress = TransportError ConnectNotFound From 6e7c9be6f93d5ab9c375535ea013617652097ae1 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 22 May 2012 16:22:47 +0100 Subject: [PATCH 0069/2357] More error handling In particular, don't assume that errors will be caught and dealt with the thread that does the 'recv'. When a 'send' fails, we should immediately close the connection so that a subsequent call to 'connect' will re-establish the connection. --- src/Network/Transport/TCP.hs | 432 ++++++++++++++++++++++++----------- tests/TestTCP.hs | 188 +++++++++++++-- 2 files changed, 472 insertions(+), 148 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index d05540cb..507bb8bc 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -17,6 +17,7 @@ module Network.Transport.TCP ( -- * Main API createTransport -- * Internals (exposed for unit tests) , createTransportExposeInternals + , TransportInternals(..) , EndPointId , encodeEndPointAddress , decodeEndPointAddress @@ -74,8 +75,18 @@ import Control.Concurrent.MVar ( MVar ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) -import Control.Monad (forM_, when, unless) -import Control.Exception (IOException, SomeException, handle, throw, try, bracketOnError, mask_) +import Control.Monad (when, unless, join) +import Control.Monad.Error (ErrorT(..), runErrorT) +import Control.Exception ( IOException + , SomeException + , handle + , throw + , try + , bracketOnError + , mask + , mask_ + , onException + ) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (concat) @@ -89,6 +100,7 @@ import Data.Map (Map) import qualified Data.Map as Map (empty, elems) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) +import Data.Foldable (forM_) import System.IO (hPutStrLn, stderr) -- $design @@ -229,6 +241,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState , _nextCtrlRequestId :: ControlRequestId , _localConnections :: Map EndPointAddress RemoteEndPoint , _internalThreads :: [ThreadId] + , _nextRemoteId :: Int } -- A remote endpoint has incoming and outgoing connections, and when the total @@ -270,15 +283,31 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- Since we don't do concurrent reads from the same socket we don't need to -- take the lock when reading from the socket. -- --- Moreover, we insist on the invariant (INV-CLOSE) that whenever we put an --- endpoint in closed state we remove that endpoint from localConnections --- first, so that if a concurrent thread reads the mvar, finds EndPointClosed, --- and then looks up the endpoint in localConnections it is guaranteed to --- either find a different remote endpoint, or else none at all. +-- Invariants: +-- +-- INV-CLOSE: Whenever we put an endpoint in closed state we remove that +-- endpoint from localConnections first, so that if a concurrent thread reads +-- the mvar, finds EndPointClosed, and then looks up the endpoint in +-- localConnections it is guaranteed to either find a different remote +-- endpoint, or else none at all. +-- INV-RESOLVE: Whenever we move a endpoint from Closing to Closed state, we +-- signal on the corresponding MVar only *after* the endpoint has been put in +-- Closed state. This way when to threads try to resolve they don't both +-- attempt to write to the "resolved" MVar. TODO: Make sure that this +-- invariant is adhered too. +-- INV-LOST: If a send or recv fails, or a socket is closed unexpectedly, we +-- first put the remote endpoint in Closing or Closed state, and then send a +-- EventConnectionLost event. This guarantees that we only send this event +-- once. +-- INV-CLOSING: An endpoint in closing state is for all intents and purposes +-- closed; that is, we shouldn't do any 'send's on it (although 'recv' is +-- acceptable, of course -- as we are waiting for the remote endpoint to +-- confirm or deny the request). data RemoteEndPoint = RemoteEndPoint { remoteAddress :: EndPointAddress , remoteState :: MVar RemoteState + , remoteId :: Int } data RemoteState = @@ -313,6 +342,14 @@ data ConnectionRequestResponse = | ConnectionRequestCrossed -- ^ /A/s request crossed with a request from /B/ (see protocols) deriving (Enum, Bounded, Show) +-- Internal functionality we expose for unit testing +data TransportInternals = TransportInternals + { -- | The ID of the thread that listens for new incoming connections + transportThread :: ThreadId + -- | Find the socket between a local and a remote endpoint + , socketBetween :: EndPointAddress -> EndPointAddress -> IO (Either String N.Socket) + } + -------------------------------------------------------------------------------- -- Top-level functionality -- -------------------------------------------------------------------------------- @@ -330,7 +367,7 @@ createTransport host port = do return $ Right transport -- | You should probably not use this function (used for unit testing only) -createTransportExposeInternals :: N.HostName -> N.ServiceName -> IO (Either IOException (Transport, ThreadId)) +createTransportExposeInternals :: N.HostName -> N.ServiceName -> IO (Either IOException (Transport, TransportInternals)) createTransportExposeInternals host port = do state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty @@ -344,11 +381,13 @@ createTransportExposeInternals host port = do -- For a discussion of the use of N.sOMAXCONN, see -- http://tangentsoft.net/wskfaq/advanced.html#backlog -- http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/023/2333/2333s2.html - transportThread <- forkServer host port N.sOMAXCONN (terminationHandler transport) (handleConnectionRequest transport) + tid <- forkServer host port N.sOMAXCONN (terminationHandler transport) (handleConnectionRequest transport) return ( Transport { newEndPoint = apiNewEndPoint transport - , closeTransport = apiCloseTransport transport (Just transportThread) [EndPointClosed] + , closeTransport = apiCloseTransport transport (Just tid) [EndPointClosed] } - , transportThread + , TransportInternals { transportThread = tid + , socketBetween = internalSocketBetween transport + } ) where terminationHandler :: TCPTransport -> SomeException -> IO () @@ -366,22 +405,13 @@ apiCloseTransport transport mTransportThread evs = do mTSt <- modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> return (TransportClosed, Just vst) TransportClosed -> return (TransportClosed, Nothing) - case mTSt of - Nothing -> - -- Transport already closed - return () - Just tSt -> do - mapM_ (apiCloseEndPoint transport evs) (Map.elems $ tSt ^. localEndPoints) - case mTransportThread of - Just transportThread -> - -- This will invoke the termination handler, which in turn will call - -- apiCloseTransport again, but at that point the transport is - -- already in closed state so at that point we return immediately - killThread transportThread - Nothing -> - -- This will happen is apiCloseTransport was invoked *because* the - -- transport thread died (in which case we don't need to kill it) - return () + forM_ mTSt $ \tSt -> do + mapM_ (apiCloseEndPoint transport evs) (Map.elems $ tSt ^. localEndPoints) + forM_ mTransportThread $ \tid -> do + -- This will invoke the termination handler, which in turn will call + -- apiCloseTransport again, but at that point the transport is + -- already in closed state so at that point we return immediately + killThread tid -- | Create a new endpoint apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) @@ -412,8 +442,8 @@ apiConnect ourEndPoint theirAddress _reliability _hints = try $ do -- connAlive can be an IORef rather than an MVar because it is protected by -- the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True - return $ Connection { send = apiSend theirEndPoint connId connAlive - , close = apiClose theirEndPoint connId connAlive + return $ Connection { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive + , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive } -- | Close a connection @@ -422,53 +452,49 @@ apiConnect ourEndPoint theirAddress _reliability _hints = try $ do -- GUARANTEE: If the connection is alive on entry then the remote endpoint will -- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the state -- of the remote endpoint will not be changed. -apiClose :: RemoteEndPoint -> ConnectionId -> IORef Bool -> IO () -apiClose theirEndPoint connId connAlive = void . tryIO $ do - modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointValid vst -> do +apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () +apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do + modifyRemoteState_ (ourEndPoint, theirEndPoint) + -- RemoteEndPointInvalid + (\_ -> fail "apiClose RELY violation") + -- RemoteEndPointValid + (\vst -> do alive <- readIORef connAlive if alive then do writeIORef connAlive False - -- Possible exception caught top-level sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ vst) else - return (RemoteEndPointValid vst) - RemoteEndPointClosed -> do - return st - RemoteEndPointClosing _ _ -> - -- See discussion in apiSend why we must allow for this case - return st - RemoteEndPointInvalid _ -> - fail "apiClose RELY violation" - closeIfUnused theirEndPoint + return (RemoteEndPointValid vst)) + -- RemoteEndPointClosing + (\(resolved, vst) -> return $ RemoteEndPointClosing resolved vst) + -- RemoteEndPoinClosed + (return $ RemoteEndPointClosed) + closeIfUnused (ourEndPoint, theirEndPoint) -- | Send data across a connection -- -- RELY: The endpoint must not be in invalid state. -- GUARANTEE: The state of the remote endpoint will not be changed. -apiSend :: RemoteEndPoint -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) -apiSend theirEndPoint connId connAlive payload = do - withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointValid vst -> do +apiSend :: EndPointPair -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) +apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = do + join <$> (try . mapExceptionIO sendFailed $ modifyRemoteState (ourEndPoint, theirEndPoint) + -- RemoteEndPointInvalid + (\_ -> fail "apiSend RELY violation") + -- RemoteEndPointValid + (\vst -> do alive <- readIORef connAlive - try $ if alive - then mapExceptionIO sendFailed $ sendOn vst (encodeInt32 connId : prependLength payload) - else throw $ TransportError SendClosed "Connection closed" - RemoteEndPointClosed -> do - return . Left $ TransportError SendClosed "Connection lost" - RemoteEndPointClosing _ _ -> - -- The only way for the endpoint to be in closing state, while a - -- connection is still active, is for the application to call 'close' - -- on the connection triggering "garbage collection" of the TCP channel - -- (CloseSocket request to the remote endpoint) which hasn't completed - -- yet. Even if the remote endpoint comes back with "please don't - -- close", this still means that our *outgoing* connection has been - -- closed - return . Left $ TransportError SendClosed "Connection lost" - RemoteEndPointInvalid _ -> - error "apiSend RELY violation" + if alive + then do + sendOn vst (encodeInt32 connId : prependLength payload) + return (RemoteEndPointValid vst, Right $ ()) + else + return (RemoteEndPointValid vst, Left $ TransportError SendClosed "Connection closed")) + -- RemoteEndPointClosing + (\(resolved, vst) -> return (RemoteEndPointClosing resolved vst, Left $ TransportError SendClosed "Connection lost")) + -- RemoteEndPointClosed + (return (RemoteEndPointClosed, Left $ TransportError SendClosed "Connection lost"))) where sendFailed :: IOException -> TransportError SendErrorCode sendFailed = TransportError SendFailed . show @@ -488,14 +514,11 @@ apiCloseEndPoint transport evs ourEndPoint = do return (LocalEndPointClosed, Just vst) LocalEndPointClosed -> return (LocalEndPointClosed, Nothing) - case mOurState of - Nothing -> -- Already closed - return () - Just vst -> do - -- Close all endpoints and kill all threads - forM_ (Map.elems $ vst ^. localConnections) $ tryCloseRemoteSocket - forM_ (vst ^. internalThreads) killThread - forM_ evs $ writeChan (localChannel ourEndPoint) + forM_ mOurState $ \vst -> do + -- Close all endpoints and kill all threads + forM_ (Map.elems $ vst ^. localConnections) $ tryCloseRemoteSocket + forM_ (vst ^. internalThreads) killThread + forM_ evs $ writeChan (localChannel ourEndPoint) where -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO () @@ -552,6 +575,43 @@ connectToSelf ourEndPoint = do when alive $ writeChan ourChan (ConnectionClosed connId) return False +-------------------------------------------------------------------------------- +-- Functions from TransportInternals -- +-------------------------------------------------------------------------------- + +-- Find a socket between two endpoints +internalSocketBetween :: TCPTransport -- ^ Transport + -> EndPointAddress -- ^ Local endpoint + -> EndPointAddress -- ^ Remote endpoint + -> IO (Either String N.Socket) +internalSocketBetween transport ourAddress theirAddress = runErrorT $ do + ourEndPoint <- ErrorT $ do + withMVar (transportState transport) $ \st -> case st of + TransportClosed -> + return . Left $ "Transport closed" + TransportValid vst -> do + case vst ^. localEndPointAt ourAddress of + Nothing -> return . Left $ "Local endpoint not found" + Just ep -> return . Right $ ep + theirEndPoint <- ErrorT $ do + withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointClosed -> + return . Left $ "Local endpoint closed" + LocalEndPointValid vst -> do + case vst ^. localConnectionTo theirAddress of + Nothing -> return . Left $ "Remote endpoint not found" + Just ep -> return . Right $ ep + ErrorT $ do + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> + return . Right $ remoteSocket vst + RemoteEndPointClosing _ _ -> + return . Left $ "Remote endpoint closing" + RemoteEndPointClosed -> + return . Left $ "Remote endpoint closed" + RemoteEndPointInvalid _ -> + return . Left $ "Remote endpoint invalid" + -------------------------------------------------------------------------------- -- Lower level functionality -- -------------------------------------------------------------------------------- @@ -568,6 +628,7 @@ createLocalEndPoint transport = do , _nextCtrlRequestId = 0 , _localConnections = Map.empty , _internalThreads = [] + , _nextRemoteId = 0 } modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> do @@ -635,23 +696,15 @@ requestConnectionTo ourEndPoint theirAddress = go go RemoteEndPointValid _ -> do - -- On a failure we decrement the refcount again and return an error. - -- The only state the remote endpoint can be in at this point is - -- valid. As mentioned above, we can rely on the endpoint being in - -- valid state at this point. - let failureHandler :: IOException -> IO b - failureHandler err = do - modifyMVar_ theirState $ \(RemoteEndPointValid ep) -> - return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ ep) - throw $ TransportError ConnectFailed (show err) - -- Do the actual connection request. This blocks until the remote -- endpoint replies (but note that we don't hold any locks at this - -- point) - reply <- handle failureHandler $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + -- point). Note that doRemoteRequest may throw an error if the send + -- fails, and if it does, it will have put the remote endpoint in + -- closed state. + reply <- mapExceptionIO connectFailed $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId case decodeInt32 . BS.concat $ reply of Nothing -> - failureHandler (userError "Invalid integer") + throw (connectFailed $ userError "Invalid integer") Just cid -> do return (theirEndPoint, cid) @@ -672,8 +725,9 @@ requestConnectionTo ourEndPoint theirAddress = go theirState <- newEmptyMVar let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress , remoteState = theirState + , remoteId = vst ^. nextRemoteId } - return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) $ vst, (theirEndPoint, True)) + return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) . (nextRemoteId ^: (+ 1)) $ vst, (theirEndPoint, True)) -- The only way for forkEndPointThread to fail is if the local endpoint -- gets closed. This error will be caught elsewhere, so we ignore it @@ -685,6 +739,9 @@ requestConnectionTo ourEndPoint theirAddress = go ourState :: MVar LocalEndPointState ourState = localState ourEndPoint + connectFailed :: IOException -> TransportError ConnectErrorCode + connectFailed = TransportError ConnectFailed . show + -- | Set up a remote endpoint -- -- RELY: The state of the remote endpoint must be uninitialized. @@ -700,7 +757,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do { remoteSocket = sock , _remoteOutgoing = 0 , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock + , sendOn = \msg -> sendMany sock msg `onException` (tryIO $ N.sClose sock) } putMVar theirState (RemoteEndPointValid vst) return True @@ -783,11 +840,20 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do -- If the local endpoint is closed, do nothing removeRemoteEndPoint :: EndPointPair -> IO () removeRemoteEndPoint (ourEndPoint, theirEndPoint) = do - modifyMVar_ (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> - return (LocalEndPointValid . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) $ vst) - LocalEndPointClosed -> - return LocalEndPointClosed + modifyMVar_ ourState $ \st -> case st of + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Nothing -> + return st + Just remoteEndPoint' -> + if remoteId remoteEndPoint' == remoteId theirEndPoint + then return (LocalEndPointValid . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) $ vst) + else return st + LocalEndPointClosed -> + return LocalEndPointClosed + where + ourState = localState ourEndPoint + theirAddress = remoteAddress theirEndPoint -- | Remove reference to a local endpoint from the transport state -- @@ -817,11 +883,11 @@ decodeEndPointAddress (EndPointAddress bs) = case map BSC.unpack $ BSC.split ':' -- | Do a (blocking) remote request -- --- RELY: Remote endpoint must be in valid state. +-- RELY: Remote endpoint must be in valid or closed state. -- GUARANTEE: Will not change the state of the remote endpoint. -- --- May throw IO (user) exception if the local endpoint is closed or if the send --- fails. +-- May throw IO (user) exception if the local or the remote endpoint is closed, +-- or if the send fails. doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] doRemoteRequest (ourEndPoint, theirEndPoint) header = do reply <- newEmptyMVar @@ -831,24 +897,35 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do return (LocalEndPointValid . (nextCtrlRequestId ^: (+ 1)) . (pendingCtrlRequestsAt reqId ^= Just reply) $ vst, reqId) LocalEndPointClosed -> throw (userError "Local endpoint closed") - withMVar (remoteState theirEndPoint) $ \(RemoteEndPointValid vst) -> - sendOn vst [encodeInt32 header, encodeInt32 reqId] + withRemoteState (ourEndPoint, theirEndPoint) + -- RemoteEndPointInvalid + (\_ -> fail "doRemoteRequest RELY violation") + -- RemoteEndPointValid + (\vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId]) + -- RemoteEndPointClosing + (\_ -> fail "doRemoteRequest RELY violation") + -- RemoteEndPointClosed + (throw (userError "Remote endpoint closed")) takeMVar reply -- | Check if the remote endpoint is unused, and if so, send a CloseSocket request -closeIfUnused :: RemoteEndPoint -> IO () -closeIfUnused theirEndPoint = modifyMVar_ (remoteState theirEndPoint) $ \st -> - case st of - RemoteEndPointValid vst -> do - if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) - then do - resolved <- newEmptyMVar - sendOn vst [encodeInt32 CloseSocket] - return (RemoteEndPointClosing resolved vst) - else - return st - _ -> do - return st +closeIfUnused :: EndPointPair -> IO () +closeIfUnused (ourEndPoint, theirEndPoint) = + modifyRemoteState_ (ourEndPoint, theirEndPoint) + -- RemoteEndPointInvalid + (return . RemoteEndPointInvalid) + -- RemoteEndPointValid + (\vst -> if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + then do + sendOn vst [encodeInt32 CloseSocket] + resolved <- newEmptyMVar + return $ RemoteEndPointClosing resolved vst + else + return $ RemoteEndPointValid vst) + -- RemoteEndPointClosing + (return . uncurry RemoteEndPointClosing) + -- RemoteEndPointClosed + (return RemoteEndPointClosed) -- | Fork a new thread and store its ID as part of the transport state -- @@ -886,6 +963,109 @@ forkEndPointThread ourEndPoint p = ourState :: MVar LocalEndPointState ourState = localState ourEndPoint +-- | Like modifyMVar, but if an exception occurs don't restore the remote +-- endpoint to its original value but close it instead +modifyRemoteState :: EndPointPair -- ^ Local and remote endpoint + -> (TransportError ConnectErrorCode -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointInvalid + -> (ValidRemoteEndPointState -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointValid + -> ((MVar (), ValidRemoteEndPointState) -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointClosing + -> IO (RemoteState, a) -- ^ Case for RemoteEndPointClosed + -> IO a +modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = do + mask $ \restore -> do + st <- takeMVar theirState + case st of + RemoteEndPointValid vst -> do + mResult <- try $ restore (caseValid vst) + case mResult of + Right (st', a) -> do + putMVar theirState st' + return a + Left ex -> do + handleException ex vst + -- The other cases are less interesting, because unless the endpoint is + -- in Valid state we're not supposed to do any IO on it + RemoteEndPointClosing resolved vst -> do + (st', a) <- restore (caseClosing (resolved, vst)) `onException` putMVar theirState st + putMVar theirState st' + return a + RemoteEndPointInvalid err -> do + (st', a) <- restore (caseInvalid err) `onException` putMVar theirState st + putMVar theirState st' + return a + RemoteEndPointClosed -> do + (st', a) <- restore caseClosed `onException` putMVar theirState st + putMVar theirState st' + return a + where + theirState :: MVar RemoteState + theirState = remoteState theirEndPoint + + handleException :: SomeException -> ValidRemoteEndPointState -> IO a + handleException ex vst = do + -- We need to remove the remote endpoint from the local endpoint before + -- putting it in Closed state (by INV-CLOSE), but we don't want to hold + -- two locks at the same time. So we put it in closing state first .. + resolved <- newEmptyMVar + putMVar theirState (RemoteEndPointClosing resolved vst) + -- .. then remove it from the local endpoint .. + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + -- .. close it and notify the local endpoint we lost the connection .. + mIncoming <- closeRemoteEndPoint + forM_ mIncoming $ \incoming -> do + let err = TransportError (EventConnectionLost (remoteAddress theirEndPoint) incoming) (show ex) + writeChan (localChannel ourEndPoint) $ ErrorEvent err + -- .. and finally rethrow the exception + throw ex + + -- Returns the set of incoming connections if we closed the connection, or + -- 'Nothing' if the endpoint was already closed + closeRemoteEndPoint :: IO (Maybe [ConnectionId]) + closeRemoteEndPoint = do + st <- takeMVar theirState + case st of + RemoteEndPointClosing resolved vst -> do + putMVar theirState RemoteEndPointClosed + putMVar resolved () + return . Just . IntSet.elems $ vst ^. remoteIncoming + RemoteEndPointClosed -> do + putMVar theirState RemoteEndPointClosed + return Nothing + _ -> + fail "the impossible happened" + +-- | Like 'modifyRemoteState' but without a return value +modifyRemoteState_ :: EndPointPair -- ^ Local and remote endpoint + -> (TransportError ConnectErrorCode -> IO RemoteState) -- ^ Case for RemoteEndPointInvalid + -> (ValidRemoteEndPointState -> IO RemoteState) -- ^ Case for RemoteEndPointValid + -> ((MVar (), ValidRemoteEndPointState) -> IO RemoteState) -- ^ Case for RemoteEndPointClosing + -> IO RemoteState -- ^ Case for RemoteEndPointClosed + -> IO () +modifyRemoteState_ (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = + modifyRemoteState (ourEndPoint, theirEndPoint) + (u . caseInvalid) + (u . caseValid) + (u . caseClosing) + (u caseClosed) + where + u :: IO a -> IO (a, ()) + u p = p >>= \a -> return (a, ()) + +-- | Like 'modifyRemoteState' but without the ability to change the state +withRemoteState :: EndPointPair -- ^ Local and remote endpoint + -> (TransportError ConnectErrorCode -> IO a) -- ^ Case for RemoteEndPointInvalid + -> (ValidRemoteEndPointState -> IO a) -- ^ Case for RemoteEndPointValid + -> ((MVar (), ValidRemoteEndPointState) -> IO a) -- ^ Case for RemoteEndPointClosing + -> IO a -- ^ Case for RemoteEndPointClosed + -> IO a +withRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = do + modifyRemoteState (ourEndPoint, theirEndPoint) + (\err -> (,) (RemoteEndPointInvalid err) <$> caseInvalid err) + (\vst -> (,) (RemoteEndPointValid vst) <$> caseValid vst) + (\(resolved, vst) -> (,) (RemoteEndPointClosing resolved vst) <$> caseClosing (resolved, vst)) + ((,) RemoteEndPointClosed <$> caseClosed) + + -------------------------------------------------------------------------------- -- Incoming requests -- -------------------------------------------------------------------------------- @@ -928,8 +1108,9 @@ handleConnectionRequest transport sock = handle tryCloseSocket $ do theirState <- newEmptyMVar let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress , remoteState = theirState + , remoteId = vst ^. nextRemoteId } - return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) $ vst, (False, theirEndPoint)) + return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) . (nextRemoteId ^: (+ 1)) $ vst, (False, theirEndPoint)) Just theirEndPoint -> return (st, (localAddress ourEndPoint < theirAddress, theirEndPoint)) LocalEndPointClosed -> @@ -958,11 +1139,7 @@ handleConnectionRequest transport sock = handle tryCloseSocket $ do -- been recorded as part of the remote endpoint. Either way, we no longer -- have to worry about closing the socket on receiving an asynchronous -- exception from this point forward. - case mEndPoint of - Nothing -> - return () - Just theirEndPoint -> - handleIncomingMessages (ourEndPoint, theirEndPoint) + forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint tryCloseSocket :: IOException -> IO () tryCloseSocket _ = void . tryIO $ N.sClose sock @@ -1076,7 +1253,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do writeChan ourChannel (ConnectionClosed cid) modifyMVar_ theirState $ \(RemoteEndPointValid vst) -> return (RemoteEndPointValid . (remoteIncoming ^: IntSet.delete cid) $ vst) - closeIfUnused theirEndPoint + closeIfUnused (ourEndPoint, theirEndPoint) -- Close the socket (if we don't have any outgoing connections) closeSocket :: N.Socket -> IO Bool @@ -1138,24 +1315,18 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do prematureExit sock err = do N.sClose sock removeRemoteEndPoint (ourEndPoint, theirEndPoint) - mUnclosedConnections <- modifyMVar theirState $ \st -> + modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> error "handleIncomingMessages RELY violation" - RemoteEndPointValid vst -> - return (RemoteEndPointClosed, Just $ vst ^. remoteIncoming) + RemoteEndPointValid vst -> do + writeChan ourChannel . ErrorEvent $ + TransportError (EventConnectionLost (remoteAddress theirEndPoint) (IntSet.elems $ vst ^. remoteIncoming)) (show err) + return RemoteEndPointClosed RemoteEndPointClosing _ _ -> - return (RemoteEndPointClosed, Nothing) + return RemoteEndPointClosed RemoteEndPointClosed -> - return (st, Nothing) - - -- We send the connection lost message even if unclosedConnections is the - -- empty set, because *outgoing* connections will be broken too now - case mUnclosedConnections of - Nothing -> - return () - Just unclosedConnections -> writeChan ourChannel . ErrorEvent $ - TransportError (EventConnectionLost (remoteAddress theirEndPoint) (IntSet.elems unclosedConnections)) (show err) + return RemoteEndPointClosed -- | Get the next connection ID -- @@ -1202,6 +1373,9 @@ localConnections = accessor _localConnections (\es st -> st { _localConnections internalThreads :: Accessor ValidLocalEndPointState [ThreadId] internalThreads = accessor _internalThreads (\ts st -> st { _internalThreads = ts }) +nextRemoteId :: Accessor ValidLocalEndPointState Int +nextRemoteId = accessor _nextRemoteId (\rid st -> st { _nextRemoteId = rid }) + localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 7aa0b8dd..36fe5a35 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -6,29 +6,44 @@ import Prelude hiding (catch, (>>=), (>>), return, fail) import TestTransport (testTransport) import TestAuxiliary (forkTry, runTests) import Network.Transport -import Network.Transport.TCP (createTransport, createTransportExposeInternals, encodeEndPointAddress) +import Network.Transport.TCP ( createTransport + , createTransportExposeInternals + , TransportInternals(..) + , encodeEndPointAddress + ) import Data.Int (Int32) import Control.Concurrent (threadDelay, ThreadId, killThread) -import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, readMVar, isEmptyMVar) -import Control.Monad (replicateM, guard, forM_, replicateM_) +import Control.Concurrent.MVar ( MVar + , newEmptyMVar + , putMVar + , takeMVar + , readMVar + , isEmptyMVar + , newMVar + , modifyMVar + ) +import Control.Monad (replicateM, guard, forM_, replicateM_, when) import Control.Applicative ((<$>)) -import Control.Exception (throw) +import Control.Exception (throw, try, SomeException) import Network.Transport.TCP ( ControlHeader(..) , ConnectionRequestResponse(..) , socketToEndPoint ) -import Network.Transport.Internal (encodeInt32, prependLength, tlog, tryIO) +import Network.Transport.Internal (encodeInt32, prependLength, tlog, tryIO, void) import Network.Transport.Internal.TCP (recvInt32, forkServer, recvWithLength) import qualified Network.Socket as N ( sClose , ServiceName , Socket , AddrInfo + , shutdown + , ShutdownCmd(ShutdownSend) ) import Network.Socket.ByteString (sendMany) import Data.String (fromString) import Traced import GHC.IO.Exception (ioe_errno) import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) +import System.Timeout (timeout) instance Traceable ControlHeader where trace = traceShow @@ -37,7 +52,7 @@ instance Traceable ConnectionRequestResponse where trace = traceShow instance Traceable N.Socket where - trace = const Nothing + trace = traceShow instance Traceable N.AddrInfo where trace = traceShow @@ -45,6 +60,12 @@ instance Traceable N.AddrInfo where instance Traceable ThreadId where trace = const Nothing +instance Traceable TransportInternals where + trace = const Nothing + +instance Traceable SomeException where + trace = traceShow + -- Test that the server gets a ConnectionClosed message when the client closes -- the socket without sending an explicit control message to the server first testEarlyDisconnect :: IO N.ServiceName -> IO () @@ -498,10 +519,10 @@ testMany nextPort = do -- | Test what happens when the transport breaks completely testBreakTransport :: IO N.ServiceName -> IO () testBreakTransport nextPort = do - Right (transport, transportThread) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + Right (transport, internals) <- nextPort >>= createTransportExposeInternals "127.0.0.1" Right endpoint <- newEndPoint transport - killThread transportThread -- Uh oh + killThread (transportThread internals) -- Uh oh ErrorEvent (TransportError EventTransportFailed _) <- receive endpoint @@ -510,37 +531,165 @@ testBreakTransport nextPort = do -- | Test that a second call to 'connect' might succeed even if the first -- failed. This is a TCP specific test rather than an endpoint specific test -- because we must manually create the endpoint address to match an endpoint we --- have yet to set up +-- have yet to set up. +-- Then test that we get a connection lost message after the remote endpoint +-- suddenly closes the socket, and that a subsequent 'connect' allows us to +-- re-establish a connection to the same endpoint testReconnect :: IO N.ServiceName -> IO () testReconnect nextPort = do - clientDone <- newEmptyMVar + serverPort <- nextPort + serverDone <- newEmptyMVar firstAttempt <- newEmptyMVar endpointCreated <- newEmptyMVar - port <- nextPort - Right transport <- createTransport "127.0.0.1" port -- Server forkTry $ do - takeMVar firstAttempt - newEndPoint transport + -- Wait for the client to do its first attempt + readMVar firstAttempt + + counter <- newMVar (0 :: Int) + + forkServer "127.0.0.1" serverPort 5 throw $ \sock -> do + -- Accept the connection + Right 0 <- tryIO $ (recvInt32 sock :: IO Int) + Right _ <- tryIO $ recvWithLength sock + Right () <- tryIO $ sendMany sock [encodeInt32 ConnectionRequestAccepted] + + -- The first time we close the socket before accepting the logical connection + count <- modifyMVar counter $ \i -> return (i + 1, i) + + when (count > 0) $ do + -- Client requests a logical connection + Right RequestConnectionId <- tryIO $ toEnum <$> (recvInt32 sock :: IO Int) + Right reqId <- tryIO $ (recvInt32 sock :: IO Int) + Right () <- tryIO $ sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + return () + + when (count > 1) $ do + -- Client sends a message + Right 10001 <- tryIO $ (recvInt32 sock :: IO Int) + Right ["ping"] <- tryIO $ recvWithLength sock + putMVar serverDone () + + Right () <- tryIO $ N.sClose sock + return () + putMVar endpointCreated () -- Client forkTry $ do - Right endpoint <- newEndPoint transport - let theirAddr = encodeEndPointAddress "127.0.0.1" port 1 + Right transport <- nextPort >>= createTransport "127.0.0.1" + Right endpoint <- newEndPoint transport + let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 + -- The first attempt will fail because no endpoint is yet set up Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints putMVar firstAttempt () + -- The second attempt will fail because the server closes the socket before we can request a connection takeMVar endpointCreated - Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - putMVar clientDone () + -- The third attempt succeeds + Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- But a send will fail because the server has closed the connection again + Left (TransportError SendClosed _) <- send conn1 ["ping"] + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint - takeMVar clientDone + -- But a subsequent call to connect should reestablish the connection + Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + -- Send should now succeed + Right () <- send conn2 ["ping"] + return () + takeMVar serverDone + +-- Test what happens if we close the socket one way only. This means that the +-- 'recv' in 'handleIncomingMessages' will not fail, but a 'send' or 'connect' +-- *will* fail. We are testing that error handling everywhere does the right +-- thing. +testUnidirectionalError :: IO N.ServiceName -> IO () +testUnidirectionalError nextPort = do + clientDone <- newEmptyMVar + serverPort <- nextPort + serverGotPing <- newEmptyMVar + + -- Server + forkServer "127.0.0.1" serverPort 5 throw $ \sock -> do + -- We accept connections, but when an exception occurs we don't do + -- anything (in particular, we don't close the socket). This is important + -- because when we shutdown one direction of the socket a recv here will + -- fail, but we don't want to close that socket at that point (which + -- would shutdown the socket in the other direction) + void . (try :: IO () -> IO (Either SomeException ())) $ do + 0 <- recvInt32 sock :: IO Int + _ <- recvWithLength sock + () <- sendMany sock [encodeInt32 ConnectionRequestAccepted] + + RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) + reqId <- recvInt32 sock :: IO Int + sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + + 10001 <- recvInt32 sock :: IO Int + ["ping"] <- recvWithLength sock + putMVar serverGotPing () + + -- Client + forkTry $ do + Right (transport, internals) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + Right endpoint <- newEndPoint transport + let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 + + -- Establish a connection to the server + Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn1 ["ping"] + takeMVar serverGotPing + + -- Close the *outgoing* part of the socket only + Right sock <- socketBetween internals (address endpoint) theirAddr + N.shutdown sock N.ShutdownSend + + -- At this point we cannot notice the problem yet so we shouldn't receive an event yet + Nothing <- timeout 500000 $ receive endpoint + + -- But when we send we find the error + Left (TransportError SendFailed _) <- send conn1 ["ping"] + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + + -- A call to connect should now re-establish the connection + Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn2 ["ping"] + takeMVar serverGotPing + + -- Again, close the outgoing part of the socket + Right sock' <- socketBetween internals (address endpoint) theirAddr + N.shutdown sock' N.ShutdownSend + + -- We now find the error when we attempt to close the connection + Nothing <- timeout 500000 $ receive endpoint + close conn2 + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + Right conn3 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn3 ["ping"] + takeMVar serverGotPing + + -- We repeat once more. + Right sock'' <- socketBetween internals (address endpoint) theirAddr + N.shutdown sock'' N.ShutdownSend + + -- Now we notice the problem when we try to connect + Nothing <- timeout 500000 $ receive endpoint + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + Right conn4 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn4 ["ping"] + takeMVar serverGotPing + + putMVar clientDone () + + takeMVar clientDone main :: IO () main = do @@ -558,5 +707,6 @@ main = do , ("Many", testMany nextPort) , ("BreakTransport", testBreakTransport nextPort) , ("Reconnect", testReconnect nextPort) + , ("UnidirectionalError", testUnidirectionalError nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") From fe026ea3f61771513d7ac74861b96970a28f5996 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 22 May 2012 17:22:01 +0100 Subject: [PATCH 0070/2357] Code cleanup --- src/Network/Transport/Chan.hs | 10 +-- src/Network/Transport/TCP.hs | 125 ++++++++++++++++------------------ 2 files changed, 66 insertions(+), 69 deletions(-) diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs index 217f3c52..85f20b3e 100644 --- a/src/Network/Transport/Chan.hs +++ b/src/Network/Transport/Chan.hs @@ -73,11 +73,13 @@ apiConnect myAddress state theirAddress _reliability _hints = do -- | Send a message over a connection apiSend :: Chan Event -> ConnectionId -> MVar Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) apiSend chan conn connAlive msg = - modifyMVar connAlive $ \alive -> do + modifyMVar connAlive $ \alive -> if alive - then do writeChan chan (Received conn msg) - return (alive, Right ()) - else do return (alive, Left (TransportError SendFailed "Connection closed")) + then do + writeChan chan (Received conn msg) + return (alive, Right ()) + else + return (alive, Left (TransportError SendFailed "Connection closed")) -- | Close a connection apiClose :: Chan Event -> ConnectionId -> MVar Bool -> IO () diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 507bb8bc..823a82a5 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -29,7 +29,7 @@ module Network.Transport.TCP ( -- * Main API -- $design ) where -import Prelude hiding (catch) +import Prelude hiding (catch, mapM_) import Network.Transport import Network.Transport.Internal.TCP ( forkServer , recvWithLength @@ -97,10 +97,10 @@ import qualified Data.IntMap as IntMap (empty) import Data.IntSet (IntSet) import qualified Data.IntSet as IntSet (empty, insert, elems, singleton, null, delete) import Data.Map (Map) -import qualified Data.Map as Map (empty, elems) +import qualified Data.Map as Map (empty) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) -import Data.Foldable (forM_) +import Data.Foldable (forM_, mapM_) import System.IO (hPutStrLn, stderr) -- $design @@ -358,13 +358,8 @@ data TransportInternals = TransportInternals -- -- TODOs: deal with hints createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) -createTransport host port = do - result <- createTransportExposeInternals host port - case result of - Left err -> - return $ Left err - Right (transport, _) -> - return $ Right transport +createTransport host port = + either Left (Right . fst) <$> createTransportExposeInternals host port -- | You should probably not use this function (used for unit testing only) createTransportExposeInternals :: N.HostName -> N.ServiceName -> IO (Either IOException (Transport, TransportInternals)) @@ -382,12 +377,14 @@ createTransportExposeInternals host port = do -- http://tangentsoft.net/wskfaq/advanced.html#backlog -- http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/023/2333/2333s2.html tid <- forkServer host port N.sOMAXCONN (terminationHandler transport) (handleConnectionRequest transport) - return ( Transport { newEndPoint = apiNewEndPoint transport - , closeTransport = apiCloseTransport transport (Just tid) [EndPointClosed] - } - , TransportInternals { transportThread = tid - , socketBetween = internalSocketBetween transport - } + return ( Transport + { newEndPoint = apiNewEndPoint transport + , closeTransport = apiCloseTransport transport (Just tid) [EndPointClosed] + } + , TransportInternals + { transportThread = tid + , socketBetween = internalSocketBetween transport + } ) where terminationHandler :: TCPTransport -> SomeException -> IO () @@ -405,13 +402,11 @@ apiCloseTransport transport mTransportThread evs = do mTSt <- modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> return (TransportClosed, Just vst) TransportClosed -> return (TransportClosed, Nothing) - forM_ mTSt $ \tSt -> do - mapM_ (apiCloseEndPoint transport evs) (Map.elems $ tSt ^. localEndPoints) - forM_ mTransportThread $ \tid -> do - -- This will invoke the termination handler, which in turn will call - -- apiCloseTransport again, but at that point the transport is - -- already in closed state so at that point we return immediately - killThread tid + forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) + -- This will invoke the termination handler, which in turn will call + -- apiCloseTransport again, but then the transport will already be closed and + -- we won't be passed a transport thread, so we terminate immmediate + forM_ mTransportThread killThread -- | Create a new endpoint apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) @@ -442,9 +437,10 @@ apiConnect ourEndPoint theirAddress _reliability _hints = try $ do -- connAlive can be an IORef rather than an MVar because it is protected by -- the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True - return $ Connection { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive - , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive - } + return Connection + { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive + , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive + } -- | Close a connection -- @@ -468,9 +464,9 @@ apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do else return (RemoteEndPointValid vst)) -- RemoteEndPointClosing - (\(resolved, vst) -> return $ RemoteEndPointClosing resolved vst) + (return . uncurry RemoteEndPointClosing) -- RemoteEndPoinClosed - (return $ RemoteEndPointClosed) + (return RemoteEndPointClosed) closeIfUnused (ourEndPoint, theirEndPoint) -- | Send data across a connection @@ -478,8 +474,8 @@ apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do -- RELY: The endpoint must not be in invalid state. -- GUARANTEE: The state of the remote endpoint will not be changed. apiSend :: EndPointPair -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) -apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = do - join <$> (try . mapExceptionIO sendFailed $ modifyRemoteState (ourEndPoint, theirEndPoint) +apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = + join <$> (try . mapExceptionIO sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) -- RemoteEndPointInvalid (\_ -> fail "apiSend RELY violation") -- RemoteEndPointValid @@ -488,13 +484,13 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = do if alive then do sendOn vst (encodeInt32 connId : prependLength payload) - return (RemoteEndPointValid vst, Right $ ()) + return . Right $ () else - return (RemoteEndPointValid vst, Left $ TransportError SendClosed "Connection closed")) + return . Left $ TransportError SendClosed "Connection closed") -- RemoteEndPointClosing - (\(resolved, vst) -> return (RemoteEndPointClosing resolved vst, Left $ TransportError SendClosed "Connection lost")) + (return . Left . const (TransportError SendClosed "Connection lost")) -- RemoteEndPointClosed - (return (RemoteEndPointClosed, Left $ TransportError SendClosed "Connection lost"))) + (return . Left $ TransportError SendClosed "Connection lost")) where sendFailed :: IOException -> TransportError SendErrorCode sendFailed = TransportError SendFailed . show @@ -516,13 +512,13 @@ apiCloseEndPoint transport evs ourEndPoint = do return (LocalEndPointClosed, Nothing) forM_ mOurState $ \vst -> do -- Close all endpoints and kill all threads - forM_ (Map.elems $ vst ^. localConnections) $ tryCloseRemoteSocket + forM_ (vst ^. localConnections) tryCloseRemoteSocket forM_ (vst ^. internalThreads) killThread forM_ evs $ writeChan (localChannel ourEndPoint) where -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO () - tryCloseRemoteSocket theirEndPoint = do + tryCloseRemoteSocket theirEndPoint = -- We make an attempt to close the connection nicely (by sending a CloseSocket first) modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of @@ -560,7 +556,7 @@ connectToSelf ourEndPoint = do ourChan = localChannel ourEndPoint selfSend :: MVar Bool -> ConnectionId -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) - selfSend connAlive connId msg = do + selfSend connAlive connId msg = modifyMVar connAlive $ \alive -> if alive then do @@ -570,7 +566,7 @@ connectToSelf ourEndPoint = do return (alive, Left (TransportError SendFailed "Connection closed")) selfClose :: MVar Bool -> ConnectionId -> IO () - selfClose connAlive connId = do + selfClose connAlive connId = modifyMVar_ connAlive $ \alive -> do when alive $ writeChan ourChan (ConnectionClosed connId) return False @@ -585,23 +581,23 @@ internalSocketBetween :: TCPTransport -- ^ Transport -> EndPointAddress -- ^ Remote endpoint -> IO (Either String N.Socket) internalSocketBetween transport ourAddress theirAddress = runErrorT $ do - ourEndPoint <- ErrorT $ do + ourEndPoint <- ErrorT $ withMVar (transportState transport) $ \st -> case st of TransportClosed -> return . Left $ "Transport closed" - TransportValid vst -> do + TransportValid vst -> case vst ^. localEndPointAt ourAddress of Nothing -> return . Left $ "Local endpoint not found" Just ep -> return . Right $ ep - theirEndPoint <- ErrorT $ do + theirEndPoint <- ErrorT $ withMVar (localState ourEndPoint) $ \st -> case st of LocalEndPointClosed -> return . Left $ "Local endpoint closed" - LocalEndPointValid vst -> do + LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of Nothing -> return . Left $ "Remote endpoint not found" Just ep -> return . Right $ ep - ErrorT $ do + ErrorT $ withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointValid vst -> return . Right $ remoteSocket vst @@ -679,7 +675,7 @@ requestConnectionTo ourEndPoint theirAddress = go -- endpoint state, and might have changed in the meantime, these changes -- won't matter. case endPointStateSnapshot of - RemoteEndPointInvalid err -> do + RemoteEndPointInvalid err -> throw err RemoteEndPointClosing resolved _ -> @@ -687,7 +683,7 @@ requestConnectionTo ourEndPoint theirAddress = go -- this is resolved and we then try again readMVar resolved >> go - RemoteEndPointClosed -> do + RemoteEndPointClosed -> -- EndPointClosed indicates that a concurrent thread was in the -- process of closing the TCP connection to the remote endpoint when -- we obtained a reference to it. By INV-CLOSE we can assume that the @@ -705,7 +701,7 @@ requestConnectionTo ourEndPoint theirAddress = go case decodeInt32 . BS.concat $ reply of Nothing -> throw (connectFailed $ userError "Invalid integer") - Just cid -> do + Just cid -> return (theirEndPoint, cid) -- If this is a new endpoint, fork a thread to listen for incoming @@ -750,14 +746,14 @@ requestConnectionTo ourEndPoint theirAddress = go setupRemoteEndPoint :: EndPointPair -> IO () setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do didAccept <- bracketOnError (socketToEndPoint (localAddress ourEndPoint) (remoteAddress theirEndPoint)) - onError $ \result -> do + onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do let vst = ValidRemoteEndPointState { remoteSocket = sock , _remoteOutgoing = 0 , _remoteIncoming = IntSet.empty - , sendOn = \msg -> sendMany sock msg `onException` (tryIO $ N.sClose sock) + , sendOn = \msg -> sendMany sock msg `onException` tryIO (N.sClose sock) } putMVar theirState (RemoteEndPointValid vst) return True @@ -827,7 +823,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do Just r -> return (sock, r) where createSocket :: N.AddrInfo -> IO N.Socket - createSocket addr = mapExceptionIO insufficientResources $ do + createSocket addr = mapExceptionIO insufficientResources $ N.socket (N.addrFamily addr) N.Stream N.defaultProtocol invalidAddress, insufficientResources, failed :: IOException -> TransportError ConnectErrorCode @@ -839,7 +835,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do -- -- If the local endpoint is closed, do nothing removeRemoteEndPoint :: EndPointPair -> IO () -removeRemoteEndPoint (ourEndPoint, theirEndPoint) = do +removeRemoteEndPoint (ourEndPoint, theirEndPoint) = modifyMVar_ ourState $ \st -> case st of LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of @@ -859,7 +855,7 @@ removeRemoteEndPoint (ourEndPoint, theirEndPoint) = do -- -- Does nothing if the transport is closed removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () -removeLocalEndPoint transport ourEndPoint = do +removeLocalEndPoint transport ourEndPoint = modifyMVar_ (transportState transport) $ \st -> case st of TransportValid vst -> return (TransportValid . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) $ vst) @@ -971,7 +967,7 @@ modifyRemoteState :: EndPointPair -> ((MVar (), ValidRemoteEndPointState) -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointClosing -> IO (RemoteState, a) -- ^ Case for RemoteEndPointClosed -> IO a -modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = do +modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = mask $ \restore -> do st <- takeMVar theirState case st of @@ -981,7 +977,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing Right (st', a) -> do putMVar theirState st' return a - Left ex -> do + Left ex -> handleException ex vst -- The other cases are less interesting, because unless the endpoint is -- in Valid state we're not supposed to do any IO on it @@ -1058,13 +1054,12 @@ withRemoteState :: EndPointPair -- ^ Local and -> ((MVar (), ValidRemoteEndPointState) -> IO a) -- ^ Case for RemoteEndPointClosing -> IO a -- ^ Case for RemoteEndPointClosed -> IO a -withRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = do - modifyRemoteState (ourEndPoint, theirEndPoint) - (\err -> (,) (RemoteEndPointInvalid err) <$> caseInvalid err) - (\vst -> (,) (RemoteEndPointValid vst) <$> caseValid vst) - (\(resolved, vst) -> (,) (RemoteEndPointClosing resolved vst) <$> caseClosing (resolved, vst)) - ((,) RemoteEndPointClosed <$> caseClosed) - +withRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = + modifyRemoteState (ourEndPoint, theirEndPoint) + (\err -> (,) (RemoteEndPointInvalid err) <$> caseInvalid err) + (\vst -> (,) (RemoteEndPointValid vst) <$> caseValid vst) + (\(resolved, vst) -> (,) (RemoteEndPointClosing resolved vst) <$> caseClosing (resolved, vst)) + ((,) RemoteEndPointClosed <$> caseClosed) -------------------------------------------------------------------------------- -- Incoming requests -- @@ -1094,7 +1089,7 @@ handleConnectionRequest transport sock = handle tryCloseSocket $ do fail "Invalid endpoint" Just ourEndPoint -> return ourEndPoint - TransportClosed -> do + TransportClosed -> fail "Transport closed" void . forkEndPointThread ourEndPoint $ go ourEndPoint theirAddress where @@ -1182,7 +1177,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do then do readMessage sock connId go sock - else do + else case tryToEnum (fromIntegral connId) of Just RequestConnectionId -> do recvInt32 sock >>= createNewConnection @@ -1241,7 +1236,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do fail "Local endpoint closed" case mmvar of Nothing -> do - hPutStrLn stderr $ "Warning: Invalid request ID" + hPutStrLn stderr "Warning: Invalid request ID" return () -- Invalid request ID. TODO: We just ignore it? Just mvar -> putMVar mvar response @@ -1273,8 +1268,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- We regard a CloseSocket message as an (optimized) way for the -- remote endpoint to indicate that all its connections to us are -- now properly closed - forM_ (IntSet.elems $ vst ^. remoteIncoming) $ \cid -> - writeChan ourChannel (ConnectionClosed cid) + forM_ (IntSet.elems $ vst ^. remoteIncoming) $ + writeChan ourChannel . ConnectionClosed let vst' = remoteIncoming ^= IntSet.empty $ vst -- Check if we agree that the connection should be closed if vst' ^. remoteOutgoing == 0 From 4f5935a7e66d629c0acc93b04370f3ca66861106 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 23 May 2012 09:23:09 +0100 Subject: [PATCH 0071/2357] Use throwIO instead of throw --- src/Network/Transport/Chan.hs | 6 +++--- src/Network/Transport/Internal.hs | 4 ++-- src/Network/Transport/Internal/TCP.hs | 8 ++++---- src/Network/Transport/TCP.hs | 24 ++++++++++++++---------- tests/TestTCP.hs | 12 ++++++------ tests/TestTransport.hs | 1 - tests/Traced.hs | 8 ++++---- 7 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs index 85f20b3e..1c194e24 100644 --- a/src/Network/Transport/Chan.hs +++ b/src/Network/Transport/Chan.hs @@ -6,7 +6,7 @@ import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) import Control.Applicative ((<$>)) import Control.Category ((>>>)) import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_, readMVar) -import Control.Exception (throw) +import Control.Exception (throwIO) import Control.Monad (forM_, when) import Data.Map (Map) import qualified Data.Map as Map (empty, insert, size, delete, findWithDefault) @@ -34,7 +34,7 @@ createTransport = do , _multigroups = Map.empty } return Transport { newEndPoint = apiNewEndPoint state - , closeTransport = throw (userError "closeEndPoint not implemented") + , closeTransport = throwIO (userError "closeEndPoint not implemented") } -- | Create a new end point @@ -47,7 +47,7 @@ apiNewEndPoint state = do return . Right $ EndPoint { receive = readChan chan , address = addr , connect = apiConnect addr state - , closeEndPoint = throw (userError "closeEndPoint not implemented") + , closeEndPoint = throwIO (userError "closeEndPoint not implemented") , newMulticastGroup = apiNewMulticastGroup state addr , resolveMulticastGroup = apiResolveMulticastGroup state addr } diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index f79bff77..a34ab90a 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -22,7 +22,7 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length) import qualified Data.ByteString.Internal as BSI (unsafeCreate, toForeignPtr, inlinePerformIO) import Control.Monad.IO.Class (MonadIO, liftIO) -import Control.Exception (IOException, Exception, catch, try, throw) +import Control.Exception (IOException, Exception, catch, try, throwIO) --import Control.Concurrent (myThreadId) foreign import ccall unsafe "htonl" htonl :: CInt -> CInt @@ -66,7 +66,7 @@ prependLength bss = encodeInt32 (sum . map BS.length $ bss) : bss -- | Translate exceptions that arise in IO computations mapExceptionIO :: (Exception e1, Exception e2) => (e1 -> e2) -> IO a -> IO a -mapExceptionIO f p = catch p (throw . f) +mapExceptionIO f p = catch p (throwIO . f) -- | Like 'try', but lifted and specialized to IOExceptions tryIO :: MonadIO m => IO a -> m (Either IOException a) diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 68a9c260..1597a80f 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -27,7 +27,7 @@ import qualified Network.Socket as N ( HostName import qualified Network.Socket.ByteString as NBS (recv) import Control.Concurrent (ThreadId, forkIOWithUnmask) import Control.Monad (liftM, forever) -import Control.Exception (SomeException, catch, bracketOnError, throw, mask_) +import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length, concat, null) import Data.Int (Int32) @@ -84,7 +84,7 @@ recvInt32 :: Num a => N.Socket -> IO a recvInt32 sock = do mi <- liftM (decodeInt32 . BS.concat) $ recvExact sock 4 case mi of - Nothing -> throw (userError "Invalid integer") + Nothing -> throwIO (userError "Invalid integer") Just i -> return i -- | Read an exact number of bytes from a socket @@ -94,7 +94,7 @@ recvInt32 sock = do recvExact :: N.Socket -- ^ Socket to read from -> Int32 -- ^ Number of bytes to read -> IO [ByteString] -recvExact _ len | len <= 0 = throw (userError "Negative length") +recvExact _ len | len <= 0 = throwIO (userError "Negative length") recvExact sock len = go [] len where go :: [ByteString] -> Int32 -> IO [ByteString] @@ -102,5 +102,5 @@ recvExact sock len = go [] len go acc l = do bs <- NBS.recv sock (fromIntegral l `min` 4096) if BS.null bs - then throw (userError "Socket closed") + then throwIO (userError "Socket closed") else go (bs : acc) (l - fromIntegral (BS.length bs)) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 823a82a5..f12d69d8 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -80,7 +80,7 @@ import Control.Monad.Error (ErrorT(..), runErrorT) import Control.Exception ( IOException , SomeException , handle - , throw + , throwIO , try , bracketOnError , mask @@ -475,6 +475,10 @@ apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do -- GUARANTEE: The state of the remote endpoint will not be changed. apiSend :: EndPointPair -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = + -- The 'join' joins the inner exception that we explicitly return) for + -- instance if the connection is closed) with the outer exception (which is + -- returned by 'try' when an exception is thrown by 'sendOn', and handled + -- by 'withRemoteState') join <$> (try . mapExceptionIO sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) -- RemoteEndPointInvalid (\_ -> fail "apiSend RELY violation") @@ -636,7 +640,7 @@ createLocalEndPoint transport = do } return (TransportValid . (localEndPointAt addr ^= Just localEndPoint) . (nextEndPointId ^= ix + 1) $ vst, localEndPoint) TransportClosed -> - throw (TransportError NewEndPointFailed "Transport closed") + throwIO (TransportError NewEndPointFailed "Transport closed") -- | Request a connection to a remote endpoint -- @@ -676,7 +680,7 @@ requestConnectionTo ourEndPoint theirAddress = go -- won't matter. case endPointStateSnapshot of RemoteEndPointInvalid err -> - throw err + throwIO err RemoteEndPointClosing resolved _ -> -- If the remote endpoint is closing, then we need to block until @@ -700,7 +704,7 @@ requestConnectionTo ourEndPoint theirAddress = go reply <- mapExceptionIO connectFailed $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId case decodeInt32 . BS.concat $ reply of Nothing -> - throw (connectFailed $ userError "Invalid integer") + throwIO (connectFailed $ userError "Invalid integer") Just cid -> return (theirEndPoint, cid) @@ -712,7 +716,7 @@ requestConnectionTo ourEndPoint theirAddress = go findTheirEndPoint = do (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of LocalEndPointClosed -> - throw (TransportError ConnectFailed "Local endpoint closed") + throwIO (TransportError ConnectFailed "Local endpoint closed") LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of Just theirEndPoint -> @@ -809,7 +813,7 @@ socketToEndPoint :: EndPointAddress -- ^ Our address -> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse)) socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of - Nothing -> throw (failed . userError $ "Could not parse") + Nothing -> throwIO (failed . userError $ "Could not parse") Just dec -> return dec addr:_ <- mapExceptionIO invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port) bracketOnError (createSocket addr) N.sClose $ \sock -> do @@ -819,7 +823,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) recvInt32 sock case tryToEnum response of - Nothing -> throw (failed . userError $ "Unexpected response") + Nothing -> throwIO (failed . userError $ "Unexpected response") Just r -> return (sock, r) where createSocket :: N.AddrInfo -> IO N.Socket @@ -892,7 +896,7 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do let reqId = vst ^. nextCtrlRequestId return (LocalEndPointValid . (nextCtrlRequestId ^: (+ 1)) . (pendingCtrlRequestsAt reqId ^= Just reply) $ vst, reqId) LocalEndPointClosed -> - throw (userError "Local endpoint closed") + throwIO (userError "Local endpoint closed") withRemoteState (ourEndPoint, theirEndPoint) -- RemoteEndPointInvalid (\_ -> fail "doRemoteRequest RELY violation") @@ -901,7 +905,7 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do -- RemoteEndPointClosing (\_ -> fail "doRemoteRequest RELY violation") -- RemoteEndPointClosed - (throw (userError "Remote endpoint closed")) + (throwIO (userError "Remote endpoint closed")) takeMVar reply -- | Check if the remote endpoint is unused, and if so, send a CloseSocket request @@ -1012,7 +1016,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing let err = TransportError (EventConnectionLost (remoteAddress theirEndPoint) incoming) (show ex) writeChan (localChannel ourEndPoint) $ ErrorEvent err -- .. and finally rethrow the exception - throw ex + throwIO ex -- Returns the set of incoming connections if we closed the connection, or -- 'Nothing' if the endpoint was already closed diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 36fe5a35..040e9f5e 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -24,7 +24,7 @@ import Control.Concurrent.MVar ( MVar ) import Control.Monad (replicateM, guard, forM_, replicateM_, when) import Control.Applicative ((<$>)) -import Control.Exception (throw, try, SomeException) +import Control.Exception (throwIO, try, SomeException) import Network.Transport.TCP ( ControlHeader(..) , ConnectionRequestResponse(..) , socketToEndPoint @@ -135,7 +135,7 @@ testEarlyDisconnect nextPort = do putMVar clientAddr ourAddress -- Listen for incoming messages - forkServer "127.0.0.1" clientPort 5 throw $ \sock -> do + forkServer "127.0.0.1" clientPort 5 throwIO $ \sock -> do -- Initial setup 0 <- recvInt32 sock :: IO Int _ <- recvWithLength sock @@ -246,7 +246,7 @@ testEarlyCloseSocket nextPort = do putMVar clientAddr ourAddress -- Listen for incoming messages - forkServer "127.0.0.1" clientPort 5 throw $ \sock -> do + forkServer "127.0.0.1" clientPort 5 throwIO $ \sock -> do -- Initial setup 0 <- recvInt32 sock :: IO Int _ <- recvWithLength sock @@ -509,7 +509,7 @@ testMany nextPort = do case (ioe_errno ex) of Just no | Errno no == eADDRNOTAVAIL -> putStrLn "(ADDRNOTAVAIL)" _ -> return () - throw ex + throwIO ex Right transport -> replicateM_ 3 $ do Right endpoint <- newEndPoint transport @@ -549,7 +549,7 @@ testReconnect nextPort = do counter <- newMVar (0 :: Int) - forkServer "127.0.0.1" serverPort 5 throw $ \sock -> do + forkServer "127.0.0.1" serverPort 5 throwIO $ \sock -> do -- Accept the connection Right 0 <- tryIO $ (recvInt32 sock :: IO Int) Right _ <- tryIO $ recvWithLength sock @@ -617,7 +617,7 @@ testUnidirectionalError nextPort = do serverGotPing <- newEmptyMVar -- Server - forkServer "127.0.0.1" serverPort 5 throw $ \sock -> do + forkServer "127.0.0.1" serverPort 5 throwIO $ \sock -> do -- We accept connections, but when an exception occurs we don't do -- anything (in particular, we don't close the socket). This is important -- because when we shutdown one direction of the socket a recv here will diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index d0e8bf91..ffa926b9 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -16,7 +16,6 @@ import Data.Map (Map) import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) import Data.String (fromString) import Traced -import Control.Concurrent (threadDelay) -- | We overload connect to always pass the default hints connect :: EndPoint -> EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) diff --git a/tests/Traced.hs b/tests/Traced.hs index 6f839003..b2d6fc09 100644 --- a/tests/Traced.hs +++ b/tests/Traced.hs @@ -52,7 +52,7 @@ module Traced ( MonadS(..) import Prelude hiding ((>>=), return, fail, catch, (>>)) import qualified Prelude -import Control.Exception (catches, Handler(..), SomeException, throw, Exception(..), IOException) +import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) import Control.Applicative ((<$>)) import Data.Typeable (Typeable) import Data.Maybe (catMaybes) @@ -180,9 +180,9 @@ instance Show TracedException where traceHandlers :: Traceable a => a -> [Handler b] traceHandlers a = case trace a of - Nothing -> [ Handler $ \ex -> throw (ex :: SomeException) ] - Just t -> [ Handler $ \(TracedException ts ex) -> throw $ TracedException (show t : ts) ex - , Handler $ \ex -> throw $ TracedException [show t] (ex :: SomeException) + Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] + Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex + , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) ] -- | Definition of 'ifThenElse' for use with RebindableSyntax From 5bc45cc71bb5df34214eacae33208b8a10d55f15 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 23 May 2012 12:45:11 +0100 Subject: [PATCH 0072/2357] Code cleanup --- src/Network/Transport/Internal.hs | 6 +- src/Network/Transport/Internal/TCP.hs | 13 +- src/Network/Transport/TCP.hs | 664 +++++++++++++++----------- 3 files changed, 389 insertions(+), 294 deletions(-) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index a34ab90a..34f755f9 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -6,7 +6,7 @@ module Network.Transport.Internal ( -- * Encoders/decoders , decodeInt16 , prependLength -- * Miscellaneous abstractions - , mapExceptionIO + , mapIOException , tryIO , tryToEnum , void @@ -65,8 +65,8 @@ prependLength :: [ByteString] -> [ByteString] prependLength bss = encodeInt32 (sum . map BS.length $ bss) : bss -- | Translate exceptions that arise in IO computations -mapExceptionIO :: (Exception e1, Exception e2) => (e1 -> e2) -> IO a -> IO a -mapExceptionIO f p = catch p (throwIO . f) +mapIOException :: Exception e => (IOException -> e) -> IO a -> IO a +mapIOException f p = catch p (throwIO . f) -- | Like 'try', but lifted and specialized to IOExceptions tryIO :: MonadIO m => IO a -> m (Either IOException a) diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 1597a80f..18147f3c 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -3,10 +3,11 @@ module Network.Transport.Internal.TCP ( forkServer , recvWithLength , recvExact , recvInt32 + , tryCloseSocket ) where import Prelude hiding (catch) -import Network.Transport.Internal (decodeInt32) +import Network.Transport.Internal (decodeInt32, void, tryIO) import qualified Network.Socket as N ( HostName , ServiceName , Socket @@ -61,18 +62,18 @@ forkServer host port backlog terminationHandler requestHandler = do -- the "best" address first, whatever that means addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just host) (Just port) bracketOnError (N.socket (N.addrFamily addr) N.Stream N.defaultProtocol) - N.sClose $ \sock -> do + tryCloseSocket $ \sock -> do N.setSocketOption sock N.ReuseAddr 1 N.bindSocket sock (N.addrAddress addr) N.listen sock backlog mask_ $ forkIOWithUnmask $ \unmask -> catch (unmask (forever $ acceptRequest sock)) $ \ex -> do - N.sClose sock + tryCloseSocket sock terminationHandler ex where acceptRequest :: N.Socket -> IO () acceptRequest sock = bracketOnError (N.accept sock) - (N.sClose . fst) + (tryCloseSocket . fst) (requestHandler . fst) -- | Read a length and then a payload of that length @@ -87,6 +88,10 @@ recvInt32 sock = do Nothing -> throwIO (userError "Invalid integer") Just i -> return i +-- | Close a socket, ignoring I/O exceptions +tryCloseSocket :: N.Socket -> IO () +tryCloseSocket = void . tryIO . N.sClose + -- | Read an exact number of bytes from a socket -- -- Throws an I/O exception if the socket closes before the specified diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index f12d69d8..dce42e86 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -34,11 +34,12 @@ import Network.Transport import Network.Transport.Internal.TCP ( forkServer , recvWithLength , recvInt32 + , tryCloseSocket ) import Network.Transport.Internal ( encodeInt32 , decodeInt32 , prependLength - , mapExceptionIO + , mapIOException , tryIO , tryToEnum , void @@ -46,7 +47,6 @@ import Network.Transport.Internal ( encodeInt32 import qualified Network.Socket as N ( HostName , ServiceName , Socket - , sClose , getAddrInfo , socket , addrFamily @@ -75,7 +75,7 @@ import Control.Concurrent.MVar ( MVar ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) -import Control.Monad (when, unless, join) +import Control.Monad (when, unless, join, liftM) import Control.Monad.Error (ErrorT(..), runErrorT) import Control.Exception ( IOException , SomeException @@ -95,13 +95,18 @@ import Data.Int (Int32) import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap (empty) import Data.IntSet (IntSet) -import qualified Data.IntSet as IntSet (empty, insert, elems, singleton, null, delete) +import qualified Data.IntSet as IntSet ( empty + , insert + , elems + , singleton + , null + , delete + ) import Data.Map (Map) import qualified Data.Map as Map (empty) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) import Data.Foldable (forM_, mapM_) -import System.IO (hPutStrLn, stderr) -- $design -- @@ -236,12 +241,12 @@ data LocalEndPointState = | LocalEndPointClosed data ValidLocalEndPointState = ValidLocalEndPointState - { _nextConnectionId :: ConnectionId + { _nextConnectionId :: !ConnectionId , _pendingCtrlRequests :: IntMap (MVar [ByteString]) - , _nextCtrlRequestId :: ControlRequestId + , _nextCtrlRequestId :: !ControlRequestId , _localConnections :: Map EndPointAddress RemoteEndPoint , _internalThreads :: [ThreadId] - , _nextRemoteId :: Int + , _nextRemoteId :: !Int } -- A remote endpoint has incoming and outgoing connections, and when the total @@ -329,17 +334,24 @@ type EndPointPair = (LocalEndPoint, RemoteEndPoint) -- | Control headers data ControlHeader = - RequestConnectionId -- ^ Request a new connection ID from the remote endpoint - | CloseConnection -- ^ Tell the remote endpoint we will no longer be using a connection - | ControlResponse -- ^ Respond to a control request /from/ the remote endpoint - | CloseSocket -- ^ Request to close the connection (see module description) + -- | Request a new connection ID from the remote endpoint + RequestConnectionId + -- | Tell the remote endpoint we will no longer be using a connection + | CloseConnection + -- | Respond to a control request /from/ the remote endpoint + | ControlResponse + -- | Request to close the connection (see module description) + | CloseSocket deriving (Enum, Bounded, Show) -- Response sent by /B/ to /A/ when /A/ tries to connect data ConnectionRequestResponse = - ConnectionRequestAccepted -- ^ /B/ accepts the connection - | ConnectionRequestEndPointInvalid -- ^ /A/ requested an invalid endpoint - | ConnectionRequestCrossed -- ^ /A/s request crossed with a request from /B/ (see protocols) + -- | /B/ accepts the connection + ConnectionRequestAccepted + -- | /A/ requested an invalid endpoint + | ConnectionRequestEndPointInvalid + -- | /A/s request crossed with a request from /B/ (see protocols) + | ConnectionRequestCrossed deriving (Enum, Bounded, Show) -- Internal functionality we expose for unit testing @@ -347,7 +359,9 @@ data TransportInternals = TransportInternals { -- | The ID of the thread that listens for new incoming connections transportThread :: ThreadId -- | Find the socket between a local and a remote endpoint - , socketBetween :: EndPointAddress -> EndPointAddress -> IO (Either String N.Socket) + , socketBetween :: EndPointAddress + -> EndPointAddress + -> IO (Either String N.Socket) } -------------------------------------------------------------------------------- @@ -357,12 +371,17 @@ data TransportInternals = TransportInternals -- | Create a TCP transport -- -- TODOs: deal with hints -createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) +createTransport :: N.HostName + -> N.ServiceName + -> IO (Either IOException Transport) createTransport host port = either Left (Right . fst) <$> createTransportExposeInternals host port -- | You should probably not use this function (used for unit testing only) -createTransportExposeInternals :: N.HostName -> N.ServiceName -> IO (Either IOException (Transport, TransportInternals)) +createTransportExposeInternals + :: N.HostName + -> N.ServiceName + -> IO (Either IOException (Transport, TransportInternals)) createTransportExposeInternals host port = do state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty @@ -373,19 +392,20 @@ createTransportExposeInternals host port = do , transportPort = port } tryIO $ do - -- For a discussion of the use of N.sOMAXCONN, see - -- http://tangentsoft.net/wskfaq/advanced.html#backlog - -- http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/023/2333/2333s2.html - tid <- forkServer host port N.sOMAXCONN (terminationHandler transport) (handleConnectionRequest transport) - return ( Transport - { newEndPoint = apiNewEndPoint transport - , closeTransport = apiCloseTransport transport (Just tid) [EndPointClosed] - } - , TransportInternals - { transportThread = tid - , socketBetween = internalSocketBetween transport - } - ) + tid <- forkServer host port N.sOMAXCONN + (terminationHandler transport) + (handleConnectionRequest transport) + return + ( Transport + { newEndPoint = apiNewEndPoint transport + , closeTransport = let evs = [EndPointClosed] in + apiCloseTransport transport (Just tid) evs + } + , TransportInternals + { transportThread = tid + , socketBetween = internalSocketBetween transport + } + ) where terminationHandler :: TCPTransport -> SomeException -> IO () terminationHandler transport ex = do @@ -409,7 +429,8 @@ apiCloseTransport transport mTransportThread evs = do forM_ mTransportThread killThread -- | Create a new endpoint -apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) +apiNewEndPoint :: TCPTransport + -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) apiNewEndPoint transport = try $ do ourEndPoint <- createLocalEndPoint transport return EndPoint @@ -421,8 +442,10 @@ apiNewEndPoint transport = try $ do , resolveMulticastGroup = return . Left . const resolveMulticastGroupError } where - newMulticastGroupError = TransportError NewMulticastGroupUnsupported "TCP does not support multicast" - resolveMulticastGroupError = TransportError ResolveMulticastGroupUnsupported "TCP does not support multicast" + newMulticastGroupError = + TransportError NewMulticastGroupUnsupported "Multicast not supported" + resolveMulticastGroupError = + TransportError ResolveMulticastGroupUnsupported "Multicast not supported" -- | Connnect to an endpoint apiConnect :: LocalEndPoint -- ^ Local end point @@ -430,78 +453,80 @@ apiConnect :: LocalEndPoint -- ^ Local end point -> Reliability -- ^ Reliability (ignored) -> ConnectHints -- ^ Hints (ignored for now) -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect ourEndPoint theirAddress _reliability _hints | localAddress ourEndPoint == theirAddress = - connectToSelf ourEndPoint -apiConnect ourEndPoint theirAddress _reliability _hints = try $ do - (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress - -- connAlive can be an IORef rather than an MVar because it is protected by - -- the remoteState MVar. We don't need the overhead of locking twice. - connAlive <- newIORef True - return Connection - { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive - , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive - } +apiConnect ourEndPoint theirAddress _reliability _hints = + if localAddress ourEndPoint == theirAddress + then connectToSelf ourEndPoint + else try $ do + (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress + -- connAlive can be an IORef rather than an MVar because it is protected + -- by the remoteState MVar. We don't need the overhead of locking twice. + connAlive <- newIORef True + return Connection + { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive + , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive + } -- | Close a connection -- -- RELY: The endpoint must not be invalid -- GUARANTEE: If the connection is alive on entry then the remote endpoint will --- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the state --- of the remote endpoint will not be changed. +-- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the +-- state of the remote endpoint will not be changed. apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do - modifyRemoteState_ (ourEndPoint, theirEndPoint) - -- RemoteEndPointInvalid - (\_ -> fail "apiClose RELY violation") - -- RemoteEndPointValid - (\vst -> do - alive <- readIORef connAlive - if alive - then do - writeIORef connAlive False - sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] - return (RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ vst) - else - return (RemoteEndPointValid vst)) - -- RemoteEndPointClosing - (return . uncurry RemoteEndPointClosing) - -- RemoteEndPoinClosed - (return RemoteEndPointClosed) + modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity + { caseValid = \vst -> do + alive <- readIORef connAlive + if alive + then do + writeIORef connAlive False + sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] + return ( RemoteEndPointValid + . (remoteOutgoing ^: (\x -> x - 1)) + $ vst + ) + else + return (RemoteEndPointValid vst) + } closeIfUnused (ourEndPoint, theirEndPoint) -- | Send data across a connection -- -- RELY: The endpoint must not be in invalid state. -- GUARANTEE: The state of the remote endpoint will not be changed. -apiSend :: EndPointPair -> ConnectionId -> IORef Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) +apiSend :: EndPointPair -- ^ Local and remote endpoint + -> ConnectionId -- ^ Connection ID (supplied by remote endpoint) + -> IORef Bool -- ^ Is the connection still alive? + -> [ByteString] -- ^ Payload + -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = -- The 'join' joins the inner exception that we explicitly return) for -- instance if the connection is closed) with the outer exception (which is -- returned by 'try' when an exception is thrown by 'sendOn', and handled -- by 'withRemoteState') - join <$> (try . mapExceptionIO sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) - -- RemoteEndPointInvalid - (\_ -> fail "apiSend RELY violation") - -- RemoteEndPointValid - (\vst -> do - alive <- readIORef connAlive - if alive - then do - sendOn vst (encodeInt32 connId : prependLength payload) - return . Right $ () - else - return . Left $ TransportError SendClosed "Connection closed") - -- RemoteEndPointClosing - (return . Left . const (TransportError SendClosed "Connection lost")) - -- RemoteEndPointClosed - (return . Left $ TransportError SendClosed "Connection lost")) + liftM join . try . mapIOException sendFailed $ + withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch + { caseInvalid = \_ -> + fail "apiSend RELY violation" + , caseValid = \vst -> do + alive <- readIORef connAlive + if alive + then do + sendOn vst (encodeInt32 connId : prependLength payload) + return . Right $ () + else + return . Left $ TransportError SendClosed "Connection closed" + , caseClosing = \_ _ -> + return . Left $ TransportError SendClosed "Connection lost" + , caseClosed = + return . Left $ TransportError SendClosed "Connection lost" + } where - sendFailed :: IOException -> TransportError SendErrorCode sendFailed = TransportError SendFailed . show -- | Force-close the endpoint apiCloseEndPoint :: TCPTransport -- ^ Transport - -> [Event] -- ^ Events to output on the endpoint to indicate closure + -> [Event] -- ^ Events used to report closure -> LocalEndPoint -- ^ Local endpoint -> IO () apiCloseEndPoint transport evs ourEndPoint = do @@ -523,7 +548,8 @@ apiCloseEndPoint transport evs ourEndPoint = do -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO () tryCloseRemoteSocket theirEndPoint = - -- We make an attempt to close the connection nicely (by sending a CloseSocket first) + -- We make an attempt to close the connection nicely + -- (by sending a CloseSocket first) modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> @@ -532,17 +558,17 @@ apiCloseEndPoint transport evs ourEndPoint = do -- Try to send a CloseSocket request tryIO $ sendOn conn [encodeInt32 CloseSocket] -- .. but even if it fails, close the socket anyway - -- (hence, two separate calls to tryIO) - tryIO $ N.sClose (remoteSocket conn) + tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed RemoteEndPointClosing _ conn -> do - tryIO $ N.sClose (remoteSocket conn) + tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed RemoteEndPointClosed -> return RemoteEndPointClosed -- | Special case of 'apiConnect': connect an endpoint to itself -connectToSelf :: LocalEndPoint -> IO (Either (TransportError ConnectErrorCode) Connection) +connectToSelf :: LocalEndPoint + -> IO (Either (TransportError ConnectErrorCode) Connection) connectToSelf ourEndPoint = do -- Here connAlive must an MVar because it is not protected by another lock connAlive <- newMVar True @@ -551,15 +577,20 @@ connectToSelf ourEndPoint = do Left err -> return . Left $ TransportError ConnectNotFound (show err) Right connId -> do - writeChan ourChan (ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint)) - return . Right $ Connection { send = selfSend connAlive connId - , close = selfClose connAlive connId - } + writeChan ourChan $ + ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) + return . Right $ Connection + { send = selfSend connAlive connId + , close = selfClose connAlive connId + } where ourChan :: Chan Event ourChan = localChannel ourEndPoint - selfSend :: MVar Bool -> ConnectionId -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) + selfSend :: MVar Bool + -> ConnectionId + -> [ByteString] + -> IO (Either (TransportError SendErrorCode) ()) selfSend connAlive connId msg = modifyMVar connAlive $ \alive -> if alive @@ -605,8 +636,8 @@ internalSocketBetween transport ourAddress theirAddress = runErrorT $ do withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointValid vst -> return . Right $ remoteSocket vst - RemoteEndPointClosing _ _ -> - return . Left $ "Remote endpoint closing" + RemoteEndPointClosing _ vst -> + return . Right $ remoteSocket vst RemoteEndPointClosed -> return . Left $ "Remote endpoint closed" RemoteEndPointInvalid _ -> @@ -618,7 +649,8 @@ internalSocketBetween transport ourAddress theirAddress = runErrorT $ do -- | Create a new local endpoint -- --- May throw a TransportError NewEndPointErrorCode exception if the transport is closed. +-- May throw a TransportError NewEndPointErrorCode exception if the transport +-- is closed. createLocalEndPoint :: TCPTransport -> IO LocalEndPoint createLocalEndPoint transport = do chan <- newChan @@ -633,12 +665,19 @@ createLocalEndPoint transport = do modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> do let ix = vst ^. nextEndPointId - let addr = encodeEndPointAddress (transportHost transport) (transportPort transport) ix + let addr = encodeEndPointAddress (transportHost transport) + (transportPort transport) + ix let localEndPoint = LocalEndPoint { localAddress = addr , localChannel = chan , localState = state } - return (TransportValid . (localEndPointAt addr ^= Just localEndPoint) . (nextEndPointId ^= ix + 1) $ vst, localEndPoint) + return ( TransportValid + . (localEndPointAt addr ^= Just localEndPoint) + . (nextEndPointId ^= ix + 1) + $ vst + , localEndPoint + ) TransportClosed -> throwIO (TransportError NewEndPointFailed "Transport closed") @@ -656,13 +695,18 @@ requestConnectionTo ourEndPoint theirAddress = go where go = do -- Find the remote endpoint (create it if it doesn't yet exist) - theirEndPoint <- findTheirEndPoint + (theirEndPoint, isNew) <- mapIOException connectFailed $ + findRemoteEndPoint ourEndPoint theirAddress + + -- If it was new, start listening for incoming messages, too + when isNew . void . forkEndPointThread ourEndPoint $ + setupRemoteEndPoint (ourEndPoint, theirEndPoint) + let theirState = remoteState theirEndPoint - -- Before we initiate the new connection request we want to make sure that - -- refcount on the endpoint is incremented so that a concurrent thread will - -- not close the connection. Note that if IF we return RemoteEndPointValid - -- here then we can rely on the endpoint remaining in that state. + -- Before we initiate the new connection request we want to make sure + -- that refcount on the endpoint is incremented so that a concurrent + -- thread will not close the connection. endPointStateSnapshot <- modifyMVar theirState $ \st -> case st of RemoteEndPointValid ep -> @@ -670,10 +714,10 @@ requestConnectionTo ourEndPoint theirAddress = go _ -> return (st, st) - -- From this point on we are guaranteed the refcount is positive, provided - -- that the endpoint was valid. We still need to deal with the case where - -- it was not valid, however, which we didn't want to do while holding the - -- endpoint lock. + -- From this point on we are guaranteed the refcount is positive, + -- provided that the endpoint was valid. We still need to deal with the + -- case where it was not valid, however, which we didn't want to do while + -- holding the endpoint lock. -- -- Although 'endPointStateSnapshot' here refers to a snapshot of the -- endpoint state, and might have changed in the meantime, these changes @@ -701,47 +745,45 @@ requestConnectionTo ourEndPoint theirAddress = go -- point). Note that doRemoteRequest may throw an error if the send -- fails, and if it does, it will have put the remote endpoint in -- closed state. - reply <- mapExceptionIO connectFailed $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId case decodeInt32 . BS.concat $ reply of Nothing -> throwIO (connectFailed $ userError "Invalid integer") Just cid -> return (theirEndPoint, cid) - -- If this is a new endpoint, fork a thread to listen for incoming - -- connections. We don't want to do this while we hold the lock, because - -- forkEndPointThread modifies the local state too (to record the thread - -- ID) - findTheirEndPoint :: IO RemoteEndPoint - findTheirEndPoint = do - (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of - LocalEndPointClosed -> - throwIO (TransportError ConnectFailed "Local endpoint closed") - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Just theirEndPoint -> - return (st, (theirEndPoint, False)) - Nothing -> do - theirState <- newEmptyMVar - let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId - } - return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) . (nextRemoteId ^: (+ 1)) $ vst, (theirEndPoint, True)) - - -- The only way for forkEndPointThread to fail is if the local endpoint - -- gets closed. This error will be caught elsewhere, so we ignore it - -- here. - when isNew . void . forkEndPointThread ourEndPoint $ - setupRemoteEndPoint (ourEndPoint, theirEndPoint) - return theirEndPoint - - ourState :: MVar LocalEndPointState - ourState = localState ourEndPoint - - connectFailed :: IOException -> TransportError ConnectErrorCode connectFailed = TransportError ConnectFailed . show +-- | Find a remote endpoint. Create an uninitialized remote endpoint if the +-- remote endpoint did not yet exist. +-- +-- Throws an IOException if the local endpoint is closed. +findRemoteEndPoint :: LocalEndPoint -- ^ Our endpoint + -> EndPointAddress -- ^ Their address + -> IO (RemoteEndPoint, Bool) -- ^ Remote endpoint, new? +findRemoteEndPoint ourEndPoint theirAddress = + modifyMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Just theirEndPoint -> + return (st, (theirEndPoint, False)) + Nothing -> do + theirState <- newEmptyMVar + let theirEndPoint = RemoteEndPoint + { remoteAddress = theirAddress + , remoteState = theirState + , remoteId = vst ^. nextRemoteId + } + return ( LocalEndPointValid + . (localConnectionTo theirAddress ^= Just theirEndPoint) + . (nextRemoteId ^: (+ 1)) + $ vst + , (theirEndPoint, True) + ) + -- | Set up a remote endpoint -- -- RELY: The state of the remote endpoint must be uninitialized. @@ -749,7 +791,7 @@ requestConnectionTo ourEndPoint theirAddress = go -- RemoteEndPointInvalid. setupRemoteEndPoint :: EndPointPair -> IO () setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do - didAccept <- bracketOnError (socketToEndPoint (localAddress ourEndPoint) (remoteAddress theirEndPoint)) + didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress) onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do @@ -757,7 +799,8 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do { remoteSocket = sock , _remoteOutgoing = 0 , _remoteIncoming = IntSet.empty - , sendOn = \msg -> sendMany sock msg `onException` tryIO (N.sClose sock) + , sendOn = (`onException` tryCloseSocket sock) + . sendMany sock } putMVar theirState (RemoteEndPointValid vst) return True @@ -768,11 +811,12 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do -- RemoteEndPointInvalid; subsequent threads will initiate a new -- connection requests. removeRemoteEndPoint (ourEndPoint, theirEndPoint) - putMVar theirState (RemoteEndPointInvalid (invalidAddress "Invalid endpoint")) - N.sClose sock + let err = invalidAddress "Invalid endpoint" + putMVar theirState (RemoteEndPointInvalid err) + tryCloseSocket sock return False Right (sock, ConnectionRequestCrossed) -> do - N.sClose sock + tryCloseSocket sock return False Left err -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- See comment above @@ -780,10 +824,11 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do return False -- If we get to this point without an exception, then - -- * if didAccept is False the socket has already been closed - -- * if didAccept is True, the socket has been stored as part of the remote - -- state so we no longer need to worry about closing it when an - -- asynchronous exception occurs + -- + -- if didAccept is False the socket has already been closed + -- if didAccept is True, the socket has been stored as part of the remote + -- state so we no longer need to worry about closing it when an + -- asynchronous exception occurs when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) where -- If an asynchronous exception occurs while we set up the remote endpoint @@ -792,34 +837,42 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do -- that are blocked on reading the remote state are unblocked. It is -- possible, however, that the exception occurred after we already -- initialized the remote state, which is why we use tryPutMVar here. - onError :: Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse) -> IO () + onError :: Either (TransportError ConnectErrorCode) + (N.Socket, ConnectionRequestResponse) + -> IO () onError result = do removeRemoteEndPoint (ourEndPoint, theirEndPoint) case result of Left err -> do - tryPutMVar theirState (RemoteEndPointInvalid (TransportError ConnectFailed (show err))) + tryPutMVar theirState (RemoteEndPointInvalid err) return () Right (sock, _) -> do - tryPutMVar theirState (RemoteEndPointInvalid (TransportError ConnectFailed "setupRemoteEndPoint failed")) - tryIO $ N.sClose sock + let err = failed "setupRemoteEndPoint failed" + tryPutMVar theirState (RemoteEndPointInvalid err) + tryCloseSocket sock return () + failed = TransportError ConnectFailed + ourAddress = localAddress ourEndPoint + theirAddress = remoteAddress theirEndPoint theirState = remoteState theirEndPoint invalidAddress = TransportError ConnectNotFound -- | Establish a connection to a remote endpoint socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address - -> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse)) + -> IO (Either (TransportError ConnectErrorCode) + (N.Socket, ConnectionRequestResponse)) socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of Nothing -> throwIO (failed . userError $ "Could not parse") Just dec -> return dec - addr:_ <- mapExceptionIO invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port) - bracketOnError (createSocket addr) N.sClose $ \sock -> do - mapExceptionIO failed $ N.setSocketOption sock N.ReuseAddr 1 - mapExceptionIO invalidAddress $ N.connect sock (N.addrAddress addr) - response <- mapExceptionIO failed $ do + addr:_ <- mapIOException invalidAddress $ + N.getAddrInfo Nothing (Just host) (Just port) + bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do + mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 + mapIOException invalidAddress $ N.connect sock (N.addrAddress addr) + response <- mapIOException failed $ do sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) recvInt32 sock case tryToEnum response of @@ -827,10 +880,9 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do Just r -> return (sock, r) where createSocket :: N.AddrInfo -> IO N.Socket - createSocket addr = mapExceptionIO insufficientResources $ + createSocket addr = mapIOException insufficientResources $ N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - invalidAddress, insufficientResources, failed :: IOException -> TransportError ConnectErrorCode invalidAddress = TransportError ConnectNotFound . show insufficientResources = TransportError ConnectInsufficientResources . show failed = TransportError ConnectFailed . show @@ -847,7 +899,11 @@ removeRemoteEndPoint (ourEndPoint, theirEndPoint) = return st Just remoteEndPoint' -> if remoteId remoteEndPoint' == remoteId theirEndPoint - then return (LocalEndPointValid . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) $ vst) + then return + ( LocalEndPointValid + . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) + $ vst + ) else return st LocalEndPointClosed -> return LocalEndPointClosed @@ -862,18 +918,26 @@ removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () removeLocalEndPoint transport ourEndPoint = modifyMVar_ (transportState transport) $ \st -> case st of TransportValid vst -> - return (TransportValid . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) $ vst) + return ( TransportValid + . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) + $ vst + ) TransportClosed -> return TransportClosed -- | Encode end point address -encodeEndPointAddress :: N.HostName -> N.ServiceName -> EndPointId -> EndPointAddress +encodeEndPointAddress :: N.HostName + -> N.ServiceName + -> EndPointId + -> EndPointAddress encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ host ++ ":" ++ port ++ ":" ++ show ix -- | Decode end point address -decodeEndPointAddress :: EndPointAddress -> Maybe (N.HostName, N.ServiceName, EndPointId) -decodeEndPointAddress (EndPointAddress bs) = case map BSC.unpack $ BSC.split ':' bs of +decodeEndPointAddress :: EndPointAddress + -> Maybe (N.HostName, N.ServiceName, EndPointId) +decodeEndPointAddress (EndPointAddress bs) = + case map BSC.unpack $ BSC.split ':' bs of [host, port, endPointIdStr] -> case reads endPointIdStr of [(endPointId, "")] -> Just (host, port, endPointId) @@ -894,38 +958,35 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do reqId <- modifyMVar (localState ourEndPoint) $ \st -> case st of LocalEndPointValid vst -> do let reqId = vst ^. nextCtrlRequestId - return (LocalEndPointValid . (nextCtrlRequestId ^: (+ 1)) . (pendingCtrlRequestsAt reqId ^= Just reply) $ vst, reqId) + return ( LocalEndPointValid + . (nextCtrlRequestId ^: (+ 1)) + . (pendingCtrlRequestsAt reqId ^= Just reply) + $ vst + , reqId + ) LocalEndPointClosed -> throwIO (userError "Local endpoint closed") - withRemoteState (ourEndPoint, theirEndPoint) - -- RemoteEndPointInvalid - (\_ -> fail "doRemoteRequest RELY violation") - -- RemoteEndPointValid - (\vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId]) - -- RemoteEndPointClosing - (\_ -> fail "doRemoteRequest RELY violation") - -- RemoteEndPointClosed - (throwIO (userError "Remote endpoint closed")) + withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch + { caseInvalid = \_ -> fail "doRemoteRequest RELY violation" + , caseValid = \vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId] + , caseClosing = \_ -> fail "doRemoteRequest RELY violation" + , caseClosed = throwIO (userError "Remote endpoint closed") + } takeMVar reply --- | Check if the remote endpoint is unused, and if so, send a CloseSocket request +-- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO () closeIfUnused (ourEndPoint, theirEndPoint) = - modifyRemoteState_ (ourEndPoint, theirEndPoint) - -- RemoteEndPointInvalid - (return . RemoteEndPointInvalid) - -- RemoteEndPointValid - (\vst -> if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) - then do - sendOn vst [encodeInt32 CloseSocket] - resolved <- newEmptyMVar - return $ RemoteEndPointClosing resolved vst - else - return $ RemoteEndPointValid vst) - -- RemoteEndPointClosing - (return . uncurry RemoteEndPointClosing) - -- RemoteEndPointClosed - (return RemoteEndPointClosed) + modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity + { caseValid = \vst -> + if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + then do + sendOn vst [encodeInt32 CloseSocket] + resolved <- newEmptyMVar + return $ RemoteEndPointClosing resolved vst + else + return $ RemoteEndPointValid vst + } -- | Fork a new thread and store its ID as part of the transport state -- @@ -944,7 +1005,10 @@ forkEndPointThread ourEndPoint p = LocalEndPointValid vst -> do threadRegistered <- newEmptyMVar tid <- forkIO (takeMVar threadRegistered >> p >> removeThread) - putMVar ourState $ LocalEndPointValid . (internalThreads ^: (tid :)) $ vst + putMVar ourState ( LocalEndPointValid + . (internalThreads ^: (tid :)) + $ vst + ) putMVar threadRegistered () return True LocalEndPointClosed -> do @@ -956,27 +1020,49 @@ forkEndPointThread ourEndPoint p = tid <- myThreadId modifyMVar_ ourState $ \st -> case st of LocalEndPointValid vst -> - return (LocalEndPointValid . (internalThreads ^: filter (/= tid)) $ vst) + return ( LocalEndPointValid + . (internalThreads ^: filter (/= tid)) + $ vst + ) LocalEndPointClosed -> return LocalEndPointClosed ourState :: MVar LocalEndPointState ourState = localState ourEndPoint +-------------------------------------------------------------------------------- +-- As soon as a remote connection fails, we want to put notify our endpoint -- +-- and put it into a closed state. Since this may happen in many places, we -- +-- provide some abstractions. -- +-------------------------------------------------------------------------------- + +data RemoteStatePatternMatch a = RemoteStatePatternMatch + { caseInvalid :: TransportError ConnectErrorCode -> IO a + , caseValid :: ValidRemoteEndPointState -> IO a + , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a + , caseClosed :: IO a + } + +remoteStateIdentity :: RemoteStatePatternMatch RemoteState +remoteStateIdentity = + RemoteStatePatternMatch + { caseInvalid = return . RemoteEndPointInvalid + , caseValid = return . RemoteEndPointValid + , caseClosing = (return .) . RemoteEndPointClosing + , caseClosed = return RemoteEndPointClosed + } + -- | Like modifyMVar, but if an exception occurs don't restore the remote -- endpoint to its original value but close it instead -modifyRemoteState :: EndPointPair -- ^ Local and remote endpoint - -> (TransportError ConnectErrorCode -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointInvalid - -> (ValidRemoteEndPointState -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointValid - -> ((MVar (), ValidRemoteEndPointState) -> IO (RemoteState, a)) -- ^ Case for RemoteEndPointClosing - -> IO (RemoteState, a) -- ^ Case for RemoteEndPointClosed +modifyRemoteState :: EndPointPair + -> RemoteStatePatternMatch (RemoteState, a) -> IO a -modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = +modifyRemoteState (ourEndPoint, theirEndPoint) match = mask $ \restore -> do st <- takeMVar theirState case st of RemoteEndPointValid vst -> do - mResult <- try $ restore (caseValid vst) + mResult <- try $ restore (caseValid match vst) case mResult of Right (st', a) -> do putMVar theirState st' @@ -986,15 +1072,18 @@ modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing -- The other cases are less interesting, because unless the endpoint is -- in Valid state we're not supposed to do any IO on it RemoteEndPointClosing resolved vst -> do - (st', a) <- restore (caseClosing (resolved, vst)) `onException` putMVar theirState st + (st', a) <- onException (restore $ caseClosing match resolved vst) + (putMVar theirState st) putMVar theirState st' return a RemoteEndPointInvalid err -> do - (st', a) <- restore (caseInvalid err) `onException` putMVar theirState st + (st', a) <- onException (restore $ caseInvalid match err) + (putMVar theirState st) putMVar theirState st' return a RemoteEndPointClosed -> do - (st', a) <- restore caseClosed `onException` putMVar theirState st + (st', a) <- onException (restore $ caseClosed match) + (putMVar theirState st) putMVar theirState st' return a where @@ -1003,6 +1092,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing handleException :: SomeException -> ValidRemoteEndPointState -> IO a handleException ex vst = do + tryCloseSocket (remoteSocket vst) -- We need to remove the remote endpoint from the local endpoint before -- putting it in Closed state (by INV-CLOSE), but we don't want to hold -- two locks at the same time. So we put it in closing state first .. @@ -1013,7 +1103,8 @@ modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing -- .. close it and notify the local endpoint we lost the connection .. mIncoming <- closeRemoteEndPoint forM_ mIncoming $ \incoming -> do - let err = TransportError (EventConnectionLost (remoteAddress theirEndPoint) incoming) (show ex) + let code = EventConnectionLost (remoteAddress theirEndPoint) incoming + err = TransportError code (show ex) writeChan (localChannel ourEndPoint) $ ErrorEvent err -- .. and finally rethrow the exception throwIO ex @@ -1035,35 +1126,41 @@ modifyRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing fail "the impossible happened" -- | Like 'modifyRemoteState' but without a return value -modifyRemoteState_ :: EndPointPair -- ^ Local and remote endpoint - -> (TransportError ConnectErrorCode -> IO RemoteState) -- ^ Case for RemoteEndPointInvalid - -> (ValidRemoteEndPointState -> IO RemoteState) -- ^ Case for RemoteEndPointValid - -> ((MVar (), ValidRemoteEndPointState) -> IO RemoteState) -- ^ Case for RemoteEndPointClosing - -> IO RemoteState -- ^ Case for RemoteEndPointClosed +modifyRemoteState_ :: EndPointPair + -> RemoteStatePatternMatch RemoteState -> IO () -modifyRemoteState_ (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = +modifyRemoteState_ (ourEndPoint, theirEndPoint) match = modifyRemoteState (ourEndPoint, theirEndPoint) - (u . caseInvalid) - (u . caseValid) - (u . caseClosing) - (u caseClosed) + RemoteStatePatternMatch + { caseInvalid = u . caseInvalid match + , caseValid = u . caseValid match + , caseClosing = \resolved vst -> u $ caseClosing match resolved vst + , caseClosed = u $ caseClosed match + } where u :: IO a -> IO (a, ()) u p = p >>= \a -> return (a, ()) -- | Like 'modifyRemoteState' but without the ability to change the state -withRemoteState :: EndPointPair -- ^ Local and remote endpoint - -> (TransportError ConnectErrorCode -> IO a) -- ^ Case for RemoteEndPointInvalid - -> (ValidRemoteEndPointState -> IO a) -- ^ Case for RemoteEndPointValid - -> ((MVar (), ValidRemoteEndPointState) -> IO a) -- ^ Case for RemoteEndPointClosing - -> IO a -- ^ Case for RemoteEndPointClosed +withRemoteState :: EndPointPair + -> RemoteStatePatternMatch a -> IO a -withRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing caseClosed = +withRemoteState (ourEndPoint, theirEndPoint) match = modifyRemoteState (ourEndPoint, theirEndPoint) - (\err -> (,) (RemoteEndPointInvalid err) <$> caseInvalid err) - (\vst -> (,) (RemoteEndPointValid vst) <$> caseValid vst) - (\(resolved, vst) -> (,) (RemoteEndPointClosing resolved vst) <$> caseClosing (resolved, vst)) - ((,) RemoteEndPointClosed <$> caseClosed) + RemoteStatePatternMatch + { caseInvalid = \err -> do + a <- caseInvalid match err + return (RemoteEndPointInvalid err, a) + , caseValid = \vst -> do + a <- caseValid match vst + return (RemoteEndPointValid vst, a) + , caseClosing = \resolved vst -> do + a <- caseClosing match resolved vst + return (RemoteEndPointClosing resolved vst, a) + , caseClosed = do + a <- caseClosed match + return (RemoteEndPointClosed, a) + } -------------------------------------------------------------------------------- -- Incoming requests -- @@ -1081,43 +1178,33 @@ withRemoteState (ourEndPoint, theirEndPoint) caseInvalid caseValid caseClosing c -- asynchronous, ThreadKilled) exception occurs. (If an exception escapes from -- handleConnectionRequest the transport will be shut down.) handleConnectionRequest :: TCPTransport -> N.Socket -> IO () -handleConnectionRequest transport sock = handle tryCloseSocket $ do +handleConnectionRequest transport sock = handle handleException $ do ourEndPointId <- recvInt32 sock theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock - let ourAddress = encodeEndPointAddress (transportHost transport) (transportPort transport) ourEndPointId + let ourAddress = encodeEndPointAddress (transportHost transport) + (transportPort transport) + ourEndPointId ourEndPoint <- withMVar (transportState transport) $ \st -> case st of TransportValid vst -> case vst ^. localEndPointAt ourAddress of Nothing -> do sendMany sock [encodeInt32 ConnectionRequestEndPointInvalid] - fail "Invalid endpoint" + throwIO $ userError "Invalid endpoint" Just ourEndPoint -> return ourEndPoint TransportClosed -> - fail "Transport closed" + throwIO $ userError "Transport closed" void . forkEndPointThread ourEndPoint $ go ourEndPoint theirAddress where go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do - mEndPoint <- handle (\e -> invalidEndPoint e >> return Nothing) $ do - (crossed, theirEndPoint) <- modifyMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Nothing -> do - theirState <- newEmptyMVar - let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId - } - return (LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) . (nextRemoteId ^: (+ 1)) $ vst, (False, theirEndPoint)) - Just theirEndPoint -> - return (st, (localAddress ourEndPoint < theirAddress, theirEndPoint)) - LocalEndPointClosed -> - fail "Local endpoint closed" + mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do + (theirEndPoint, isNew) <- findRemoteEndPoint ourEndPoint theirAddress + let crossed = not isNew && localAddress ourEndPoint < theirAddress if crossed then do tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] - tryIO $ N.sClose sock + tryCloseSocket sock return Nothing else do let vst = ValidRemoteEndPointState @@ -1140,13 +1227,13 @@ handleConnectionRequest transport sock = handle tryCloseSocket $ do -- exception from this point forward. forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint - tryCloseSocket :: IOException -> IO () - tryCloseSocket _ = void . tryIO $ N.sClose sock + handleException :: IOException -> IO () + handleException _ = tryCloseSocket sock invalidEndPoint :: IOException -> IO () - invalidEndPoint ex = do + invalidEndPoint _ = do tryIO $ sendMany sock [encodeInt32 ConnectionRequestEndPointInvalid] - tryCloseSocket ex + tryCloseSocket sock -- | Handle requests from a remote endpoint. -- @@ -1168,10 +1255,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do _ -> error "handleIncomingMessages RELY violation" - mCleanExit <- tryIO $ go sock - case mCleanExit of - Right () -> return () - Left err -> prematureExit sock err + tryIO (go sock) >>= either (prematureExit sock) return where -- Dispatch go :: N.Socket -> IO () @@ -1196,8 +1280,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do didClose <- closeSocket sock unless didClose $ go sock Nothing -> - -- Invalid control request, exit - hPutStrLn stderr "Warning: invalid control request" + throwIO $ userError "Warning: invalid control request" -- Create a new connection createNewConnection :: ControlRequestId -> IO () @@ -1224,8 +1307,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do : encodeInt32 reqId : prependLength [encodeInt32 newId] ) - -- We add the new connection ID to the list of open connections only once the - -- endpoint has been notified of the new connection (sendOn may fail) + -- We add the new connection ID to the list of open connections only + -- once the endpoint has been notified of the new connection (sendOn + -- may fail) return (RemoteEndPointValid vst) writeChan ourChannel (ConnectionOpened newId ReliableOrdered theirAddr) @@ -1235,13 +1319,16 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do response <- recvWithLength sock mmvar <- modifyMVar ourState $ \st -> case st of LocalEndPointValid vst -> - return (LocalEndPointValid . (pendingCtrlRequestsAt reqId ^= Nothing) $ vst, vst ^. pendingCtrlRequestsAt reqId) + return ( LocalEndPointValid + . (pendingCtrlRequestsAt reqId ^= Nothing) + $ vst + , vst ^. pendingCtrlRequestsAt reqId + ) LocalEndPointClosed -> - fail "Local endpoint closed" + throwIO $ userError "Local endpoint closed" case mmvar of - Nothing -> do - hPutStrLn stderr "Warning: Invalid request ID" - return () -- Invalid request ID. TODO: We just ignore it? + Nothing -> + throwIO $ userError "Warning: Invalid request ID" Just mvar -> putMVar mvar response @@ -1251,21 +1338,17 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- TODO: we should check that this connection is in fact open writeChan ourChannel (ConnectionClosed cid) modifyMVar_ theirState $ \(RemoteEndPointValid vst) -> - return (RemoteEndPointValid . (remoteIncoming ^: IntSet.delete cid) $ vst) + return ( RemoteEndPointValid + . (remoteIncoming ^: IntSet.delete cid) + $ vst + ) closeIfUnused (ourEndPoint, theirEndPoint) -- Close the socket (if we don't have any outgoing connections) closeSocket :: N.Socket -> IO Bool closeSocket sock = do - -- We need to check if we can close the socket (that is, if we don't have - -- any outgoing connections), and once we are sure that we can put the - -- endpoint in Closed state. However, by INV-CLOSE we can only put the - -- endpoint in Closed state once we remove the endpoint from our local - -- connections. But we can do that only once we are sure that we can - -- close the endpoint. Catch-22. We can resolve the catch-22 by locking - -- /both/ our local state and the endpoint state, but at the cost of - -- introducing a double lock and all the associated perils, or by putting - -- the remote endpoint in Closing state first. We opt for the latter. + -- As in modifyRemoteState we put the remote state in Closing state + -- before putting it in Closed state. canClose <- modifyMVar theirState $ \st -> case st of RemoteEndPointValid vst -> do @@ -1295,13 +1378,14 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do Just resolved -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) modifyMVar_ theirState $ return . const RemoteEndPointClosed - N.sClose sock + tryCloseSocket sock putMVar resolved () return True -- Read a message and output it on the endPoint's channel readMessage :: N.Socket -> ConnectionId -> IO () - readMessage sock connId = recvWithLength sock >>= writeChan ourChannel . Received connId + readMessage sock connId = + recvWithLength sock >>= writeChan ourChannel . Received connId -- Arguments ourChannel = localChannel ourEndPoint @@ -1312,15 +1396,17 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Deal with a premature exit prematureExit :: N.Socket -> IOException -> IO () prematureExit sock err = do - N.sClose sock + tryCloseSocket sock removeRemoteEndPoint (ourEndPoint, theirEndPoint) modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> error "handleIncomingMessages RELY violation" RemoteEndPointValid vst -> do - writeChan ourChannel . ErrorEvent $ - TransportError (EventConnectionLost (remoteAddress theirEndPoint) (IntSet.elems $ vst ^. remoteIncoming)) (show err) + let code = EventConnectionLost + (remoteAddress theirEndPoint) + (IntSet.elems $ vst ^. remoteIncoming) + writeChan ourChannel . ErrorEvent $ TransportError code (show err) return RemoteEndPointClosed RemoteEndPointClosing _ _ -> return RemoteEndPointClosed @@ -1335,9 +1421,12 @@ getNextConnectionId ourEndpoint = modifyMVar (localState ourEndpoint) $ \st -> case st of LocalEndPointValid vst -> do let connId = vst ^. nextConnectionId - return (LocalEndPointValid . (nextConnectionId ^= connId + 1) $ vst, connId) + return ( LocalEndPointValid + . (nextConnectionId ^= connId + 1) + $ vst + , connId) LocalEndPointClosed -> - fail "Local endpoint closed" + throwIO $ userError "Local endpoint closed" -------------------------------------------------------------------------------- -- Constants -- @@ -1348,7 +1437,7 @@ firstNonReservedConnectionId :: ConnectionId firstNonReservedConnectionId = 1024 -------------------------------------------------------------------------------- --- Accessor definitions -- +-- Accessor definitions -- -------------------------------------------------------------------------------- localEndPoints :: Accessor ValidTransportState (Map EndPointAddress LocalEndPoint) @@ -1375,17 +1464,18 @@ internalThreads = accessor _internalThreads (\ts st -> st { _internalThreads = t nextRemoteId :: Accessor ValidLocalEndPointState Int nextRemoteId = accessor _nextRemoteId (\rid st -> st { _nextRemoteId = rid }) +remoteOutgoing :: Accessor ValidRemoteEndPointState Int +remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs }) + +remoteIncoming :: Accessor ValidRemoteEndPointState IntSet +remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) + localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidLocalEndPointState (Maybe (MVar [ByteString])) pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral ix) -localConnectionTo :: EndPointAddress -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) +localConnectionTo :: EndPointAddress + -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) localConnectionTo addr = localConnections >>> DAC.mapMaybe addr - -remoteOutgoing :: Accessor ValidRemoteEndPointState Int -remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs }) - -remoteIncoming :: Accessor ValidRemoteEndPointState IntSet -remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) From 406602816fcc430ff884e1c2741497a21d4dace6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 23 May 2012 13:19:10 +0100 Subject: [PATCH 0073/2357] Deal with invalid close requests --- src/Network/Transport/TCP.hs | 14 +++++++++---- tests/TestTCP.hs | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index dce42e86..72ff0c92 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -101,6 +101,7 @@ import qualified Data.IntSet as IntSet ( empty , singleton , null , delete + , member ) import Data.Map (Map) import qualified Data.Map as Map (empty) @@ -1333,15 +1334,18 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do putMVar mvar response -- Close a connection + -- It is important that we verify that the connection is in fact open, + -- because otherwise we should not decrement the reference count closeConnection :: ConnectionId -> IO () closeConnection cid = do - -- TODO: we should check that this connection is in fact open - writeChan ourChannel (ConnectionClosed cid) - modifyMVar_ theirState $ \(RemoteEndPointValid vst) -> + modifyMVar_ theirState $ \(RemoteEndPointValid vst) -> do + unless (IntSet.member cid (vst ^. remoteIncoming)) $ + throwIO $ userError "Invalid CloseConnection" return ( RemoteEndPointValid . (remoteIncoming ^: IntSet.delete cid) $ vst ) + writeChan ourChannel (ConnectionClosed cid) closeIfUnused (ourEndPoint, theirEndPoint) -- Close the socket (if we don't have any outgoing connections) @@ -1382,7 +1386,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do putMVar resolved () return True - -- Read a message and output it on the endPoint's channel + -- Read a message and output it on the endPoint's channel By rights we + -- should verify that the connection ID is valid, but this is unnecessary + -- overhead readMessage :: N.Socket -> ConnectionId -> IO () readMessage sock connId = recvWithLength sock >>= writeChan ourChannel . Received connId diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 040e9f5e..6038ffc6 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -691,6 +691,43 @@ testUnidirectionalError nextPort = do takeMVar clientDone +testInvalidCloseConnection :: IO N.ServiceName -> IO () +testInvalidCloseConnection nextPort = do + Right (transport, internals) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + serverDone <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + ConnectionOpened _ _ _ <- receive endpoint + + -- At this point the client sends an invalid request, so we terminate the + -- connection + ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint + + putMVar serverDone () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + let ourAddr = address endpoint + + -- Connect so that we have a TCP connection + theirAddr <- readMVar serverAddr + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- Get a handle on the TCP connection and manually send an invalid CloseConnection request + Right sock <- socketBetween internals ourAddr theirAddr + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (12345 :: Int)] + + putMVar clientDone () + + mapM_ takeMVar [clientDone, serverDone] + main :: IO () main = do portMVar <- newEmptyMVar @@ -708,5 +745,6 @@ main = do , ("BreakTransport", testBreakTransport nextPort) , ("Reconnect", testReconnect nextPort) , ("UnidirectionalError", testUnidirectionalError nextPort) + , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") From adb0f9315eb3332d5652f4f17632870eb07aa432 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 23 May 2012 13:55:00 +0100 Subject: [PATCH 0074/2357] Output exceptional event after endpoint is closed --- src/Network/Transport/TCP.hs | 25 +++++++++++++++---------- tests/TestAuxiliary.hs | 6 ++++++ tests/TestTCP.hs | 6 ------ tests/TestTransport.hs | 24 +++++++++++++++++++++++- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 72ff0c92..f54f73db 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -10,9 +10,6 @@ -- Applications that use the TCP transport should use -- 'Network.Socket.withSocketsDo' in their main function for Windows -- compatibility (see "Network.Socket"). --- --- TODOs: --- * Output exception on channel after endpoint is closed module Network.Transport.TCP ( -- * Main API createTransport -- * Internals (exposed for unit tests) @@ -80,6 +77,7 @@ import Control.Monad.Error (ErrorT(..), runErrorT) import Control.Exception ( IOException , SomeException , handle + , throw , throwIO , try , bracketOnError @@ -293,7 +291,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- -- INV-CLOSE: Whenever we put an endpoint in closed state we remove that -- endpoint from localConnections first, so that if a concurrent thread reads --- the mvar, finds EndPointClosed, and then looks up the endpoint in +-- the mvar, finds RemoteEndPointClosed, and then looks up the endpoint in -- localConnections it is guaranteed to either find a different remote -- endpoint, or else none at all. -- INV-RESOLVE: Whenever we move a endpoint from Closing to Closed state, we @@ -399,7 +397,9 @@ createTransportExposeInternals host port = do return ( Transport { newEndPoint = apiNewEndPoint transport - , closeTransport = let evs = [EndPointClosed] in + , closeTransport = let evs = [ EndPointClosed + , throw $ userError "Transport closed" + ] in apiCloseTransport transport (Just tid) evs } , TransportInternals @@ -410,8 +410,10 @@ createTransportExposeInternals host port = do where terminationHandler :: TCPTransport -> SomeException -> IO () terminationHandler transport ex = do - let ev = ErrorEvent (TransportError EventTransportFailed (show ex)) - apiCloseTransport transport Nothing [ev] + let evs = [ ErrorEvent (TransportError EventTransportFailed (show ex)) + , throw $ userError "Transport closed" + ] + apiCloseTransport transport Nothing evs -------------------------------------------------------------------------------- -- API functions -- @@ -438,7 +440,10 @@ apiNewEndPoint transport = try $ do { receive = readChan (localChannel ourEndPoint) , address = localAddress ourEndPoint , connect = apiConnect ourEndPoint - , closeEndPoint = apiCloseEndPoint transport [EndPointClosed] ourEndPoint + , closeEndPoint = let evs = [ EndPointClosed + , throw $ userError "Endpoint closed" + ] in + apiCloseEndPoint transport evs ourEndPoint , newMulticastGroup = return . Left $ newMulticastGroupError , resolveMulticastGroup = return . Left . const resolveMulticastGroupError } @@ -501,7 +506,7 @@ apiSend :: EndPointPair -- ^ Local and remote endpoint -> [ByteString] -- ^ Payload -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = - -- The 'join' joins the inner exception that we explicitly return) for + -- The 'join' joins the inner exception (which we explicitly return, for -- instance if the connection is closed) with the outer exception (which is -- returned by 'try' when an exception is thrown by 'sendOn', and handled -- by 'withRemoteState') @@ -733,7 +738,7 @@ requestConnectionTo ourEndPoint theirAddress = go readMVar resolved >> go RemoteEndPointClosed -> - -- EndPointClosed indicates that a concurrent thread was in the + -- RemoteEndPointClosed indicates that a concurrent thread was in the -- process of closing the TCP connection to the remote endpoint when -- we obtained a reference to it. By INV-CLOSE we can assume that the -- remote endpoint will now have been removed from ourState, so we diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index e090a810..0570f697 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -80,3 +80,9 @@ instance Show err => Traceable (TransportError err) where instance Traceable EndPointAddress where trace = traceShow . endPointAddressToByteString + +instance Traceable SomeException where + trace = traceShow + +instance Traceable ThreadId where + trace = const Nothing diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 6038ffc6..f8a34b9d 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -57,15 +57,9 @@ instance Traceable N.Socket where instance Traceable N.AddrInfo where trace = traceShow -instance Traceable ThreadId where - trace = const Nothing - instance Traceable TransportInternals where trace = const Nothing -instance Traceable SomeException where - trace = traceShow - -- Test that the server gets a ConnectionClosed message when the client closes -- the socket without sending an explicit control message to the server first testEarlyDisconnect :: IO N.ServiceName -> IO () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index ffa926b9..ac1f9798 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -4,11 +4,12 @@ module TestTransport where import Prelude hiding (catch, (>>=), (>>), return, fail) import TestAuxiliary (forkTry, runTests) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) +import Control.Exception (evaluate, try, SomeException) import Control.Monad (replicateM, replicateM_, when, guard, forM_) import Control.Monad.Error () import Network.Transport hiding (connect) import qualified Network.Transport as NT -import Network.Transport.Internal (tlog) +import Network.Transport.Internal (tlog, tryIO) import Network.Transport.Util (spawn) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) @@ -734,6 +735,26 @@ testConnectClosedEndPoint transport = do takeMVar clientDone +-- | We should receive an exception when doing a 'receive' after we have been +-- notified that an endpoint has been closed +testExceptionOnReceive :: IO (Either String Transport) -> IO () +testExceptionOnReceive newTransport = do + Right transport <- newTransport + + -- Test one: when we close an endpoint specifically + Right endpoint1 <- newEndPoint transport + closeEndPoint endpoint1 + EndPointClosed <- receive endpoint1 + Left _ <- (try :: IO a -> IO (Either SomeException a)) (receive endpoint1 >>= evaluate) + + -- Test two: when we close the entire transport + Right endpoint2 <- newEndPoint transport + closeTransport transport + EndPointClosed <- receive endpoint2 + Left _ <- (try :: IO a -> IO (Either SomeException a)) (receive endpoint2 >>= evaluate) + + return () + -- Transport tests testTransport :: IO (Either String Transport) -> IO () testTransport newTransport = do @@ -753,6 +774,7 @@ testTransport newTransport = do , ("CloseEndPoint", testCloseEndPoint transport numPings) , ("CloseTransport", testCloseTransport newTransport) , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) + , ("ExceptionOnReceive", testExceptionOnReceive newTransport) ] where numPings = 10000 :: Int From f239371a2b31f62a3c3c85b67b3b4759a55d751e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 23 May 2012 14:47:07 +0100 Subject: [PATCH 0075/2357] Remove potential deadlock --- src/Network/Transport/TCP.hs | 97 +++++++++++++----------------------- 1 file changed, 34 insertions(+), 63 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index f54f73db..ac6fb2c8 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -248,61 +248,29 @@ data ValidLocalEndPointState = ValidLocalEndPointState , _nextRemoteId :: !Int } --- A remote endpoint has incoming and outgoing connections, and when the total --- number of connections (that is, the 'remoteRefCount') drops to zero we want --- to close the TCP connection to the endpoint. +-- Invariants for dealing with remote endpoints: -- --- What we need to avoid, however, is a situation with two concurrent threads --- where one closes the last (incoming or outgoing) connection, initiating the --- process of closing the connection, while another requests (but does not yet --- have) a new connection. +-- INV-SEND: Whenever we send data the remote endpoint must be locked (to avoid +-- interleaving bits of payload). -- --- We therefore insist that: --- --- 1. All operations that change the state of the endpoint (ask for a new --- connection, close a connection, close the endpoint completely) are --- serialized (that is, they take the contents of the MVar containing the --- endpoint state before starting and don't put the updated contents back --- until they have completed). --- 2. Writing to ('apiSend') or reading from (in 'handleIncomingMessages') must --- maintain the invariant that the connection they are writing to/reading --- from *must* be "included" in the 'remoteRefCount'. --- 3. Since every endpoint is associated with a single socket, we regard writes --- that endpoint a state change too (i.e., we take the MVar before the write --- and put it back after). The reason is that we don't want to "scramble" the --- output of multiple concurrent writes (either from an explicit 'send' or --- the writes for control messages). --- --- Of course, "serialize" does not mean that we want for the remote endpoint to --- reply. "Send" takes the mvar, sends to the endpoint (asynchronously), and --- then puts the mvar back, without waiting for the endpoint to receive the --- message. Similarly, when requesting a new connection, we take the mvar, --- tentatively increment the reference count, send the control request, and --- then put the mvar back. When the remote host responds to the new connection --- request we might have to do another state change (reduce the refcount) if --- the connection request was refused but we don't want to increment the ref --- count only after the remote host acknowledges the request because then a --- concurrent 'close' might actually attempt to close the socket. --- --- Since we don't do concurrent reads from the same socket we don't need to --- take the lock when reading from the socket. --- --- Invariants: --- --- INV-CLOSE: Whenever we put an endpoint in closed state we remove that +-- INV-CLOSE: Local endpoints should never point to remote endpoint in closed +-- state. Whenever we put an endpoint in closed state we remove that -- endpoint from localConnections first, so that if a concurrent thread reads --- the mvar, finds RemoteEndPointClosed, and then looks up the endpoint in +-- the MVar, finds RemoteEndPointClosed, and then looks up the endpoint in -- localConnections it is guaranteed to either find a different remote --- endpoint, or else none at all. --- INV-RESOLVE: Whenever we move a endpoint from Closing to Closed state, we --- signal on the corresponding MVar only *after* the endpoint has been put in --- Closed state. This way when to threads try to resolve they don't both --- attempt to write to the "resolved" MVar. TODO: Make sure that this --- invariant is adhered too. +-- endpoint, or else none at all (if we don't insist in this order some +-- threads might start spinning). +-- +-- INV-RESOLVE: We should only signal on 'resolved' while the remote endpoint is +-- locked, and the remote endpoint must be in Valid or Closed state once +-- unlocked. This guarantees that there will not be two threads attempting to +-- both signal on 'resolved'. +-- -- INV-LOST: If a send or recv fails, or a socket is closed unexpectedly, we -- first put the remote endpoint in Closing or Closed state, and then send a -- EventConnectionLost event. This guarantees that we only send this event -- once. +-- -- INV-CLOSING: An endpoint in closing state is for all intents and purposes -- closed; that is, we shouldn't do any 'send's on it (although 'recv' is -- acceptable, of course -- as we are waiting for the remote endpoint to @@ -368,8 +336,6 @@ data TransportInternals = TransportInternals -------------------------------------------------------------------------------- -- | Create a TCP transport --- --- TODOs: deal with hints createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) @@ -1122,8 +1088,8 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = st <- takeMVar theirState case st of RemoteEndPointClosing resolved vst -> do - putMVar theirState RemoteEndPointClosed putMVar resolved () + putMVar theirState RemoteEndPointClosed return . Just . IntSet.elems $ vst ^. remoteIncoming RemoteEndPointClosed -> do putMVar theirState RemoteEndPointClosed @@ -1373,23 +1339,28 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Attempt to reply (but don't insist) tryIO $ sendOn vst' [encodeInt32 CloseSocket] resolved <- newEmptyMVar - return (RemoteEndPointClosing resolved vst', Just resolved) + return (RemoteEndPointClosing resolved vst', True) else - return (RemoteEndPointValid vst', Nothing) - RemoteEndPointClosing resolved _ -> - return (st, Just resolved) + return (RemoteEndPointValid vst', False) + RemoteEndPointClosing _ _ -> + return (st, True) _ -> error "handleIncomingConnections RELY violation" + + when canClose $ do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + modifyMVar_ theirState $ \st -> do + case st of + RemoteEndPointClosing resolved _ -> + putMVar resolved () + RemoteEndPointClosed -> + return () + _ -> + fail "Impossible case" + return RemoteEndPointClosed + tryCloseSocket sock - case canClose of - Nothing -> - return False - Just resolved -> do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - modifyMVar_ theirState $ return . const RemoteEndPointClosed - tryCloseSocket sock - putMVar resolved () - return True + return canClose -- Read a message and output it on the endPoint's channel By rights we -- should verify that the connection ID is valid, but this is unnecessary From 4dd93434c6457c6cdbc9d0421bab4749255f9c45 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 23 May 2012 15:27:41 +0100 Subject: [PATCH 0076/2357] Make it work with ghc 7.0.4 --- network-transport.cabal | 8 ++++---- src/Network/Transport/Internal.hs | 8 ++++++++ src/Network/Transport/Internal/TCP.hs | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 9fb93841..d16f56ff 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -23,7 +23,7 @@ Library Network.Transport.Chan, Network.Transport.TCP, Network.Transport.Util - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src @@ -38,7 +38,7 @@ Test-Suite TestTCP mtl, transformers, ansi-terminal - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -52,7 +52,7 @@ Test-Suite TestMulticastInMemory mtl, transformers, ansi-terminal - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -66,6 +66,6 @@ Test-Suite TestInMemory mtl, transformers, ansi-terminal - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleInstances, RankNTypes, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 34f755f9..ea1cb205 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -9,7 +9,9 @@ module Network.Transport.Internal ( -- * Encoders/decoders , mapIOException , tryIO , tryToEnum + -- * Replicated functionality from "base" , void + , forkIOWithUnmask -- * Debugging , tlog ) where @@ -23,6 +25,8 @@ import qualified Data.ByteString as BS (length) import qualified Data.ByteString.Internal as BSI (unsafeCreate, toForeignPtr, inlinePerformIO) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Exception (IOException, Exception, catch, try, throwIO) +import Control.Concurrent (ThreadId, forkIO) +import GHC.IO (unsafeUnmask) --import Control.Concurrent (myThreadId) foreign import ccall unsafe "htonl" htonl :: CInt -> CInt @@ -85,6 +89,10 @@ tlog msg = liftIO $ do void :: Monad m => m a -> m () void p = p >> return () +-- | This was introduced in "base" some time after 7.0.4 +forkIOWithUnmask :: ((forall a . IO a -> IO a) -> IO ()) -> IO ThreadId +forkIOWithUnmask io = forkIO (io unsafeUnmask) + -- | Safe version of 'toEnum' tryToEnum :: (Enum a, Bounded a) => Int -> Maybe a tryToEnum = go minBound maxBound diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 18147f3c..6aef5ffd 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -7,7 +7,7 @@ module Network.Transport.Internal.TCP ( forkServer ) where import Prelude hiding (catch) -import Network.Transport.Internal (decodeInt32, void, tryIO) +import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) import qualified Network.Socket as N ( HostName , ServiceName , Socket @@ -26,7 +26,7 @@ import qualified Network.Socket as N ( HostName , sClose ) import qualified Network.Socket.ByteString as NBS (recv) -import Control.Concurrent (ThreadId, forkIOWithUnmask) +import Control.Concurrent (ThreadId) import Control.Monad (liftM, forever) import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) import Data.ByteString (ByteString) From 53756b9e95e0d14f096ce213e58540a95c47eb2e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 24 May 2012 08:38:35 +0100 Subject: [PATCH 0077/2357] Remove compiler warnings --- tests/TestTCP.hs | 2 +- tests/TestTransport.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index f8a34b9d..f1a73e63 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -12,7 +12,7 @@ import Network.Transport.TCP ( createTransport , encodeEndPointAddress ) import Data.Int (Int32) -import Control.Concurrent (threadDelay, ThreadId, killThread) +import Control.Concurrent (threadDelay, killThread) import Control.Concurrent.MVar ( MVar , newEmptyMVar , putMVar diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index ac1f9798..a62fded0 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -9,7 +9,7 @@ import Control.Monad (replicateM, replicateM_, when, guard, forM_) import Control.Monad.Error () import Network.Transport hiding (connect) import qualified Network.Transport as NT -import Network.Transport.Internal (tlog, tryIO) +import Network.Transport.Internal (tlog) import Network.Transport.Util (spawn) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) From d5e1b08f07030ba8fc85ecf5013923674dc94e1e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 24 May 2012 12:09:09 +0100 Subject: [PATCH 0078/2357] Relax RELY assumptions in handleIncomingMessages --- src/Network/Transport/TCP.hs | 173 +++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 80 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index ac6fb2c8..26b5d7e2 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -275,6 +275,12 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- closed; that is, we shouldn't do any 'send's on it (although 'recv' is -- acceptable, of course -- as we are waiting for the remote endpoint to -- confirm or deny the request). +-- +-- INV-LOCK-ORDER: Remote endpoint must be locked before their local endpoints. +-- In other words: it is okay to call modifyMVar on a local endpoint inside a +-- modifyMVar on a remote endpoint, but not the other way around. In +-- particular, it is okay to call removeRemoteEndPoint inside +-- modifyRemoteState. data RemoteEndPoint = RemoteEndPoint { remoteAddress :: EndPointAddress @@ -285,7 +291,7 @@ data RemoteEndPoint = RemoteEndPoint data RemoteState = RemoteEndPointInvalid (TransportError ConnectErrorCode) | RemoteEndPointValid ValidRemoteEndPointState - | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState + | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState | RemoteEndPointClosed data ValidRemoteEndPointState = ValidRemoteEndPointState @@ -479,7 +485,7 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = liftM join . try . mapIOException sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = \_ -> - fail "apiSend RELY violation" + relyViolation (ourEndPoint, theirEndPoint) "apiSend" , caseValid = \vst -> do alive <- readIORef connAlive if alive @@ -533,6 +539,7 @@ apiCloseEndPoint transport evs ourEndPoint = do tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed RemoteEndPointClosing _ conn -> do + -- TODO: should we signal on resolved here? tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed RemoteEndPointClosed -> @@ -939,9 +946,9 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do LocalEndPointClosed -> throwIO (userError "Local endpoint closed") withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseInvalid = \_ -> fail "doRemoteRequest RELY violation" + { caseInvalid = \_ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest" , caseValid = \vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId] - , caseClosing = \_ -> fail "doRemoteRequest RELY violation" + , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest" , caseClosed = throwIO (userError "Remote endpoint closed") } takeMVar reply @@ -955,7 +962,7 @@ closeIfUnused (ourEndPoint, theirEndPoint) = then do sendOn vst [encodeInt32 CloseSocket] resolved <- newEmptyMVar - return $ RemoteEndPointClosing resolved vst + return $ RemoteEndPointClosing resolved vst else return $ RemoteEndPointValid vst } @@ -1065,38 +1072,14 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = handleException :: SomeException -> ValidRemoteEndPointState -> IO a handleException ex vst = do tryCloseSocket (remoteSocket vst) - -- We need to remove the remote endpoint from the local endpoint before - -- putting it in Closed state (by INV-CLOSE), but we don't want to hold - -- two locks at the same time. So we put it in closing state first .. - resolved <- newEmptyMVar - putMVar theirState (RemoteEndPointClosing resolved vst) - -- .. then remove it from the local endpoint .. removeRemoteEndPoint (ourEndPoint, theirEndPoint) - -- .. close it and notify the local endpoint we lost the connection .. - mIncoming <- closeRemoteEndPoint - forM_ mIncoming $ \incoming -> do - let code = EventConnectionLost (remoteAddress theirEndPoint) incoming - err = TransportError code (show ex) - writeChan (localChannel ourEndPoint) $ ErrorEvent err - -- .. and finally rethrow the exception + putMVar theirState RemoteEndPointClosed + let incoming = IntSet.elems $ vst ^. remoteIncoming + code = EventConnectionLost (remoteAddress theirEndPoint) incoming + err = TransportError code (show ex) + writeChan (localChannel ourEndPoint) $ ErrorEvent err throwIO ex - -- Returns the set of incoming connections if we closed the connection, or - -- 'Nothing' if the endpoint was already closed - closeRemoteEndPoint :: IO (Maybe [ConnectionId]) - closeRemoteEndPoint = do - st <- takeMVar theirState - case st of - RemoteEndPointClosing resolved vst -> do - putMVar resolved () - putMVar theirState RemoteEndPointClosed - return . Just . IntSet.elems $ vst ^. remoteIncoming - RemoteEndPointClosed -> do - putMVar theirState RemoteEndPointClosed - return Nothing - _ -> - fail "the impossible happened" - -- | Like 'modifyRemoteState' but without a return value modifyRemoteState_ :: EndPointPair -> RemoteStatePatternMatch RemoteState @@ -1211,25 +1194,39 @@ handleConnectionRequest transport sock = handle handleException $ do -- -- Returns only if the remote party closes the socket or if an error occurs. -- --- RELY: The remote endpoint must be in RemoteEndPointValid or --- RemoteEndPointClosing state. If the latter, then the 'resolved' MVar --- associated with the closing state must be empty. +-- RELY: The remote endpoint must not be invalid. -- GUARANTEE: May change the remote endpoint to RemoteEndPointClosed state. handleIncomingMessages :: EndPointPair -> IO () handleIncomingMessages (ourEndPoint, theirEndPoint) = do - -- For efficiency sake we get the socket once and for all - sock <- withMVar theirState $ \st -> + mSock <- withMVar theirState $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:initialization" RemoteEndPointValid ep -> - return (remoteSocket ep) + return . Just $ remoteSocket ep RemoteEndPointClosing _ ep -> - return (remoteSocket ep) - _ -> - error "handleIncomingMessages RELY violation" - - tryIO (go sock) >>= either (prematureExit sock) return + return . Just $ remoteSocket ep + RemoteEndPointClosed -> + -- The remote endpoint got closed before we got a chance to start + -- dealing with incoming messages + return Nothing + + forM_ mSock $ \sock -> + tryIO (go sock) >>= either (prematureExit sock) return where -- Dispatch + -- + -- If a recv throws an exception this will be caught top-level and + -- 'prematureExit' will be invoked. The same will happen if the remote + -- endpoint is put into a Closed (or Closing) state by a concurrent thread + -- (because a 'send' failed) -- the individual handlers below will throw a + -- user exception which is then caught and handled the same way as an + -- exception thrown by 'recv'. + -- + -- Note: modifyRemoteState closes the socket before putting the remote + -- endpoint in closing state, so it is not possible that modifyRemoteState + -- puts the remote endpoint in Closing state only for it to be reset to + -- Valid by the RequestConnectionId handler below. go :: N.Socket -> IO () go sock = do connId <- recvInt32 sock @@ -1262,6 +1259,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do newId <- getNextConnectionId ourEndPoint modifyMVar_ theirState $ \st -> do vst <- case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" RemoteEndPointValid vst -> return (remoteIncoming ^: IntSet.insert newId $ vst) RemoteEndPointClosing resolved vst -> do @@ -1273,8 +1272,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- to RemoteEndPointValid putMVar resolved () return (remoteIncoming ^= IntSet.singleton newId $ vst) - _ -> - error "handleIncomingMessages RELY violation" + RemoteEndPointClosed -> + throw $ userError "Remote endpoint closed" sendOn vst ( encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 newId] @@ -1309,23 +1308,33 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- because otherwise we should not decrement the reference count closeConnection :: ConnectionId -> IO () closeConnection cid = do - modifyMVar_ theirState $ \(RemoteEndPointValid vst) -> do - unless (IntSet.member cid (vst ^. remoteIncoming)) $ - throwIO $ userError "Invalid CloseConnection" - return ( RemoteEndPointValid - . (remoteIncoming ^: IntSet.delete cid) - $ vst - ) + modifyMVar_ theirState $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "closeConnection" + RemoteEndPointValid vst -> do + unless (IntSet.member cid (vst ^. remoteIncoming)) $ + throwIO $ userError "Invalid CloseConnection" + return ( RemoteEndPointValid + . (remoteIncoming ^: IntSet.delete cid) + $ vst + ) + RemoteEndPointClosing _ _ -> + -- If the remote endpoint is in Closing state, that means that are as + -- far as we are concerned there are no incoming connections. This + -- means that a CloseConnection request at this point is invalid. + throwIO $ userError "Invalid CloseConnection request" + RemoteEndPointClosed -> + throwIO $ userError "Remote endpoint closed" writeChan ourChannel (ConnectionClosed cid) closeIfUnused (ourEndPoint, theirEndPoint) -- Close the socket (if we don't have any outgoing connections) closeSocket :: N.Socket -> IO Bool closeSocket sock = do - -- As in modifyRemoteState we put the remote state in Closing state - -- before putting it in Closed state. - canClose <- modifyMVar theirState $ \st -> + modifyMVar theirState $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" RemoteEndPointValid vst -> do -- We regard a CloseSocket message as an (optimized) way for the -- remote endpoint to indicate that all its connections to us are @@ -1338,29 +1347,18 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do then do -- Attempt to reply (but don't insist) tryIO $ sendOn vst' [encodeInt32 CloseSocket] - resolved <- newEmptyMVar - return (RemoteEndPointClosing resolved vst', True) + tryCloseSocket sock + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + return (RemoteEndPointClosed, True) else return (RemoteEndPointValid vst', False) - RemoteEndPointClosing _ _ -> - return (st, True) - _ -> - error "handleIncomingConnections RELY violation" - - when canClose $ do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - modifyMVar_ theirState $ \st -> do - case st of - RemoteEndPointClosing resolved _ -> - putMVar resolved () - RemoteEndPointClosed -> - return () - _ -> - fail "Impossible case" - return RemoteEndPointClosed - tryCloseSocket sock - - return canClose + RemoteEndPointClosing resolved _ -> do + tryCloseSocket sock + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + putMVar resolved () + return (RemoteEndPointClosed, True) + RemoteEndPointClosed -> + throw $ userError "Remote endpoint closed" -- Read a message and output it on the endPoint's channel By rights we -- should verify that the connection ID is valid, but this is unnecessary @@ -1383,14 +1381,15 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> - error "handleIncomingMessages RELY violation" + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do let code = EventConnectionLost (remoteAddress theirEndPoint) (IntSet.elems $ vst ^. remoteIncoming) writeChan ourChannel . ErrorEvent $ TransportError code (show err) return RemoteEndPointClosed - RemoteEndPointClosing _ _ -> + RemoteEndPointClosing resolved _ -> do + putMVar resolved () return RemoteEndPointClosed RemoteEndPointClosed -> return RemoteEndPointClosed @@ -1461,3 +1460,17 @@ pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral localConnectionTo :: EndPointAddress -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) localConnectionTo addr = localConnections >>> DAC.mapMaybe addr + +------------------------------------------------------------------------------- +-- Debugging -- +------------------------------------------------------------------------------- + +relyViolation :: EndPointPair -> String -> IO a +relyViolation (ourEndPoint, theirEndPoint) str = do + elog (ourEndPoint, theirEndPoint) (str ++ " RELY violation") + fail (str ++ " RELY violation") + +elog :: EndPointPair -> String -> IO () +elog (ourEndPoint, theirEndPoint) msg = do + tid <- myThreadId + putStrLn $ show (localAddress ourEndPoint) ++ "/" ++ show (remoteAddress theirEndPoint) ++ "/" ++ show tid ++ ": " ++ msg From 5a3b93003e52db8ac6094d3355baa4ae7cd2c359 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 24 May 2012 12:57:18 +0100 Subject: [PATCH 0079/2357] Don't close connection on asynchronous exception In modifyRemoteState we put the remote state in closed state when any exception occured. This is incorrect, however. If a thread that is trying to send gets killed, that doesn't necessary mean that the connection is now broken. Now we close the remote endpoint only if an IO exception occurs. --- src/Network/Transport/Internal.hs | 4 ++-- src/Network/Transport/TCP.hs | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index ea1cb205..0b9d7999 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -9,9 +9,9 @@ module Network.Transport.Internal ( -- * Encoders/decoders , mapIOException , tryIO , tryToEnum - -- * Replicated functionality from "base" + -- * Replicated functionality from "base" , void - , forkIOWithUnmask + , forkIOWithUnmask -- * Debugging , tlog ) where diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 26b5d7e2..be6b8d99 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -84,6 +84,7 @@ import Control.Exception ( IOException , mask , mask_ , onException + , fromException ) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) @@ -542,7 +543,7 @@ apiCloseEndPoint transport evs ourEndPoint = do -- TODO: should we signal on resolved here? tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed - RemoteEndPointClosed -> + RemoteEndPointClosed -> return RemoteEndPointClosed -- | Special case of 'apiConnect': connect an endpoint to itself @@ -1031,7 +1032,7 @@ remoteStateIdentity = , caseClosed = return RemoteEndPointClosed } --- | Like modifyMVar, but if an exception occurs don't restore the remote +-- | Like modifyMVar, but if an I/O exception occurs don't restore the remote -- endpoint to its original value but close it instead modifyRemoteState :: EndPointPair -> RemoteStatePatternMatch (RemoteState, a) @@ -1046,8 +1047,11 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = Right (st', a) -> do putMVar theirState st' return a - Left ex -> - handleException ex vst + Left ex -> do + case fromException ex of + Just ioEx -> handleIOException ioEx vst + Nothing -> putMVar theirState st + throw ex -- The other cases are less interesting, because unless the endpoint is -- in Valid state we're not supposed to do any IO on it RemoteEndPointClosing resolved vst -> do @@ -1069,8 +1073,8 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = theirState :: MVar RemoteState theirState = remoteState theirEndPoint - handleException :: SomeException -> ValidRemoteEndPointState -> IO a - handleException ex vst = do + handleIOException :: IOException -> ValidRemoteEndPointState -> IO () + handleIOException ex vst = do tryCloseSocket (remoteSocket vst) removeRemoteEndPoint (ourEndPoint, theirEndPoint) putMVar theirState RemoteEndPointClosed @@ -1078,7 +1082,6 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = code = EventConnectionLost (remoteAddress theirEndPoint) incoming err = TransportError code (show ex) writeChan (localChannel ourEndPoint) $ ErrorEvent err - throwIO ex -- | Like 'modifyRemoteState' but without a return value modifyRemoteState_ :: EndPointPair @@ -1330,7 +1333,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Close the socket (if we don't have any outgoing connections) closeSocket :: N.Socket -> IO Bool - closeSocket sock = do + closeSocket sock = modifyMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> From 8bffb63f9649f080a1074c359cb592ccf5b371f7 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 24 May 2012 13:16:22 +0100 Subject: [PATCH 0080/2357] Test sending an exceptional value --- tests/TestAuxiliary.hs | 9 +++++++++ tests/TestTransport.hs | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index 0570f697..c1a48c9f 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -4,10 +4,12 @@ module TestAuxiliary ( -- Running tests , runTests -- Writing tests , forkTry + , trySome ) where import Prelude hiding (catch) import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo) +import Control.Concurrent.Chan (Chan) import Control.Monad (liftM2, unless) import Control.Exception (SomeException, try, catch) import System.Timeout (timeout) @@ -27,6 +29,10 @@ forkTry p = do tid <- myThreadId forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) +-- | Like try, but specialized to SomeException +trySome :: IO a -> IO (Either SomeException a) +trySome = try + -- | Run the given test, catching timeouts and exceptions runTest :: String -> IO () -> IO Bool runTest description test = do @@ -86,3 +92,6 @@ instance Traceable SomeException where instance Traceable ThreadId where trace = const Nothing + +instance Traceable (Chan a) where + trace = const Nothing diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index a62fded0..42ed2d10 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -2,9 +2,9 @@ module TestTransport where import Prelude hiding (catch, (>>=), (>>), return, fail) -import TestAuxiliary (forkTry, runTests) +import TestAuxiliary (forkTry, runTests, trySome) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) -import Control.Exception (evaluate, try, SomeException) +import Control.Exception (evaluate, throw) import Control.Monad (replicateM, replicateM_, when, guard, forM_) import Control.Monad.Error () import Network.Transport hiding (connect) @@ -745,13 +745,43 @@ testExceptionOnReceive newTransport = do Right endpoint1 <- newEndPoint transport closeEndPoint endpoint1 EndPointClosed <- receive endpoint1 - Left _ <- (try :: IO a -> IO (Either SomeException a)) (receive endpoint1 >>= evaluate) + Left _ <- trySome (receive endpoint1 >>= evaluate) -- Test two: when we close the entire transport Right endpoint2 <- newEndPoint transport closeTransport transport EndPointClosed <- receive endpoint2 - Left _ <- (try :: IO a -> IO (Either SomeException a)) (receive endpoint2 >>= evaluate) + Left _ <- trySome (receive endpoint2 >>= evaluate) + + return () + +testSendException :: IO (Either String Transport) -> IO () +testSendException newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + + -- Connect endpoint1 to endpoint2 + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered + ConnectionOpened _ _ _ <- receive endpoint2 + + -- Send an exceptional value + Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") + + -- This will have been as a failure to send by endpoint1, which will + -- therefore have closed the socket. In turn this will have caused endpoint2 + -- to report that the connection was lost + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint1 + ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 + + -- A new connection will re-establish the connection + Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered + send conn2 ["ping"] + close conn2 + + ConnectionOpened _ _ _ <- receive endpoint2 + Received _ ["ping"] <- receive endpoint2 + ConnectionClosed _ <- receive endpoint2 return () @@ -775,6 +805,7 @@ testTransport newTransport = do , ("CloseTransport", testCloseTransport newTransport) , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) , ("ExceptionOnReceive", testExceptionOnReceive newTransport) + , ("SendException", testSendException newTransport) ] where numPings = 10000 :: Int From 462b1a3a73b90126087fe034dd58711efb4f95d1 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 24 May 2012 16:55:54 +0100 Subject: [PATCH 0081/2357] Another instance of the async exception bug --- network-transport.cabal | 11 ++++--- src/Network/Transport/TCP.hs | 23 +++++++-------- tests/TestAuxiliary.hs | 15 ++++++++-- tests/TestTCP.hs | 2 +- tests/TestTransport.hs | 57 ++++++++++++++++++++++++++++++++++-- 5 files changed, 86 insertions(+), 22 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index d16f56ff..05a61fcf 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -37,9 +37,10 @@ Test-Suite TestTCP network, mtl, transformers, - ansi-terminal + ansi-terminal, + random extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances - ghc-options: -Wall -fno-warn-unused-do-bind + ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N HS-Source-Dirs: tests src Test-Suite TestMulticastInMemory @@ -51,7 +52,8 @@ Test-Suite TestMulticastInMemory data-accessor, mtl, transformers, - ansi-terminal + ansi-terminal, + random extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -65,7 +67,8 @@ Test-Suite TestInMemory data-accessor, mtl, transformers, - ansi-terminal + ansi-terminal, + random extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index be6b8d99..f37fa0bd 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -483,7 +483,7 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = -- instance if the connection is closed) with the outer exception (which is -- returned by 'try' when an exception is thrown by 'sendOn', and handled -- by 'withRemoteState') - liftM join . try . mapIOException sendFailed $ + liftM join . try . mapIOException sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = \_ -> relyViolation (ourEndPoint, theirEndPoint) "apiSend" @@ -679,7 +679,7 @@ requestConnectionTo ourEndPoint theirAddress = go findRemoteEndPoint ourEndPoint theirAddress -- If it was new, start listening for incoming messages, too - when isNew . void . forkEndPointThread ourEndPoint $ + when isNew . void . forkEndPointThread ourEndPoint $ setupRemoteEndPoint (ourEndPoint, theirEndPoint) let theirState = remoteState theirEndPoint @@ -703,15 +703,15 @@ requestConnectionTo ourEndPoint theirAddress = go -- endpoint state, and might have changed in the meantime, these changes -- won't matter. case endPointStateSnapshot of - RemoteEndPointInvalid err -> + RemoteEndPointInvalid err -> throwIO err - RemoteEndPointClosing resolved _ -> + RemoteEndPointClosing resolved _ -> -- If the remote endpoint is closing, then we need to block until -- this is resolved and we then try again readMVar resolved >> go - RemoteEndPointClosed -> + RemoteEndPointClosed -> -- RemoteEndPointClosed indicates that a concurrent thread was in the -- process of closing the TCP connection to the remote endpoint when -- we obtained a reference to it. By INV-CLOSE we can assume that the @@ -728,9 +728,9 @@ requestConnectionTo ourEndPoint theirAddress = go reply <- mapIOException connectFailed $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId case decodeInt32 . BS.concat $ reply of - Nothing -> + Nothing -> throwIO (connectFailed $ userError "Invalid integer") - Just cid -> + Just cid -> return (theirEndPoint, cid) connectFailed = TransportError ConnectFailed . show @@ -772,15 +772,14 @@ findRemoteEndPoint ourEndPoint theirAddress = setupRemoteEndPoint :: EndPointPair -> IO () setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress) - onError $ \result -> + onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do let vst = ValidRemoteEndPointState { remoteSocket = sock , _remoteOutgoing = 0 , _remoteIncoming = IntSet.empty - , sendOn = (`onException` tryCloseSocket sock) - . sendMany sock + , sendOn = sendMany sock } putMVar theirState (RemoteEndPointValid vst) return True @@ -871,7 +870,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do -- -- If the local endpoint is closed, do nothing removeRemoteEndPoint :: EndPointPair -> IO () -removeRemoteEndPoint (ourEndPoint, theirEndPoint) = +removeRemoteEndPoint (ourEndPoint, theirEndPoint) = modifyMVar_ ourState $ \st -> case st of LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of @@ -956,7 +955,7 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do -- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO () -closeIfUnused (ourEndPoint, theirEndPoint) = +closeIfUnused (ourEndPoint, theirEndPoint) = modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity { caseValid = \vst -> if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index c1a48c9f..4a8035ec 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -5,10 +5,11 @@ module TestAuxiliary ( -- Running tests -- Writing tests , forkTry , trySome + , randomThreadDelay ) where import Prelude hiding (catch) -import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo) +import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) import Control.Concurrent.Chan (Chan) import Control.Monad (liftM2, unless) import Control.Exception (SomeException, try, catch) @@ -20,6 +21,7 @@ import System.Console.ANSI ( SGR(SetColor, Reset) , ColorIntensity(Vivid) , setSGR ) +import System.Random (randomIO) import Network.Transport import Traced (Traceable(..), traceShow) @@ -38,7 +40,7 @@ runTest :: String -> IO () -> IO Bool runTest description test = do putStr $ "Running " ++ show description ++ ": " hFlush stdout - done <- try . timeout 20000000 $ test -- 20 seconds + done <- try . timeout 30000000 $ test -- 30 seconds case done of Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" Right Nothing -> failed $ "(timeout)" @@ -65,6 +67,12 @@ runTests tests = do success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests unless success $ fail "Some tests failed" +-- | Random thread delay between 0 and the specified max +randomThreadDelay :: Int -> IO () +randomThreadDelay maxDelay = do + delay <- randomIO :: IO Int + threadDelay (delay `mod` maxDelay) + -------------------------------------------------------------------------------- -- traceShow instances -- -------------------------------------------------------------------------------- @@ -95,3 +103,6 @@ instance Traceable ThreadId where instance Traceable (Chan a) where trace = const Nothing + +instance Traceable Float where + trace = traceShow diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index f1a73e63..fa1125e6 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -588,7 +588,7 @@ testReconnect nextPort = do Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- But a send will fail because the server has closed the connection again - Left (TransportError SendClosed _) <- send conn1 ["ping"] + Left (TransportError SendFailed _) <- send conn1 ["ping"] ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint -- But a subsequent call to connect should reestablish the connection diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 42ed2d10..becc1494 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -2,20 +2,23 @@ module TestTransport where import Prelude hiding (catch, (>>=), (>>), return, fail) -import TestAuxiliary (forkTry, runTests, trySome) +import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) +import Control.Concurrent (forkIO, killThread) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) import Control.Exception (evaluate, throw) -import Control.Monad (replicateM, replicateM_, when, guard, forM_) +import Control.Monad (replicateM, replicateM_, when, guard, forM, forM_) import Control.Monad.Error () import Network.Transport hiding (connect) import qualified Network.Transport as NT import Network.Transport.Internal (tlog) import Network.Transport.Util (spawn) +import System.Random (randomIO) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) import Data.Map (Map) import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) import Data.String (fromString) +import Data.Maybe (catMaybes) import Traced -- | We overload connect to always pass the default hints @@ -648,9 +651,9 @@ testCloseTransport newTransport = do -- Client now closes down its transport. We should receive connection closed messages -- TODO: this assumes a certain ordering on the messages we receive; that's not guaranteed + ConnectionClosed cid1' <- receive endpoint ; True <- return $ cid1' == cid1 ConnectionClosed cid2'' <- receive endpoint ; True <- return $ cid2'' == cid2 ErrorEvent (TransportError (EventConnectionLost addr'' []) _) <- receive endpoint ; True <- return $ addr'' == theirAddr2 - ConnectionClosed cid1' <- receive endpoint ; True <- return $ cid1' == cid1 -- An attempt to send to the endpoint should now fail Left (TransportError SendClosed _) <- send conn ["pong2"] @@ -755,6 +758,7 @@ testExceptionOnReceive newTransport = do return () +-- | Test what happens when the argument to 'send' is an exceptional value testSendException :: IO (Either String Transport) -> IO () testSendException newTransport = do Right transport <- newTransport @@ -785,6 +789,52 @@ testSendException newTransport = do return () +-- | If threads get killed while executing a 'connect', 'send', or 'close', this +-- should not affect other threads. +-- +-- The intention of this test is to see what happens when a asynchronous +-- exception happes _while executing a send, connect, or close_. This is +-- exceedingly difficult to guarantee, however. Hence we run a large number of +-- tests and insert random thread delays -- and even then it might not happen. +-- Moreover, it will only happen when we run on multiple cores. +testKill :: IO (Either String Transport) -> Int -> IO () +testKill newTransport numThreads = do + Right transport1 <- newTransport + Right transport2 <- newTransport + Right endpoint1 <- newEndPoint transport1 + Right endpoint2 <- newEndPoint transport2 + + threads <- forM [1 .. numThreads] $ \_ -> do + done <- newEmptyMVar + tid <- forkIO $ do + randomThreadDelay 10 + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered + randomThreadDelay 10 + Right () <- send conn ["ping"] + randomThreadDelay 10 + close conn + putMVar done () + + return (tid, done) + + -- Kill half of those threads, and wait on the rest + killerDone <- newEmptyMVar + forkIO $ do + wait <- forM threads $ \(tid, done) -> do + shouldKill <- randomIO + if shouldKill + then do + randomThreadDelay 30 + killThread tid + return Nothing + else + return (Just done) + + mapM_ takeMVar (catMaybes wait) + putMVar killerDone () + + takeMVar killerDone + -- Transport tests testTransport :: IO (Either String Transport) -> IO () testTransport newTransport = do @@ -806,6 +856,7 @@ testTransport newTransport = do , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) , ("ExceptionOnReceive", testExceptionOnReceive newTransport) , ("SendException", testSendException newTransport) + , ("Kill", testKill newTransport 100000) ] where numPings = 10000 :: Int From 3699b67faf0b91f32a4a25f761a2f2d381909d6d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 25 May 2012 16:49:07 +0100 Subject: [PATCH 0082/2357] Document and fix (?) race condition testCloseTwice is still failing occassionally however. Problem hasn't been completely solved, but I can't figure out what's going on :( --- src/Network/Transport/Internal.hs | 27 +++- src/Network/Transport/TCP.hs | 229 +++++++++++++++--------------- tests/TestTCP.hs | 25 +++- tests/TestTransport.hs | 10 +- 4 files changed, 164 insertions(+), 127 deletions(-) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 0b9d7999..97a510b3 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -9,6 +9,7 @@ module Network.Transport.Internal ( -- * Encoders/decoders , mapIOException , tryIO , tryToEnum + , tryModifyMVar -- * Replicated functionality from "base" , void , forkIOWithUnmask @@ -22,10 +23,20 @@ import Foreign.C (CInt(..), CShort(..)) import Foreign.ForeignPtr (withForeignPtr) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length) -import qualified Data.ByteString.Internal as BSI (unsafeCreate, toForeignPtr, inlinePerformIO) +import qualified Data.ByteString.Internal as BSI ( unsafeCreate + , toForeignPtr + , inlinePerformIO) import Control.Monad.IO.Class (MonadIO, liftIO) -import Control.Exception (IOException, Exception, catch, try, throwIO) +import Control.Exception ( IOException + , Exception + , catch + , try + , throwIO + , mask + , onException + ) import Control.Concurrent (ThreadId, forkIO) +import Control.Concurrent (MVar, tryTakeMVar, putMVar) import GHC.IO (unsafeUnmask) --import Control.Concurrent (myThreadId) @@ -99,3 +110,15 @@ tryToEnum = go minBound maxBound where go :: Enum b => b -> b -> Int -> Maybe b go lo hi n = if fromEnum lo <= n && n <= fromEnum hi then Just (toEnum n) else Nothing + +tryModifyMVar :: MVar a -> (a -> IO (a, b)) -> IO (Maybe b) +tryModifyMVar m io = + mask $ \restore -> do + ma <- tryTakeMVar m + case ma of + Nothing -> return Nothing + Just a -> do + (a', b) <- restore (io a) `onException` putMVar m a + putMVar m a' + return (Just b) + diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index f37fa0bd..657540d1 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -39,6 +39,7 @@ import Network.Transport.Internal ( encodeInt32 , mapIOException , tryIO , tryToEnum + , tryModifyMVar , void ) import qualified Network.Socket as N ( HostName @@ -323,7 +324,7 @@ data ConnectionRequestResponse = -- | /B/ accepts the connection ConnectionRequestAccepted -- | /A/ requested an invalid endpoint - | ConnectionRequestEndPointInvalid + | ConnectionRequestInvalid -- | /A/s request crossed with a request from /B/ (see protocols) | ConnectionRequestCrossed deriving (Enum, Bounded, Show) @@ -495,10 +496,16 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = return . Right $ () else return . Left $ TransportError SendClosed "Connection closed" - , caseClosing = \_ _ -> - return . Left $ TransportError SendClosed "Connection lost" - , caseClosed = - return . Left $ TransportError SendClosed "Connection lost" + , caseClosing = \_ _ -> do + alive <- readIORef connAlive + if alive + then return . Left $ TransportError SendFailed "Connection lost" + else return . Left $ TransportError SendClosed "Connection closed" + , caseClosed = do + alive <- readIORef connAlive + if alive + then return . Left $ TransportError SendFailed "Connection lost" + else return . Left $ TransportError SendClosed "Connection closed" } where sendFailed = TransportError SendFailed . show @@ -674,116 +681,46 @@ requestConnectionTo :: LocalEndPoint requestConnectionTo ourEndPoint theirAddress = go where go = do - -- Find the remote endpoint (create it if it doesn't yet exist) (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress - - -- If it was new, start listening for incoming messages, too - when isNew . void . forkEndPointThread ourEndPoint $ - setupRemoteEndPoint (ourEndPoint, theirEndPoint) - - let theirState = remoteState theirEndPoint - - -- Before we initiate the new connection request we want to make sure - -- that refcount on the endpoint is incremented so that a concurrent - -- thread will not close the connection. - endPointStateSnapshot <- modifyMVar theirState $ \st -> - case st of - RemoteEndPointValid ep -> - return (RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ ep, st) - _ -> - return (st, st) - - -- From this point on we are guaranteed the refcount is positive, - -- provided that the endpoint was valid. We still need to deal with the - -- case where it was not valid, however, which we didn't want to do while - -- holding the endpoint lock. - -- - -- Although 'endPointStateSnapshot' here refers to a snapshot of the - -- endpoint state, and might have changed in the meantime, these changes - -- won't matter. - case endPointStateSnapshot of - RemoteEndPointInvalid err -> - throwIO err - - RemoteEndPointClosing resolved _ -> - -- If the remote endpoint is closing, then we need to block until - -- this is resolved and we then try again - readMVar resolved >> go - - RemoteEndPointClosed -> - -- RemoteEndPointClosed indicates that a concurrent thread was in the - -- process of closing the TCP connection to the remote endpoint when - -- we obtained a reference to it. By INV-CLOSE we can assume that the - -- remote endpoint will now have been removed from ourState, so we - -- simply try again. - go - - RemoteEndPointValid _ -> do - -- Do the actual connection request. This blocks until the remote - -- endpoint replies (but note that we don't hold any locks at this - -- point). Note that doRemoteRequest may throw an error if the send - -- fails, and if it does, it will have put the remote endpoint in - -- closed state. - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - case decodeInt32 . BS.concat $ reply of - Nothing -> - throwIO (connectFailed $ userError "Invalid integer") - Just cid -> - return (theirEndPoint, cid) + findRemoteEndPoint ourEndPoint theirAddress + (Just $ RemoteEndPointValid . (remoteOutgoing ^: (+ 1))) - connectFailed = TransportError ConnectFailed . show + when isNew . void . forkEndPointThread ourEndPoint $ do + let outgoing = 1 + setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing --- | Find a remote endpoint. Create an uninitialized remote endpoint if the --- remote endpoint did not yet exist. --- --- Throws an IOException if the local endpoint is closed. -findRemoteEndPoint :: LocalEndPoint -- ^ Our endpoint - -> EndPointAddress -- ^ Their address - -> IO (RemoteEndPoint, Bool) -- ^ Remote endpoint, new? -findRemoteEndPoint ourEndPoint theirAddress = - modifyMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Just theirEndPoint -> - return (st, (theirEndPoint, False)) - Nothing -> do - theirState <- newEmptyMVar - let theirEndPoint = RemoteEndPoint - { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId - } - return ( LocalEndPointValid - . (localConnectionTo theirAddress ^= Just theirEndPoint) - . (nextRemoteId ^: (+ 1)) - $ vst - , (theirEndPoint, True) - ) + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + case decodeInt32 . BS.concat $ reply of + Nothing -> + throwIO (connectFailed $ userError "Invalid integer") + Just cid -> do + return (theirEndPoint, cid) + + connectFailed = TransportError ConnectFailed . show -- | Set up a remote endpoint -- -- RELY: The state of the remote endpoint must be uninitialized. -- GUARANTEE: Will only change the state to RemoteEndPointValid or -- RemoteEndPointInvalid. -setupRemoteEndPoint :: EndPointPair -> IO () -setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do +setupRemoteEndPoint :: EndPointPair -- ^ Local endpoint, remote endpoint + -> Int -- ^ Initial (outgoing) refcount + -> IO () +setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress) onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do let vst = ValidRemoteEndPointState { remoteSocket = sock - , _remoteOutgoing = 0 + , _remoteOutgoing = outgoing , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } putMVar theirState (RemoteEndPointValid vst) return True - Right (sock, ConnectionRequestEndPointInvalid) -> do + Right (sock, ConnectionRequestInvalid) -> do -- We remove the endpoint from our local state again because the next -- call to 'connect' might give a different result. Threads that were -- waiting on the result of this call to connect will get the @@ -946,10 +883,10 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do LocalEndPointClosed -> throwIO (userError "Local endpoint closed") withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseInvalid = \_ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest" + { caseInvalid = throwIO , caseValid = \vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId] - , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest" - , caseClosed = throwIO (userError "Remote endpoint closed") + , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" + , caseClosed = throwIO $ userError "Remote endpoint closed" } takeMVar reply @@ -1050,7 +987,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = case fromException ex of Just ioEx -> handleIOException ioEx vst Nothing -> putMVar theirState st - throw ex + throwIO ex -- The other cases are less interesting, because unless the endpoint is -- in Valid state we're not supposed to do any IO on it RemoteEndPointClosing resolved vst -> do @@ -1145,7 +1082,7 @@ handleConnectionRequest transport sock = handle handleException $ do TransportValid vst -> case vst ^. localEndPointAt ourAddress of Nothing -> do - sendMany sock [encodeInt32 ConnectionRequestEndPointInvalid] + sendMany sock [encodeInt32 ConnectionRequestInvalid] throwIO $ userError "Invalid endpoint" Just ourEndPoint -> return ourEndPoint @@ -1156,7 +1093,9 @@ handleConnectionRequest transport sock = handle handleException $ do go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do - (theirEndPoint, isNew) <- findRemoteEndPoint ourEndPoint theirAddress + (theirEndPoint, isNew) <- + findRemoteEndPoint ourEndPoint theirAddress Nothing + let crossed = not isNew && localAddress ourEndPoint < theirAddress if crossed then do @@ -1170,12 +1109,8 @@ handleConnectionRequest transport sock = handle handleException $ do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } - sendMany sock [encodeInt32 ConnectionRequestAccepted] - -- If the remote endpoint (due to a bug) attempts to connect the - -- same local endpoint twice, the sceond attempt wil have been - -- rejected with ConnectionRequestCrossed and so will never get to - -- this point putMVar (remoteState theirEndPoint) (RemoteEndPointValid vst) + sendMany sock [encodeInt32 ConnectionRequestAccepted] return (Just theirEndPoint) -- If we left the scope of the exception handler with a return value of -- Nothing then the socket is already closed; otherwise, the socket has @@ -1189,9 +1124,75 @@ handleConnectionRequest transport sock = handle handleException $ do invalidEndPoint :: IOException -> IO () invalidEndPoint _ = do - tryIO $ sendMany sock [encodeInt32 ConnectionRequestEndPointInvalid] + tryIO $ sendMany sock [encodeInt32 ConnectionRequestInvalid] tryCloseSocket sock +-- | Find a remote endpoint. Pass 'Nothing' for the third argument to find an +-- uninitialized remote endpoint; pass 'Just' a function to find a Valid remote +-- endpoint (and modify its state). +findRemoteEndPoint + :: LocalEndPoint + -> EndPointAddress + -> Maybe (ValidRemoteEndPointState -> RemoteState) + -> IO (RemoteEndPoint, Bool) +findRemoteEndPoint ourEndPoint theirAddress mCaseValid = go + where + go = do + (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Just theirEndPoint -> + return (st, (theirEndPoint, False)) + Nothing -> do + theirState <- newEmptyMVar + let theirEndPoint = RemoteEndPoint + { remoteAddress = theirAddress + , remoteState = theirState + , remoteId = vst ^. nextRemoteId + } + return ( LocalEndPointValid + . (localConnectionTo theirAddress ^= Just theirEndPoint) + . (nextRemoteId ^: (+ 1)) + $ vst + , (theirEndPoint, True) + ) + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + + if isNew + then + return (theirEndPoint, True) + else do + let theirState = remoteState theirEndPoint + snapshot <- modify theirState $ \whenValid st -> case st of + RemoteEndPointInvalid _ -> + return (st, st) + RemoteEndPointValid vst -> do + let st' = whenValid vst + return (st', st') + RemoteEndPointClosing _ _ -> + return (st, st) + RemoteEndPointClosed -> + return (st, st) + case snapshot of + Nothing -> + return (theirEndPoint, False) + Just (RemoteEndPointInvalid _) -> + throwIO $ userError "Invalid endpoint" + Just (RemoteEndPointClosing resolved _) -> + readMVar resolved >> go + Just RemoteEndPointClosed -> + go + Just (RemoteEndPointValid _) -> + return (theirEndPoint, False) + + ourState = localState ourEndPoint + modify m io = case mCaseValid of + Nothing -> + tryModifyMVar m (io (throw $ userError "Unexpected valid state")) + Just f -> + Just <$> modifyMVar m (io f) + -- | Handle requests from a remote endpoint. -- -- Returns only if the remote party closes the socket or if an error occurs. @@ -1251,7 +1252,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do didClose <- closeSocket sock unless didClose $ go sock Nothing -> - throwIO $ userError "Warning: invalid control request" + throwIO $ userError "Invalid control request" -- Create a new connection createNewConnection :: ControlRequestId -> IO () @@ -1275,7 +1276,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do putMVar resolved () return (remoteIncoming ^= IntSet.singleton newId $ vst) RemoteEndPointClosed -> - throw $ userError "Remote endpoint closed" + throwIO $ userError "Remote endpoint closed" sendOn vst ( encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 newId] @@ -1301,7 +1302,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do throwIO $ userError "Local endpoint closed" case mmvar of Nothing -> - throwIO $ userError "Warning: Invalid request ID" + throwIO $ userError "Invalid request ID" Just mvar -> putMVar mvar response @@ -1347,20 +1348,20 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Check if we agree that the connection should be closed if vst' ^. remoteOutgoing == 0 then do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- Attempt to reply (but don't insist) tryIO $ sendOn vst' [encodeInt32 CloseSocket] tryCloseSocket sock - removeRemoteEndPoint (ourEndPoint, theirEndPoint) return (RemoteEndPointClosed, True) else return (RemoteEndPointValid vst', False) RemoteEndPointClosing resolved _ -> do - tryCloseSocket sock removeRemoteEndPoint (ourEndPoint, theirEndPoint) + tryCloseSocket sock putMVar resolved () return (RemoteEndPointClosed, True) RemoteEndPointClosed -> - throw $ userError "Remote endpoint closed" + throwIO $ userError "Remote endpoint closed" -- Read a message and output it on the endPoint's channel By rights we -- should verify that the connection ID is valid, but this is unnecessary @@ -1378,8 +1379,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Deal with a premature exit prematureExit :: N.Socket -> IOException -> IO () prematureExit sock err = do - tryCloseSocket sock removeRemoteEndPoint (ourEndPoint, theirEndPoint) + tryCloseSocket sock modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index fa1125e6..cb129c5d 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -29,7 +29,12 @@ import Network.Transport.TCP ( ControlHeader(..) , ConnectionRequestResponse(..) , socketToEndPoint ) -import Network.Transport.Internal (encodeInt32, prependLength, tlog, tryIO, void) +import Network.Transport.Internal ( encodeInt32 + , prependLength + , tlog + , tryIO + , void + ) import Network.Transport.Internal.TCP (recvInt32, forkServer, recvWithLength) import qualified Network.Socket as N ( sClose , ServiceName @@ -116,7 +121,7 @@ testEarlyDisconnect nextPort = do return () -- TEST 4: A subsequent send on an already-open connection will now break - Left (TransportError SendClosed _) <- send conn ["ping2"] + Left (TransportError SendFailed _) <- send conn ["ping2"] -- *Pfew* putMVar serverDone () @@ -227,7 +232,7 @@ testEarlyCloseSocket nextPort = do return () -- TEST 4: A subsequent send on an already-open connection will now break - Left (TransportError SendClosed _) <- send conn ["ping2"] + Left (TransportError SendFailed _) <- send conn ["ping2"] -- *Pfew* putMVar serverDone () @@ -482,9 +487,10 @@ testUnnecessaryConnect nextPort = do putMVar serverAddr (address endpoint) forkTry $ do - let ourAddress = EndPointAddress "ourAddress" + -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check + let ourAddress = EndPointAddress "126.0.0.1" Right (_, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress - Right (_, ConnectionRequestCrossed) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (_, ConnectionRequestInvalid) <- readMVar serverAddr >>= socketToEndPoint ourAddress putMVar clientDone () takeMVar clientDone @@ -582,7 +588,13 @@ testReconnect nextPort = do -- The second attempt will fail because the server closes the socket before we can request a connection takeMVar endpointCreated - Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + -- This might time out or not, depending on whether the server closes the + -- socket before or after we can send the RequestConnectionId request + resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints + case resultConnect of + Nothing -> return () + Just (Left (TransportError ConnectFailed _)) -> return () + _ -> fail "testReconnect" -- The third attempt succeeds Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -742,3 +754,4 @@ main = do , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") + return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index becc1494..c59d7d23 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -567,7 +567,7 @@ testCloseEndPoint transport _ = do ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' ErrorEvent (TransportError (EventConnectionLost addr' []) _) <- receive endpoint ; True <- return $ addr' == theirAddr - Left (TransportError SendClosed _) <- send conn ["pong2"] + Left (TransportError SendFailed _) <- send conn ["pong2"] return () @@ -605,7 +605,7 @@ testCloseEndPoint transport _ = do EndPointClosed <- receive endpoint -- Attempt to send should fail with connection closed - Left (TransportError SendClosed _) <- send conn ["ping2"] + Left (TransportError SendFailed _) <- send conn ["ping2"] -- An attempt to close the already closed connection should just return () <- close conn @@ -656,7 +656,7 @@ testCloseTransport newTransport = do ErrorEvent (TransportError (EventConnectionLost addr'' []) _) <- receive endpoint ; True <- return $ addr'' == theirAddr2 -- An attempt to send to the endpoint should now fail - Left (TransportError SendClosed _) <- send conn ["pong2"] + Left (TransportError SendFailed _) <- send conn ["pong2"] putMVar serverDone () @@ -691,7 +691,7 @@ testCloseTransport newTransport = do EndPointClosed <- receive endpoint2 -- Attempt to send should fail with connection closed - Left (TransportError SendClosed _) <- send conn ["ping2"] + Left (TransportError SendFailed _) <- send conn ["ping2"] -- An attempt to close the already closed connection should just return () <- close conn @@ -856,7 +856,7 @@ testTransport newTransport = do , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) , ("ExceptionOnReceive", testExceptionOnReceive newTransport) , ("SendException", testSendException newTransport) - , ("Kill", testKill newTransport 100000) + , ("Kill", testKill newTransport 10000) ] where numPings = 10000 :: Int From 3030b1888fbebc5f9d8b63b0172e2e93b9bebd10 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 25 May 2012 20:12:29 +0100 Subject: [PATCH 0083/2357] Race condition fixed. We implicitly represented endpoint who were in "initialization state" as an empty mvar. This does not work, because handleConnectionRequest needs to know the state of the remote endpoint explicitly (this is the bug we fixed in the previous commit), but we can't test an mvar for being empty, because this might equally mean that another thread is in the process of modifying said MVar. Hence, we introduced an additional constructor RemoteEndPointInit for the RemoteState ADT, which carries a "resolved MVar (like RemoteEndPointClosing) which can be used by threads which want to block until the remote endpoint has been fully initialized. --- src/Network/Transport/Internal.hs | 16 --- src/Network/Transport/TCP.hs | 171 ++++++++++++++++++------------ tests/TestTransport.hs | 2 +- 3 files changed, 106 insertions(+), 83 deletions(-) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 97a510b3..6f7c645b 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -9,7 +9,6 @@ module Network.Transport.Internal ( -- * Encoders/decoders , mapIOException , tryIO , tryToEnum - , tryModifyMVar -- * Replicated functionality from "base" , void , forkIOWithUnmask @@ -32,11 +31,8 @@ import Control.Exception ( IOException , catch , try , throwIO - , mask - , onException ) import Control.Concurrent (ThreadId, forkIO) -import Control.Concurrent (MVar, tryTakeMVar, putMVar) import GHC.IO (unsafeUnmask) --import Control.Concurrent (myThreadId) @@ -110,15 +106,3 @@ tryToEnum = go minBound maxBound where go :: Enum b => b -> b -> Int -> Maybe b go lo hi n = if fromEnum lo <= n && n <= fromEnum hi then Just (toEnum n) else Nothing - -tryModifyMVar :: MVar a -> (a -> IO (a, b)) -> IO (Maybe b) -tryModifyMVar m io = - mask $ \restore -> do - ma <- tryTakeMVar m - case ma of - Nothing -> return Nothing - Just a -> do - (a', b) <- restore (io a) `onException` putMVar m a - putMVar m a' - return (Just b) - diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 657540d1..7813dd43 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -39,7 +39,6 @@ import Network.Transport.Internal ( encodeInt32 , mapIOException , tryIO , tryToEnum - , tryModifyMVar , void ) import qualified Network.Socket as N ( HostName @@ -108,6 +107,7 @@ import qualified Data.Map as Map (empty) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) import Data.Foldable (forM_, mapM_) +import Data.Maybe (isNothing) -- $design -- @@ -292,6 +292,7 @@ data RemoteEndPoint = RemoteEndPoint data RemoteState = RemoteEndPointInvalid (TransportError ConnectErrorCode) + | RemoteEndPointInit (MVar ()) | RemoteEndPointValid ValidRemoteEndPointState | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState | RemoteEndPointClosed @@ -487,7 +488,9 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = liftM join . try . mapIOException sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = \_ -> - relyViolation (ourEndPoint, theirEndPoint) "apiSend" + relyViolation (ourEndPoint, theirEndPoint) "apiSend" + , caseInit = \_ -> + relyViolation (ourEndPoint, theirEndPoint) "apiSend" , caseValid = \vst -> do alive <- readIORef connAlive if alive @@ -540,6 +543,8 @@ apiCloseEndPoint transport evs ourEndPoint = do case st of RemoteEndPointInvalid _ -> return st + RemoteEndPointInit _ -> + return st RemoteEndPointValid conn -> do -- Try to send a CloseSocket request tryIO $ sendOn conn [encodeInt32 CloseSocket] @@ -621,6 +626,8 @@ internalSocketBetween transport ourAddress theirAddress = runErrorT $ do Just ep -> return . Right $ ep ErrorT $ withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit _ -> + return . Left $ "Remote endpoint not yet initialized" RemoteEndPointValid vst -> return . Right $ remoteSocket vst RemoteEndPointClosing _ vst -> @@ -682,12 +689,15 @@ requestConnectionTo ourEndPoint theirAddress = go where go = do (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress - (Just $ RemoteEndPointValid . (remoteOutgoing ^: (+ 1))) + findRemoteEndPoint ourEndPoint theirAddress True + (RemoteEndPointValid . (remoteOutgoing ^: (+ 1))) - when isNew . void . forkEndPointThread ourEndPoint $ do - let outgoing = 1 - setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing + -- If this is a new endpoint, start the initialization process and wait + forM_ isNew $ \resolved -> do + void . forkEndPointThread ourEndPoint $ do + let outgoing = 1 + setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing + readMVar resolved reply <- mapIOException connectFailed $ doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId @@ -718,7 +728,9 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } - putMVar theirState (RemoteEndPointValid vst) + modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do + putMVar resolved () + return (RemoteEndPointValid vst) return True Right (sock, ConnectionRequestInvalid) -> do -- We remove the endpoint from our local state again because the next @@ -728,15 +740,21 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do -- connection requests. removeRemoteEndPoint (ourEndPoint, theirEndPoint) let err = invalidAddress "Invalid endpoint" - putMVar theirState (RemoteEndPointInvalid err) + modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do + putMVar resolved () + return (RemoteEndPointInvalid err) tryCloseSocket sock return False Right (sock, ConnectionRequestCrossed) -> do + -- We leave the endpoint in Init state, handleConnectionRequest will + -- take care of it tryCloseSocket sock return False Left err -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- See comment above - putMVar theirState (RemoteEndPointInvalid err) + modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do + putMVar resolved () + return (RemoteEndPointInvalid err) return False -- If we get to this point without an exception, then @@ -884,6 +902,7 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do throwIO (userError "Local endpoint closed") withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = throwIO + , caseInit = \_ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" , caseValid = \vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId] , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" , caseClosed = throwIO $ userError "Remote endpoint closed" @@ -954,6 +973,7 @@ forkEndPointThread ourEndPoint p = data RemoteStatePatternMatch a = RemoteStatePatternMatch { caseInvalid :: TransportError ConnectErrorCode -> IO a + , caseInit :: MVar () -> IO a , caseValid :: ValidRemoteEndPointState -> IO a , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a , caseClosed :: IO a @@ -963,6 +983,7 @@ remoteStateIdentity :: RemoteStatePatternMatch RemoteState remoteStateIdentity = RemoteStatePatternMatch { caseInvalid = return . RemoteEndPointInvalid + , caseInit = return . RemoteEndPointInit , caseValid = return . RemoteEndPointValid , caseClosing = (return .) . RemoteEndPointClosing , caseClosed = return RemoteEndPointClosed @@ -990,6 +1011,11 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = throwIO ex -- The other cases are less interesting, because unless the endpoint is -- in Valid state we're not supposed to do any IO on it + RemoteEndPointInit resolved -> do + (st', a) <- onException (restore $ caseInit match resolved) + (putMVar theirState st) + putMVar theirState st' + return a RemoteEndPointClosing resolved vst -> do (st', a) <- onException (restore $ caseClosing match resolved vst) (putMVar theirState st) @@ -1027,6 +1053,7 @@ modifyRemoteState_ (ourEndPoint, theirEndPoint) match = modifyRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = u . caseInvalid match + , caseInit = u . caseInit match , caseValid = u . caseValid match , caseClosing = \resolved vst -> u $ caseClosing match resolved vst , caseClosed = u $ caseClosed match @@ -1045,6 +1072,9 @@ withRemoteState (ourEndPoint, theirEndPoint) match = { caseInvalid = \err -> do a <- caseInvalid match err return (RemoteEndPointInvalid err, a) + , caseInit = \resolved -> do + a <- caseInit match resolved + return (RemoteEndPointInit resolved, a) , caseValid = \vst -> do a <- caseValid match vst return (RemoteEndPointValid vst, a) @@ -1094,9 +1124,13 @@ handleConnectionRequest transport sock = handle handleException $ do go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do (theirEndPoint, isNew) <- - findRemoteEndPoint ourEndPoint theirAddress Nothing + findRemoteEndPoint ourEndPoint theirAddress False + (throw $ userError "Already connected") + + let ourAddress = localAddress ourEndPoint + theirState = remoteState theirEndPoint + crossed = isNothing isNew && ourAddress < theirAddress - let crossed = not isNew && localAddress ourEndPoint < theirAddress if crossed then do tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] @@ -1109,7 +1143,9 @@ handleConnectionRequest transport sock = handle handleException $ do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } - putMVar (remoteState theirEndPoint) (RemoteEndPointValid vst) + modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do + putMVar resolved () + return (RemoteEndPointValid vst) sendMany sock [encodeInt32 ConnectionRequestAccepted] return (Just theirEndPoint) -- If we left the scope of the exception handler with a return value of @@ -1127,71 +1163,64 @@ handleConnectionRequest transport sock = handle handleException $ do tryIO $ sendMany sock [encodeInt32 ConnectionRequestInvalid] tryCloseSocket sock --- | Find a remote endpoint. Pass 'Nothing' for the third argument to find an --- uninitialized remote endpoint; pass 'Just' a function to find a Valid remote --- endpoint (and modify its state). +-- | Find a remote endpoint. If the remote endpoint does not yet exist we +-- create it in Init state and return the corresponding 'resolved' MVar. findRemoteEndPoint - :: LocalEndPoint - -> EndPointAddress - -> Maybe (ValidRemoteEndPointState -> RemoteState) - -> IO (RemoteEndPoint, Bool) -findRemoteEndPoint ourEndPoint theirAddress mCaseValid = go + :: LocalEndPoint -- ^ Local endpoint + -> EndPointAddress -- ^ Remote address + -> Bool -- ^ Wait if remote endpoint is in Init state + -> (ValidRemoteEndPointState -> RemoteState) -- ^ Apply if in Valid state + -> IO (RemoteEndPoint, Maybe (MVar ())) +findRemoteEndPoint ourEndPoint theirAddress waitOnInit whenValid = go where go = do (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Just theirEndPoint -> - return (st, (theirEndPoint, False)) - Nothing -> do - theirState <- newEmptyMVar - let theirEndPoint = RemoteEndPoint - { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId - } - return ( LocalEndPointValid - . (localConnectionTo theirAddress ^= Just theirEndPoint) - . (nextRemoteId ^: (+ 1)) - $ vst - , (theirEndPoint, True) - ) + LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of + Just theirEndPoint -> + return (st, (theirEndPoint, Nothing)) + Nothing -> do + resolved <- newEmptyMVar + theirState <- newMVar (RemoteEndPointInit resolved) + let theirEndPoint = RemoteEndPoint + { remoteAddress = theirAddress + , remoteState = theirState + , remoteId = vst ^. nextRemoteId + } + return ( LocalEndPointValid + . (localConnectionTo theirAddress ^= Just theirEndPoint) + . (nextRemoteId ^: (+ 1)) + $ vst + , (theirEndPoint, Just resolved) + ) LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" - - if isNew - then - return (theirEndPoint, True) - else do + + case isNew of + Just resolved -> + return (theirEndPoint, Just resolved) + Nothing -> do let theirState = remoteState theirEndPoint - snapshot <- modify theirState $ \whenValid st -> case st of - RemoteEndPointInvalid _ -> - return (st, st) + snapshot <- modifyMVar theirState $ \st -> case st of RemoteEndPointValid vst -> do - let st' = whenValid vst + let st' = whenValid vst return (st', st') - RemoteEndPointClosing _ _ -> - return (st, st) - RemoteEndPointClosed -> + _ -> return (st, st) case snapshot of - Nothing -> - return (theirEndPoint, False) - Just (RemoteEndPointInvalid _) -> - throwIO $ userError "Invalid endpoint" - Just (RemoteEndPointClosing resolved _) -> + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointInit resolved -> + if waitOnInit + then readMVar resolved >> go + else return (theirEndPoint, Nothing) + RemoteEndPointValid _ -> + return (theirEndPoint, Nothing) + RemoteEndPointClosing resolved _ -> readMVar resolved >> go - Just RemoteEndPointClosed -> + RemoteEndPointClosed -> go - Just (RemoteEndPointValid _) -> - return (theirEndPoint, False) - - ourState = localState ourEndPoint - modify m io = case mCaseValid of - Nothing -> - tryModifyMVar m (io (throw $ userError "Unexpected valid state")) - Just f -> - Just <$> modifyMVar m (io f) + + ourState = localState ourEndPoint -- | Handle requests from a remote endpoint. -- @@ -1204,7 +1233,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do mSock <- withMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:initialization" + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages" + RemoteEndPointInit _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages" RemoteEndPointValid ep -> return . Just $ remoteSocket ep RemoteEndPointClosing _ ep -> @@ -1264,6 +1295,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do vst <- case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" + RemoteEndPointInit _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" RemoteEndPointValid vst -> return (remoteIncoming ^: IntSet.insert newId $ vst) RemoteEndPointClosing resolved vst -> do @@ -1314,6 +1347,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection" + RemoteEndPointInit _ -> + relyViolation (ourEndPoint, theirEndPoint) "closeConnection" RemoteEndPointValid vst -> do unless (IntSet.member cid (vst ^. remoteIncoming)) $ throwIO $ userError "Invalid CloseConnection" @@ -1338,6 +1373,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" + RemoteEndPointInit _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" RemoteEndPointValid vst -> do -- We regard a CloseSocket message as an (optimized) way for the -- remote endpoint to indicate that all its connections to us are @@ -1385,6 +1422,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" + RemoteEndPointInit _ -> + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do let code = EventConnectionLost (remoteAddress theirEndPoint) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index c59d7d23..2c21ce2e 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -35,7 +35,7 @@ echoServer endpoint = do event <- receive endpoint case event of ConnectionOpened cid rel addr -> do - tlog $ "Opened new conncetion " ++ show cid + tlog $ "Opened new connection " ++ show cid Right conn <- connect endpoint addr rel go (Map.insert cid conn cs) Received cid payload -> do From 2d12b001b0190312dd7affb15a3f92c97358a225 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 28 May 2012 11:29:12 +0100 Subject: [PATCH 0084/2357] Reduce overhead in apiSend Since previously we caught *all* exceptions in modifyRemoteState we need to distinguish between various kinds of exceptions in apiSend manually. Now we only catch IO exceptions, so we can throw TransportErrors normally. --- src/Network/Transport/TCP.hs | 72 +++++++++++++++++++++++------------- tests/TestTransport.hs | 5 +-- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 7813dd43..2a5c9a26 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -72,7 +72,7 @@ import Control.Concurrent.MVar ( MVar ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) -import Control.Monad (when, unless, join, liftM) +import Control.Monad (when, unless) import Control.Monad.Error (ErrorT(..), runErrorT) import Control.Exception ( IOException , SomeException @@ -250,6 +250,43 @@ data ValidLocalEndPointState = ValidLocalEndPointState , _nextRemoteId :: !Int } +-- REMOTE ENDPOINTS +-- +-- Remote endpoints (basically, TCP connections) have the following lifecycle: +-- +-- Init ----+---> Invalid +-- | +-- | /----------------\ +-- | | | +-- | v | +-- \---> Valid ----+---> Closing +-- | | +-- | v +-- \---> Closed +-- +-- Init: There are two places where we create new remote endpoints: in +-- requestConnectionTo (in response to an API 'connect' call) and in +-- handleConnectionRequest (when a remote node tries to connect to us). +-- 'Init' carries an MVar () 'resolved' which concurrent threads can use to +-- wait for the remote endpoint to finish initialization. +-- +-- Invalid: We put the remote endpoint in invalid state only during +-- requestConnectionTo when we fail to connect. +-- +-- Valid: This is the "normal" state for a working remote endpoint. +-- +-- Closing: When we detect that a remote endpoint is no longer used, we send a +-- CloseSocket request across the connection and put the remote endpoint in +-- closing state. As with Init, 'Closing' carries an MVar () 'resolved' which +-- concurrent threads can use to wait for the remote endpoint to either be +-- closed fully (if the communication parnet responds with another +-- CloseSocket) or be put back in 'Valid' state if the remote endpoint denies +-- the request. +-- +-- Closed: The endpoint is put in Closed state after a successful garbage +-- collection, when the endpoint (or the whole transport) is closed manually, +-- or when an IO exception occurs during communication. +-- -- Invariants for dealing with remote endpoints: -- -- INV-SEND: Whenever we send data the remote endpoint must be locked (to avoid @@ -269,7 +306,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- both signal on 'resolved'. -- -- INV-LOST: If a send or recv fails, or a socket is closed unexpectedly, we --- first put the remote endpoint in Closing or Closed state, and then send a +-- first put the remote endpoint in Closed state, and then send a -- EventConnectionLost event. This guarantees that we only send this event -- once. -- @@ -448,11 +485,6 @@ apiConnect ourEndPoint theirAddress _reliability _hints = } -- | Close a connection --- --- RELY: The endpoint must not be invalid --- GUARANTEE: If the connection is alive on entry then the remote endpoint will --- either be RemoteEndPointValid or RemoteEndPointClosing. Otherwise, the --- state of the remote endpoint will not be changed. apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity @@ -472,20 +504,13 @@ apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do closeIfUnused (ourEndPoint, theirEndPoint) -- | Send data across a connection --- --- RELY: The endpoint must not be in invalid state. --- GUARANTEE: The state of the remote endpoint will not be changed. apiSend :: EndPointPair -- ^ Local and remote endpoint -> ConnectionId -- ^ Connection ID (supplied by remote endpoint) -> IORef Bool -- ^ Is the connection still alive? -> [ByteString] -- ^ Payload -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = - -- The 'join' joins the inner exception (which we explicitly return, for - -- instance if the connection is closed) with the outer exception (which is - -- returned by 'try' when an exception is thrown by 'sendOn', and handled - -- by 'withRemoteState') - liftM join . try . mapIOException sendFailed $ + try . mapIOException sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = \_ -> relyViolation (ourEndPoint, theirEndPoint) "apiSend" @@ -494,21 +519,18 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = , caseValid = \vst -> do alive <- readIORef connAlive if alive - then do - sendOn vst (encodeInt32 connId : prependLength payload) - return . Right $ () - else - return . Left $ TransportError SendClosed "Connection closed" + then sendOn vst (encodeInt32 connId : prependLength payload) + else throwIO $ TransportError SendClosed "Connection closed" , caseClosing = \_ _ -> do alive <- readIORef connAlive if alive - then return . Left $ TransportError SendFailed "Connection lost" - else return . Left $ TransportError SendClosed "Connection closed" + then throwIO $ TransportError SendFailed "Connection lost" + else throwIO $ TransportError SendClosed "Connection closed" , caseClosed = do alive <- readIORef connAlive if alive - then return . Left $ TransportError SendFailed "Connection lost" - else return . Left $ TransportError SendClosed "Connection closed" + then throwIO $ TransportError SendFailed "Connection lost" + else throwIO $ TransportError SendClosed "Connection closed" } where sendFailed = TransportError SendFailed . show @@ -704,7 +726,7 @@ requestConnectionTo ourEndPoint theirAddress = go case decodeInt32 . BS.concat $ reply of Nothing -> throwIO (connectFailed $ userError "Invalid integer") - Just cid -> do + Just cid -> return (theirEndPoint, cid) connectFailed = TransportError ConnectFailed . show diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 2c21ce2e..e89ed874 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -650,9 +650,8 @@ testCloseTransport newTransport = do send conn ["pong"] -- Client now closes down its transport. We should receive connection closed messages - -- TODO: this assumes a certain ordering on the messages we receive; that's not guaranteed - ConnectionClosed cid1' <- receive endpoint ; True <- return $ cid1' == cid1 - ConnectionClosed cid2'' <- receive endpoint ; True <- return $ cid2'' == cid2 + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid' == cid1 || cid' == cid2 + ConnectionClosed cid'' <- receive endpoint ; True <- return $ (cid'' == cid1 || cid'' == cid2) && cid'' /= cid' ErrorEvent (TransportError (EventConnectionLost addr'' []) _) <- receive endpoint ; True <- return $ addr'' == theirAddr2 -- An attempt to send to the endpoint should now fail From cf6ceebc6bbb9f987b2ce1440d47cdb3349cada8 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 28 May 2012 12:12:42 +0100 Subject: [PATCH 0085/2357] Better error handling when self-connecting --- src/Network/Transport/TCP.hs | 79 ++++++++++++++++++------------------ tests/TestTCP.hs | 1 - tests/TestTransport.hs | 36 +++++++++++++--- 3 files changed, 71 insertions(+), 45 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 2a5c9a26..8cbd5f05 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -259,10 +259,10 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- | /----------------\ -- | | | -- | v | --- \---> Valid ----+---> Closing --- | | --- | v --- \---> Closed +-- +---> Valid ----+---> Closing +-- | | | +-- | | v +-- \---------------+---> Closed -- -- Init: There are two places where we create new remote endpoints: in -- requestConnectionTo (in response to an API 'connect' call) and in @@ -565,16 +565,15 @@ apiCloseEndPoint transport evs ourEndPoint = do case st of RemoteEndPointInvalid _ -> return st - RemoteEndPointInit _ -> - return st + RemoteEndPointInit resolved -> do + putMVar resolved () + return RemoteEndPointClosed RemoteEndPointValid conn -> do - -- Try to send a CloseSocket request tryIO $ sendOn conn [encodeInt32 CloseSocket] - -- .. but even if it fails, close the socket anyway tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed - RemoteEndPointClosing _ conn -> do - -- TODO: should we signal on resolved here? + RemoteEndPointClosing resolved conn -> do + putMVar resolved () tryCloseSocket (remoteSocket conn) return RemoteEndPointClosed RemoteEndPointClosed -> @@ -583,42 +582,44 @@ apiCloseEndPoint transport evs ourEndPoint = do -- | Special case of 'apiConnect': connect an endpoint to itself connectToSelf :: LocalEndPoint -> IO (Either (TransportError ConnectErrorCode) Connection) -connectToSelf ourEndPoint = do - -- Here connAlive must an MVar because it is not protected by another lock - connAlive <- newMVar True - mConnId <- tryIO (getNextConnectionId ourEndPoint) - case mConnId of - Left err -> - return . Left $ TransportError ConnectNotFound (show err) - Right connId -> do - writeChan ourChan $ - ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) - return . Right $ Connection - { send = selfSend connAlive connId - , close = selfClose connAlive connId - } +connectToSelf ourEndPoint = try . mapIOException connectFailed $ do + connAlive <- newIORef True -- Protected by the local endpoint lock + connId <- getNextConnectionId ourEndPoint + writeChan ourChan $ + ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) + return Connection + { send = selfSend connAlive connId + , close = selfClose connAlive connId + } where - ourChan :: Chan Event - ourChan = localChannel ourEndPoint - - selfSend :: MVar Bool + selfSend :: IORef Bool -> ConnectionId -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) selfSend connAlive connId msg = - modifyMVar connAlive $ \alive -> - if alive - then do - writeChan ourChan (Received connId msg) - return (alive, Right ()) - else - return (alive, Left (TransportError SendFailed "Connection closed")) + try . withMVar ourState $ \st -> case st of + LocalEndPointValid _ -> do + alive <- readIORef connAlive + if alive + then writeChan ourChan (Received connId msg) + else throwIO $ TransportError SendClosed "Connection closed" + LocalEndPointClosed -> + throwIO $ TransportError SendFailed "Endpoint closed" - selfClose :: MVar Bool -> ConnectionId -> IO () + selfClose :: IORef Bool -> ConnectionId -> IO () selfClose connAlive connId = - modifyMVar_ connAlive $ \alive -> do - when alive $ writeChan ourChan (ConnectionClosed connId) - return False + withMVar ourState $ \st -> case st of + LocalEndPointValid _ -> do + alive <- readIORef connAlive + when alive $ do + writeChan ourChan (ConnectionClosed connId) + writeIORef connAlive False + LocalEndPointClosed -> + return () + + ourChan = localChannel ourEndPoint + ourState = localState ourEndPoint + connectFailed = TransportError ConnectFailed . show -------------------------------------------------------------------------------- -- Functions from TransportInternals -- diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index cb129c5d..d538b446 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -754,4 +754,3 @@ main = do , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") - return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index e89ed874..b51e81f0 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -533,6 +533,36 @@ testConnectToSelfTwice transport numPings = do takeMVar done +-- | Test that we self-connections no longer work once we close our endpoint +-- or our transport +testCloseSelf :: IO (Either String Transport) -> IO () +testCloseSelf newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered + Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered + Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered + + -- Close the conneciton and try to send + close conn1 + Left (TransportError SendClosed _) <- send conn1 ["ping"] + + -- Close the first endpoint. We should not be able to use the first + -- connection anymore, or open more self connections, but the self connection + -- to the second endpoint should still be fine + closeEndPoint endpoint1 + Left (TransportError SendFailed _) <- send conn2 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered + Right () <- send conn3 ["ping"] + + -- Close the transport; now the second should no longer work + closeTransport transport + Left (TransportError SendFailed _) <- send conn3 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered + + return () + -- | Test various aspects of 'closeEndPoint' testCloseEndPoint :: Transport -> Int -> IO () testCloseEndPoint transport _ = do @@ -726,13 +756,8 @@ testConnectClosedEndPoint transport = do Right endpoint <- newEndPoint transport readMVar serverClosed - -- Connect to a remote closed endpoint Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered - -- Self-connect to a closed endpoint - closeEndPoint endpoint - Left (TransportError ConnectNotFound _) <- connect endpoint (address endpoint) ReliableOrdered - putMVar clientDone () takeMVar clientDone @@ -850,6 +875,7 @@ testTransport newTransport = do , ("CloseTwice", testCloseTwice transport 100) , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) + , ("CloseSelf", testCloseSelf newTransport) , ("CloseEndPoint", testCloseEndPoint transport numPings) , ("CloseTransport", testCloseTransport newTransport) , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) From 5ba9eb55c4ae3f71ccde2ce654ff0b416a598b4b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 28 May 2012 14:22:19 +0100 Subject: [PATCH 0086/2357] Code cleanup --- src/Network/Transport/Internal.hs | 33 +++--- src/Network/Transport/Internal/TCP.hs | 9 +- src/Network/Transport/TCP.hs | 151 ++++++++++++-------------- tests/TestTCP.hs | 8 +- tests/TestTransport.hs | 2 +- 5 files changed, 97 insertions(+), 106 deletions(-) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 6f7c645b..e03cc86e 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -30,6 +30,7 @@ import Control.Exception ( IOException , Exception , catch , try + , throw , throwIO ) import Control.Concurrent (ThreadId, forkIO) @@ -48,13 +49,15 @@ encodeInt32 i32 = pokeByteOff p 0 (htonl . fromIntegral . fromEnum $ i32) -- | Deserialize 32-bit from network byte order -decodeInt32 :: Num a => ByteString -> Maybe a -decodeInt32 bs | BS.length bs /= 4 = Nothing -decodeInt32 bs = Just . BSI.inlinePerformIO $ do - let (fp, _, _) = BSI.toForeignPtr bs - withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 - return (fromIntegral . ntohl $ w32) +-- Throws an IO exception if this is not a valid integer. +decodeInt32 :: Num a => ByteString -> a +decodeInt32 bs + | BS.length bs /= 4 = throw $ userError "decodeInt32: Invalid length" + | otherwise = BSI.inlinePerformIO $ do + let (fp, _, _) = BSI.toForeignPtr bs + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral . ntohl $ w32) -- | Serialize 16-bit to network byte order encodeInt16 :: Enum a => a -> ByteString @@ -63,13 +66,15 @@ encodeInt16 i16 = pokeByteOff p 0 (htons . fromIntegral . fromEnum $ i16) -- | Deserialize 16-bit from network byte order -decodeInt16 :: Num a => ByteString -> Maybe a -decodeInt16 bs | BS.length bs /= 2 = Nothing -decodeInt16 bs = Just . BSI.inlinePerformIO $ do - let (fp, _, _) = BSI.toForeignPtr bs - withForeignPtr fp $ \p -> do - w16 <- peekByteOff p 0 - return (fromIntegral . ntohs $ w16) +-- Throws an IO exception if this is not a valid integer +decodeInt16 :: Num a => ByteString -> a +decodeInt16 bs + | BS.length bs /= 2 = throw $ userError "decodeInt16: Invalid length" + | otherwise = BSI.inlinePerformIO $ do + let (fp, _, _) = BSI.toForeignPtr bs + withForeignPtr fp $ \p -> do + w16 <- peekByteOff p 0 + return (fromIntegral . ntohs $ w16) -- | Prepend a list of bytestrings with their total length prependLength :: [ByteString] -> [ByteString] diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 6aef5ffd..1902c8f0 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -27,8 +27,9 @@ import qualified Network.Socket as N ( HostName ) import qualified Network.Socket.ByteString as NBS (recv) import Control.Concurrent (ThreadId) -import Control.Monad (liftM, forever) +import Control.Monad (forever) import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) +import Control.Applicative ((<$>)) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length, concat, null) import Data.Int (Int32) @@ -82,11 +83,7 @@ recvWithLength sock = recvInt32 sock >>= recvExact sock -- | Receive a 32-bit integer recvInt32 :: Num a => N.Socket -> IO a -recvInt32 sock = do - mi <- liftM (decodeInt32 . BS.concat) $ recvExact sock 4 - case mi of - Nothing -> throwIO (userError "Invalid integer") - Just i -> return i +recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4 -- | Close a socket, ignoring I/O exceptions tryCloseSocket :: N.Socket -> IO () diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 8cbd5f05..de65dd44 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -73,7 +73,6 @@ import Control.Concurrent.MVar ( MVar import Control.Category ((>>>)) import Control.Applicative ((<$>)) import Control.Monad (when, unless) -import Control.Monad.Error (ErrorT(..), runErrorT) import Control.Exception ( IOException , SomeException , handle @@ -374,7 +373,7 @@ data TransportInternals = TransportInternals -- | Find the socket between a local and a remote endpoint , socketBetween :: EndPointAddress -> EndPointAddress - -> IO (Either String N.Socket) + -> IO N.Socket } -------------------------------------------------------------------------------- @@ -626,39 +625,38 @@ connectToSelf ourEndPoint = try . mapIOException connectFailed $ do -------------------------------------------------------------------------------- -- Find a socket between two endpoints +-- +-- Throws an IO exception if the socket could not be found. internalSocketBetween :: TCPTransport -- ^ Transport -> EndPointAddress -- ^ Local endpoint -> EndPointAddress -- ^ Remote endpoint - -> IO (Either String N.Socket) -internalSocketBetween transport ourAddress theirAddress = runErrorT $ do - ourEndPoint <- ErrorT $ - withMVar (transportState transport) $ \st -> case st of + -> IO N.Socket +internalSocketBetween transport ourAddress theirAddress = do + ourEndPoint <- withMVar (transportState transport) $ \st -> case st of TransportClosed -> - return . Left $ "Transport closed" + throwIO $ userError "Transport closed" TransportValid vst -> case vst ^. localEndPointAt ourAddress of - Nothing -> return . Left $ "Local endpoint not found" - Just ep -> return . Right $ ep - theirEndPoint <- ErrorT $ - withMVar (localState ourEndPoint) $ \st -> case st of + Nothing -> throwIO $ userError "Local endpoint not found" + Just ep -> return ep + theirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of LocalEndPointClosed -> - return . Left $ "Local endpoint closed" + throwIO $ userError "Local endpoint closed" LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of - Nothing -> return . Left $ "Remote endpoint not found" - Just ep -> return . Right $ ep - ErrorT $ - withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit _ -> - return . Left $ "Remote endpoint not yet initialized" - RemoteEndPointValid vst -> - return . Right $ remoteSocket vst - RemoteEndPointClosing _ vst -> - return . Right $ remoteSocket vst - RemoteEndPointClosed -> - return . Left $ "Remote endpoint closed" - RemoteEndPointInvalid _ -> - return . Left $ "Remote endpoint invalid" + Nothing -> throwIO $ userError "Remote endpoint not found" + Just ep -> return ep + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit _ -> + throwIO $ userError "Remote endpoint not yet initialized" + RemoteEndPointValid vst -> + return $ remoteSocket vst + RemoteEndPointClosing _ vst -> + return $ remoteSocket vst + RemoteEndPointClosed -> + throwIO $ userError "Remote endpoint closed" + RemoteEndPointInvalid _ -> + throwIO $ userError "Remote endpoint invalid" -------------------------------------------------------------------------------- -- Lower level functionality -- @@ -708,35 +706,25 @@ createLocalEndPoint transport = do requestConnectionTo :: LocalEndPoint -> EndPointAddress -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo ourEndPoint theirAddress = go +requestConnectionTo ourEndPoint theirAddress = do + (theirEndPoint, isNew) <- mapIOException connectFailed $ + findRemoteEndPoint ourEndPoint theirAddress BlockOnInit + (RemoteEndPointValid . (remoteOutgoing ^: (+ 1))) + + -- If this is a new endpoint, start the initialization process and wait + forM_ isNew $ \resolved -> do + void . forkEndPointThread ourEndPoint $ do + let outgoing = 1 + setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing + readMVar resolved + + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + return (theirEndPoint, decodeInt32 . BS.concat $ reply) where - go = do - (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress True - (RemoteEndPointValid . (remoteOutgoing ^: (+ 1))) - - -- If this is a new endpoint, start the initialization process and wait - forM_ isNew $ \resolved -> do - void . forkEndPointThread ourEndPoint $ do - let outgoing = 1 - setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing - readMVar resolved - - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - case decodeInt32 . BS.concat $ reply of - Nothing -> - throwIO (connectFailed $ userError "Invalid integer") - Just cid -> - return (theirEndPoint, cid) - connectFailed = TransportError ConnectFailed . show -- | Set up a remote endpoint --- --- RELY: The state of the remote endpoint must be uninitialized. --- GUARANTEE: Will only change the state to RemoteEndPointValid or --- RemoteEndPointInvalid. setupRemoteEndPoint :: EndPointPair -- ^ Local endpoint, remote endpoint -> Int -- ^ Initial (outgoing) refcount -> IO () @@ -751,9 +739,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } - modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do - putMVar resolved () - return (RemoteEndPointValid vst) + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return True Right (sock, ConnectionRequestInvalid) -> do -- We remove the endpoint from our local state again because the next @@ -763,9 +749,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do -- connection requests. removeRemoteEndPoint (ourEndPoint, theirEndPoint) let err = invalidAddress "Invalid endpoint" - modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do - putMVar resolved () - return (RemoteEndPointInvalid err) + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) tryCloseSocket sock return False Right (sock, ConnectionRequestCrossed) -> do @@ -775,9 +759,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do return False Left err -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- See comment above - modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do - putMVar resolved () - return (RemoteEndPointInvalid err) + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) return False -- If we get to this point without an exception, then @@ -800,14 +782,12 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do onError result = do removeRemoteEndPoint (ourEndPoint, theirEndPoint) case result of - Left err -> do - tryPutMVar theirState (RemoteEndPointInvalid err) - return () + Left err -> + void $ tryPutMVar theirState (RemoteEndPointInvalid err) Right (sock, _) -> do let err = failed "setupRemoteEndPoint failed" tryPutMVar theirState (RemoteEndPointInvalid err) tryCloseSocket sock - return () failed = TransportError ConnectFailed ourAddress = localAddress ourEndPoint @@ -815,6 +795,21 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do theirState = remoteState theirEndPoint invalidAddress = TransportError ConnectNotFound +resolveInit :: EndPointPair -> RemoteState -> IO () +resolveInit (ourEndPoint, theirEndPoint) newState = + modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "resolveInit (invalid)" + RemoteEndPointInit resolved -> do + putMVar resolved () + return newState + RemoteEndPointClosed -> + throwIO $ userError "Remote endpoint closed" + RemoteEndPointClosing _ _ -> + relyViolation (ourEndPoint, theirEndPoint) "resolveInit (closing)" + RemoteEndPointValid _ -> + relyViolation (ourEndPoint, theirEndPoint) "resolveInit (valid)" + -- | Establish a connection to a remote endpoint socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address @@ -904,9 +899,6 @@ decodeEndPointAddress (EndPointAddress bs) = -- | Do a (blocking) remote request -- --- RELY: Remote endpoint must be in valid or closed state. --- GUARANTEE: Will not change the state of the remote endpoint. --- -- May throw IO (user) exception if the local or the remote endpoint is closed, -- or if the send fails. doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] @@ -1147,7 +1139,7 @@ handleConnectionRequest transport sock = handle handleException $ do go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do (theirEndPoint, isNew) <- - findRemoteEndPoint ourEndPoint theirAddress False + findRemoteEndPoint ourEndPoint theirAddress DontBlockOnInit (throw $ userError "Already connected") let ourAddress = localAddress ourEndPoint @@ -1166,9 +1158,7 @@ handleConnectionRequest transport sock = handle handleException $ do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } - modifyMVar_ theirState $ \(RemoteEndPointInit resolved) -> do - putMVar resolved () - return (RemoteEndPointValid vst) + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) sendMany sock [encodeInt32 ConnectionRequestAccepted] return (Just theirEndPoint) -- If we left the scope of the exception handler with a return value of @@ -1186,15 +1176,17 @@ handleConnectionRequest transport sock = handle handleException $ do tryIO $ sendMany sock [encodeInt32 ConnectionRequestInvalid] tryCloseSocket sock +data BlockOnInit = BlockOnInit | DontBlockOnInit + -- | Find a remote endpoint. If the remote endpoint does not yet exist we -- create it in Init state and return the corresponding 'resolved' MVar. findRemoteEndPoint - :: LocalEndPoint -- ^ Local endpoint - -> EndPointAddress -- ^ Remote address - -> Bool -- ^ Wait if remote endpoint is in Init state + :: LocalEndPoint + -> EndPointAddress + -> BlockOnInit -> (ValidRemoteEndPointState -> RemoteState) -- ^ Apply if in Valid state -> IO (RemoteEndPoint, Maybe (MVar ())) -findRemoteEndPoint ourEndPoint theirAddress waitOnInit whenValid = go +findRemoteEndPoint ourEndPoint theirAddress blockOnInit whenValid = go where go = do (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of @@ -1233,9 +1225,9 @@ findRemoteEndPoint ourEndPoint theirAddress waitOnInit whenValid = go RemoteEndPointInvalid err -> throwIO err RemoteEndPointInit resolved -> - if waitOnInit - then readMVar resolved >> go - else return (theirEndPoint, Nothing) + case blockOnInit of + BlockOnInit -> readMVar resolved >> go + DontBlockOnInit -> return (theirEndPoint, Nothing) RemoteEndPointValid _ -> return (theirEndPoint, Nothing) RemoteEndPointClosing resolved _ -> @@ -1248,9 +1240,6 @@ findRemoteEndPoint ourEndPoint theirAddress waitOnInit whenValid = go -- | Handle requests from a remote endpoint. -- -- Returns only if the remote party closes the socket or if an error occurs. --- --- RELY: The remote endpoint must not be invalid. --- GUARANTEE: May change the remote endpoint to RemoteEndPointClosed state. handleIncomingMessages :: EndPointPair -> IO () handleIncomingMessages (ourEndPoint, theirEndPoint) = do mSock <- withMVar theirState $ \st -> diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index d538b446..d5b66008 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -654,7 +654,7 @@ testUnidirectionalError nextPort = do takeMVar serverGotPing -- Close the *outgoing* part of the socket only - Right sock <- socketBetween internals (address endpoint) theirAddr + sock <- socketBetween internals (address endpoint) theirAddr N.shutdown sock N.ShutdownSend -- At this point we cannot notice the problem yet so we shouldn't receive an event yet @@ -670,7 +670,7 @@ testUnidirectionalError nextPort = do takeMVar serverGotPing -- Again, close the outgoing part of the socket - Right sock' <- socketBetween internals (address endpoint) theirAddr + sock' <- socketBetween internals (address endpoint) theirAddr N.shutdown sock' N.ShutdownSend -- We now find the error when we attempt to close the connection @@ -682,7 +682,7 @@ testUnidirectionalError nextPort = do takeMVar serverGotPing -- We repeat once more. - Right sock'' <- socketBetween internals (address endpoint) theirAddr + sock'' <- socketBetween internals (address endpoint) theirAddr N.shutdown sock'' N.ShutdownSend -- Now we notice the problem when we try to connect @@ -727,7 +727,7 @@ testInvalidCloseConnection nextPort = do Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- Get a handle on the TCP connection and manually send an invalid CloseConnection request - Right sock <- socketBetween internals ourAddr theirAddr + sock <- socketBetween internals ourAddr theirAddr sendMany sock [encodeInt32 CloseConnection, encodeInt32 (12345 :: Int)] putMVar clientDone () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index b51e81f0..2fbe9ffb 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -49,7 +49,7 @@ echoServer endpoint = do -- Ignore go cs ErrorEvent _ -> - fail (show event) + putStrLn $ "Echo server received error event: " ++ show event EndPointClosed -> return () From eb10caabb03bf7d6ada3fffe6971757d1f267d58 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 28 May 2012 16:28:55 +0100 Subject: [PATCH 0087/2357] Deal with invalid connection requests --- src/Network/Transport/TCP.hs | 85 +++++++++++++++++++----------------- tests/TestTCP.hs | 30 ++++++++++--- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index de65dd44..784a3819 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -326,9 +326,11 @@ data RemoteEndPoint = RemoteEndPoint , remoteId :: Int } +data Origin = Local | Remote + data RemoteState = RemoteEndPointInvalid (TransportError ConnectErrorCode) - | RemoteEndPointInit (MVar ()) + | RemoteEndPointInit (MVar ()) Origin | RemoteEndPointValid ValidRemoteEndPointState | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState | RemoteEndPointClosed @@ -513,7 +515,7 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = \_ -> relyViolation (ourEndPoint, theirEndPoint) "apiSend" - , caseInit = \_ -> + , caseInit = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "apiSend" , caseValid = \vst -> do alive <- readIORef connAlive @@ -564,7 +566,7 @@ apiCloseEndPoint transport evs ourEndPoint = do case st of RemoteEndPointInvalid _ -> return st - RemoteEndPointInit resolved -> do + RemoteEndPointInit resolved _ -> do putMVar resolved () return RemoteEndPointClosed RemoteEndPointValid conn -> do @@ -647,7 +649,7 @@ internalSocketBetween transport ourAddress theirAddress = do Nothing -> throwIO $ userError "Remote endpoint not found" Just ep -> return ep withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit _ -> + RemoteEndPointInit _ _ -> throwIO $ userError "Remote endpoint not yet initialized" RemoteEndPointValid vst -> return $ remoteSocket vst @@ -708,8 +710,7 @@ requestConnectionTo :: LocalEndPoint -> IO (RemoteEndPoint, ConnectionId) requestConnectionTo ourEndPoint theirAddress = do (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress BlockOnInit - (RemoteEndPointValid . (remoteOutgoing ^: (+ 1))) + findRemoteEndPoint ourEndPoint theirAddress Local -- If this is a new endpoint, start the initialization process and wait forM_ isNew $ \resolved -> do @@ -800,7 +801,7 @@ resolveInit (ourEndPoint, theirEndPoint) newState = modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "resolveInit (invalid)" - RemoteEndPointInit resolved -> do + RemoteEndPointInit resolved _ -> do putMVar resolved () return newState RemoteEndPointClosed -> @@ -917,7 +918,7 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do throwIO (userError "Local endpoint closed") withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = throwIO - , caseInit = \_ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" + , caseInit = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" , caseValid = \vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId] , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" , caseClosed = throwIO $ userError "Remote endpoint closed" @@ -988,7 +989,7 @@ forkEndPointThread ourEndPoint p = data RemoteStatePatternMatch a = RemoteStatePatternMatch { caseInvalid :: TransportError ConnectErrorCode -> IO a - , caseInit :: MVar () -> IO a + , caseInit :: MVar () -> Origin -> IO a , caseValid :: ValidRemoteEndPointState -> IO a , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a , caseClosed :: IO a @@ -998,7 +999,7 @@ remoteStateIdentity :: RemoteStatePatternMatch RemoteState remoteStateIdentity = RemoteStatePatternMatch { caseInvalid = return . RemoteEndPointInvalid - , caseInit = return . RemoteEndPointInit + , caseInit = (return .) . RemoteEndPointInit , caseValid = return . RemoteEndPointValid , caseClosing = (return .) . RemoteEndPointClosing , caseClosed = return RemoteEndPointClosed @@ -1026,8 +1027,8 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = throwIO ex -- The other cases are less interesting, because unless the endpoint is -- in Valid state we're not supposed to do any IO on it - RemoteEndPointInit resolved -> do - (st', a) <- onException (restore $ caseInit match resolved) + RemoteEndPointInit resolved origin -> do + (st', a) <- onException (restore $ caseInit match resolved origin) (putMVar theirState st) putMVar theirState st' return a @@ -1068,7 +1069,7 @@ modifyRemoteState_ (ourEndPoint, theirEndPoint) match = modifyRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = u . caseInvalid match - , caseInit = u . caseInit match + , caseInit = \resolved origin -> u $ caseInit match resolved origin , caseValid = u . caseValid match , caseClosing = \resolved vst -> u $ caseClosing match resolved vst , caseClosed = u $ caseClosed match @@ -1087,9 +1088,9 @@ withRemoteState (ourEndPoint, theirEndPoint) match = { caseInvalid = \err -> do a <- caseInvalid match err return (RemoteEndPointInvalid err, a) - , caseInit = \resolved -> do - a <- caseInit match resolved - return (RemoteEndPointInit resolved, a) + , caseInit = \resolved origin -> do + a <- caseInit match resolved origin + return (RemoteEndPointInit resolved origin, a) , caseValid = \vst -> do a <- caseValid match vst return (RemoteEndPointValid vst, a) @@ -1139,11 +1140,9 @@ handleConnectionRequest transport sock = handle handleException $ do go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do (theirEndPoint, isNew) <- - findRemoteEndPoint ourEndPoint theirAddress DontBlockOnInit - (throw $ userError "Already connected") + findRemoteEndPoint ourEndPoint theirAddress Remote let ourAddress = localAddress ourEndPoint - theirState = remoteState theirEndPoint crossed = isNothing isNew && ourAddress < theirAddress if crossed @@ -1158,8 +1157,8 @@ handleConnectionRequest transport sock = handle handleException $ do , _remoteIncoming = IntSet.empty , sendOn = sendMany sock } - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) sendMany sock [encodeInt32 ConnectionRequestAccepted] + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return (Just theirEndPoint) -- If we left the scope of the exception handler with a return value of -- Nothing then the socket is already closed; otherwise, the socket has @@ -1176,17 +1175,14 @@ handleConnectionRequest transport sock = handle handleException $ do tryIO $ sendMany sock [encodeInt32 ConnectionRequestInvalid] tryCloseSocket sock -data BlockOnInit = BlockOnInit | DontBlockOnInit - -- | Find a remote endpoint. If the remote endpoint does not yet exist we -- create it in Init state and return the corresponding 'resolved' MVar. findRemoteEndPoint :: LocalEndPoint -> EndPointAddress - -> BlockOnInit - -> (ValidRemoteEndPointState -> RemoteState) -- ^ Apply if in Valid state + -> Origin -> IO (RemoteEndPoint, Maybe (MVar ())) -findRemoteEndPoint ourEndPoint theirAddress blockOnInit whenValid = go +findRemoteEndPoint ourEndPoint theirAddress findOrigin = go where go = do (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of @@ -1195,7 +1191,7 @@ findRemoteEndPoint ourEndPoint theirAddress blockOnInit whenValid = go return (st, (theirEndPoint, Nothing)) Nothing -> do resolved <- newEmptyMVar - theirState <- newMVar (RemoteEndPointInit resolved) + theirState <- newMVar (RemoteEndPointInit resolved findOrigin) let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress , remoteState = theirState @@ -1216,18 +1212,29 @@ findRemoteEndPoint ourEndPoint theirAddress blockOnInit whenValid = go Nothing -> do let theirState = remoteState theirEndPoint snapshot <- modifyMVar theirState $ \st -> case st of - RemoteEndPointValid vst -> do - let st' = whenValid vst - return (st', st') - _ -> + RemoteEndPointValid vst -> + case findOrigin of + Local -> do + let st' = RemoteEndPointValid + . (remoteOutgoing ^: (+ 1)) + $ vst + return (st', st') + Remote -> + throwIO $ userError "Already connected" + _ -> return (st, st) + -- The snapshot may no longer be up to date at this point, but if we + -- increased the refcount then it can only either be valid or closed + -- (after an explicit call to 'closeEndPoint' or 'closeTransport') case snapshot of RemoteEndPointInvalid err -> throwIO err - RemoteEndPointInit resolved -> - case blockOnInit of - BlockOnInit -> readMVar resolved >> go - DontBlockOnInit -> return (theirEndPoint, Nothing) + RemoteEndPointInit resolved initOrigin -> + case (findOrigin, initOrigin) of + (Local, Local) -> readMVar resolved >> go + (Local, Remote) -> readMVar resolved >> go + (Remote, Local) -> return (theirEndPoint, Nothing) + (Remote, Remote) -> throwIO $ userError "Already connected" RemoteEndPointValid _ -> return (theirEndPoint, Nothing) RemoteEndPointClosing resolved _ -> @@ -1246,7 +1253,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages" - RemoteEndPointInit _ -> + RemoteEndPointInit _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages" RemoteEndPointValid ep -> return . Just $ remoteSocket ep @@ -1307,7 +1314,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do vst <- case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" - RemoteEndPointInit _ -> + RemoteEndPointInit _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" RemoteEndPointValid vst -> return (remoteIncoming ^: IntSet.insert newId $ vst) @@ -1359,7 +1366,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection" - RemoteEndPointInit _ -> + RemoteEndPointInit _ _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection" RemoteEndPointValid vst -> do unless (IntSet.member cid (vst ^. remoteIncoming)) $ @@ -1385,7 +1392,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" - RemoteEndPointInit _ -> + RemoteEndPointInit _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" RemoteEndPointValid vst -> do -- We regard a CloseSocket message as an (optimized) way for the @@ -1434,7 +1441,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" - RemoteEndPointInit _ -> + RemoteEndPointInit _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do let code = EventConnectionLost diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index d5b66008..768c8a99 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -476,8 +476,8 @@ testBlockAfterCloseSocket nextPort = do -- | Test what happens when a remote endpoint sends a connection request to our -- transport for an endpoint it already has a connection to -testUnnecessaryConnect :: IO N.ServiceName -> IO () -testUnnecessaryConnect nextPort = do +testUnnecessaryConnect :: IO N.ServiceName -> Int -> IO () +testUnnecessaryConnect nextPort numThreads = do clientDone <- newEmptyMVar serverAddr <- newEmptyMVar @@ -489,8 +489,24 @@ testUnnecessaryConnect nextPort = do forkTry $ do -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check let ourAddress = EndPointAddress "126.0.0.1" - Right (_, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress - Right (_, ConnectionRequestInvalid) <- readMVar serverAddr >>= socketToEndPoint ourAddress + + -- We should only get a single 'Accepted' reply + gotAccepted <- newEmptyMVar + dones <- replicateM numThreads $ do + done <- newEmptyMVar + forkTry $ do + Right (_, reply) <- readMVar serverAddr >>= socketToEndPoint ourAddress + case reply of + ConnectionRequestAccepted -> + putMVar gotAccepted () + ConnectionRequestInvalid -> + return () + ConnectionRequestCrossed -> + throwIO $ userError "Unexpected response (Crossed)" + putMVar done () + return done + + mapM_ readMVar (gotAccepted : dones) putMVar clientDone () takeMVar clientDone @@ -501,7 +517,7 @@ testMany nextPort = do Right masterTransport <- nextPort >>= createTransport "127.0.0.1" Right masterEndPoint <- newEndPoint masterTransport - replicateM_ 20 $ do + replicateM_ 10 $ do mTransport <- nextPort >>= createTransport "127.0.0.1" case mTransport of Left ex -> do @@ -511,7 +527,7 @@ testMany nextPort = do _ -> return () throwIO ex Right transport -> - replicateM_ 3 $ do + replicateM_ 2 $ do Right endpoint <- newEndPoint transport Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered defaultConnectHints return () @@ -744,7 +760,7 @@ main = do , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) , ("BlockAfterCloseSocket", testBlockAfterCloseSocket nextPort) - , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort) + , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort 10) , ("InvalidAddress", testInvalidAddress nextPort) , ("InvalidConnect", testInvalidConnect nextPort) , ("Many", testMany nextPort) From e713e69118456cf0bc1f862c96ffa0ade8ad0900 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 30 May 2012 08:40:34 +0100 Subject: [PATCH 0088/2357] More error handling (not yet done) One of the tests is currently breaking (the wrong error is reported), and the connection protocol needs some modifications to deal with extreme delays. --- src/Network/Transport/Internal/TCP.hs | 7 +- src/Network/Transport/TCP.hs | 364 +++++++++++++++----------- tests/TestTCP.hs | 4 +- tests/TestTransport.hs | 44 +++- 4 files changed, 259 insertions(+), 160 deletions(-) diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 1902c8f0..e368f837 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -87,7 +87,8 @@ recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4 -- | Close a socket, ignoring I/O exceptions tryCloseSocket :: N.Socket -> IO () -tryCloseSocket = void . tryIO . N.sClose +tryCloseSocket sock = void . tryIO $ + N.sClose sock -- | Read an exact number of bytes from a socket -- @@ -96,7 +97,7 @@ tryCloseSocket = void . tryIO . N.sClose recvExact :: N.Socket -- ^ Socket to read from -> Int32 -- ^ Number of bytes to read -> IO [ByteString] -recvExact _ len | len <= 0 = throwIO (userError "Negative length") +recvExact _ len | len <= 0 = throwIO (userError "recvExact: Negative length") recvExact sock len = go [] len where go :: [ByteString] -> Int32 -> IO [ByteString] @@ -104,5 +105,5 @@ recvExact sock len = go [] len go acc l = do bs <- NBS.recv sock (fromIntegral l `min` 4096) if BS.null bs - then throwIO (userError "Socket closed") + then throwIO (userError "recvExact: Socket closed") else go (bs : acc) (l - fromIntegral (BS.length bs)) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 784a3819..fe753053 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -84,6 +84,7 @@ import Control.Exception ( IOException , mask_ , onException , fromException + , catch ) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) @@ -106,7 +107,6 @@ import qualified Data.Map as Map (empty) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) import Data.Foldable (forM_, mapM_) -import Data.Maybe (isNothing) -- $design -- @@ -241,12 +241,10 @@ data LocalEndPointState = | LocalEndPointClosed data ValidLocalEndPointState = ValidLocalEndPointState - { _nextConnectionId :: !ConnectionId - , _pendingCtrlRequests :: IntMap (MVar [ByteString]) - , _nextCtrlRequestId :: !ControlRequestId - , _localConnections :: Map EndPointAddress RemoteEndPoint - , _internalThreads :: [ThreadId] - , _nextRemoteId :: !Int + { _nextConnectionId :: !ConnectionId + , _localConnections :: Map EndPointAddress RemoteEndPoint + , _internalThreads :: [ThreadId] + , _nextRemoteId :: !Int } -- REMOTE ENDPOINTS @@ -327,19 +325,22 @@ data RemoteEndPoint = RemoteEndPoint } data Origin = Local | Remote + deriving (Eq, Show) data RemoteState = RemoteEndPointInvalid (TransportError ConnectErrorCode) | RemoteEndPointInit (MVar ()) Origin | RemoteEndPointValid ValidRemoteEndPointState | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState - | RemoteEndPointClosed + | RemoteEndPointClosed (Maybe IOException) data ValidRemoteEndPointState = ValidRemoteEndPointState - { _remoteOutgoing :: !Int - , _remoteIncoming :: IntSet - , remoteSocket :: N.Socket - , sendOn :: [ByteString] -> IO () + { _remoteOutgoing :: !Int + , _remoteIncoming :: IntSet + , remoteSocket :: N.Socket + , sendOn :: [ByteString] -> IO () + , _pendingCtrlRequests :: IntMap (MVar (Either IOException [ByteString])) + , _nextCtrlRequestId :: !ControlRequestId } type EndPointId = Int32 @@ -476,6 +477,7 @@ apiConnect ourEndPoint theirAddress _reliability _hints = if localAddress ourEndPoint == theirAddress then connectToSelf ourEndPoint else try $ do + resetIfInvalid ourEndPoint theirAddress (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. @@ -525,12 +527,12 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = , caseClosing = \_ _ -> do alive <- readIORef connAlive if alive - then throwIO $ TransportError SendFailed "Connection lost" + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" else throwIO $ TransportError SendClosed "Connection closed" - , caseClosed = do + , caseClosed = \err -> do alive <- readIORef connAlive if alive - then throwIO $ TransportError SendFailed "Connection lost" + then throwIO $ TransportError SendFailed (show err) else throwIO $ TransportError SendClosed "Connection closed" } where @@ -568,17 +570,17 @@ apiCloseEndPoint transport evs ourEndPoint = do return st RemoteEndPointInit resolved _ -> do putMVar resolved () - return RemoteEndPointClosed + return $ RemoteEndPointClosed (Just . userError $ "apiCloseEndPoint") RemoteEndPointValid conn -> do tryIO $ sendOn conn [encodeInt32 CloseSocket] tryCloseSocket (remoteSocket conn) - return RemoteEndPointClosed + return $ RemoteEndPointClosed (Just . userError $ "apiCloseEndPoint") RemoteEndPointClosing resolved conn -> do putMVar resolved () tryCloseSocket (remoteSocket conn) - return RemoteEndPointClosed - RemoteEndPointClosed -> - return RemoteEndPointClosed + return $ RemoteEndPointClosed (Just . userError $ "apiCloseEndPoint") + RemoteEndPointClosed err -> + return $ RemoteEndPointClosed err -- | Special case of 'apiConnect': connect an endpoint to itself connectToSelf :: LocalEndPoint @@ -655,7 +657,7 @@ internalSocketBetween transport ourAddress theirAddress = do return $ remoteSocket vst RemoteEndPointClosing _ vst -> return $ remoteSocket vst - RemoteEndPointClosed -> + RemoteEndPointClosed _ -> throwIO $ userError "Remote endpoint closed" RemoteEndPointInvalid _ -> throwIO $ userError "Remote endpoint invalid" @@ -664,6 +666,28 @@ internalSocketBetween transport ourAddress theirAddress = do -- Lower level functionality -- -------------------------------------------------------------------------------- +-- | Reset a remote endpoint if it is in Invalid mode +-- +-- If a user calls the API function 'connect' and the remote endpoint is +-- currently in Invalid state, we remove the remote endpoint first because a +-- new attempt to connect might succeed even if the previous one failed. +-- +-- Throws a TransportError ConnectFailed exception if the local endpoint is +-- closed. +resetIfInvalid :: LocalEndPoint -> EndPointAddress -> IO () +resetIfInvalid ourEndPoint theirAddress = do + mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> + return (vst ^. localConnectionTo theirAddress) + LocalEndPointClosed -> + throwIO $ TransportError ConnectFailed "Endpoint closed" + forM_ mTheirEndPoint $ \theirEndPoint -> + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInvalid _ -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + _ -> + return () + -- | Create a new local endpoint -- -- May throw a TransportError NewEndPointErrorCode exception if the transport @@ -673,8 +697,6 @@ createLocalEndPoint transport = do chan <- newChan state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState { _nextConnectionId = firstNonReservedConnectionId - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 , _localConnections = Map.empty , _internalThreads = [] , _nextRemoteId = 0 @@ -708,47 +730,43 @@ createLocalEndPoint transport = do requestConnectionTo :: LocalEndPoint -> EndPointAddress -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo ourEndPoint theirAddress = do - (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress Local - - -- If this is a new endpoint, start the initialization process and wait - forM_ isNew $ \resolved -> do - void . forkEndPointThread ourEndPoint $ do - let outgoing = 1 - setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing - readMVar resolved - - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - return (theirEndPoint, decodeInt32 . BS.concat $ reply) +requestConnectionTo ourEndPoint theirAddress = go where + go = do + (theirEndPoint, isNew, _) <- mapIOException connectFailed $ + findRemoteEndPoint ourEndPoint theirAddress Local + + if isNew + then do + void . forkEndPointThread ourEndPoint $ + catch (setupRemoteEndPoint (ourEndPoint, theirEndPoint)) + (\err -> let _ = err :: SomeException in return ()) + go + else do + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + return (theirEndPoint, decodeInt32 . BS.concat $ reply) + connectFailed = TransportError ConnectFailed . show -- | Set up a remote endpoint -setupRemoteEndPoint :: EndPointPair -- ^ Local endpoint, remote endpoint - -> Int -- ^ Initial (outgoing) refcount - -> IO () -setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do +setupRemoteEndPoint :: EndPointPair -> IO () +setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress) onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = outgoing - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 } resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return True Right (sock, ConnectionRequestInvalid) -> do - -- We remove the endpoint from our local state again because the next - -- call to 'connect' might give a different result. Threads that were - -- waiting on the result of this call to connect will get the - -- RemoteEndPointInvalid; subsequent threads will initiate a new - -- connection requests. - removeRemoteEndPoint (ourEndPoint, theirEndPoint) let err = invalidAddress "Invalid endpoint" resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) tryCloseSocket sock @@ -759,7 +777,6 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do tryCloseSocket sock return False Left err -> do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- See comment above resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) return False @@ -780,8 +797,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do onError :: Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse) -> IO () - onError result = do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) + onError result = case result of Left err -> void $ tryPutMVar theirState (RemoteEndPointInvalid err) @@ -796,21 +812,23 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) outgoing = do theirState = remoteState theirEndPoint invalidAddress = TransportError ConnectNotFound +-- TODO: Document +-- TODO: is this what we want? modifyRemoteState only does something special +-- when an exception is thrown in the Valid case. resolveInit :: EndPointPair -> RemoteState -> IO () -resolveInit (ourEndPoint, theirEndPoint) newState = - modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "resolveInit (invalid)" - RemoteEndPointInit resolved _ -> do - putMVar resolved () - return newState - RemoteEndPointClosed -> - throwIO $ userError "Remote endpoint closed" - RemoteEndPointClosing _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "resolveInit (closing)" - RemoteEndPointValid _ -> - relyViolation (ourEndPoint, theirEndPoint) "resolveInit (valid)" - +resolveInit (ourEndPoint, theirEndPoint) newState = + modifyRemoteState_ (ourEndPoint, theirEndPoint) $ remoteStateIdentity + { caseInit = \resolved _ -> do + putMVar resolved () + return newState + , caseInvalid = \_ -> throwException + , caseValid = \_ -> throwException + , caseClosing = \_ _ -> throwException + , caseClosed = \_ -> throwException + } + where + throwException = throwIO $ userError "resolveInit: not in Init state" + -- | Establish a connection to a remote endpoint socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address @@ -904,26 +922,26 @@ decodeEndPointAddress (EndPointAddress bs) = -- or if the send fails. doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] doRemoteRequest (ourEndPoint, theirEndPoint) header = do - reply <- newEmptyMVar - reqId <- modifyMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> do - let reqId = vst ^. nextCtrlRequestId - return ( LocalEndPointValid - . (nextCtrlRequestId ^: (+ 1)) - . (pendingCtrlRequestsAt reqId ^= Just reply) - $ vst - , reqId - ) - LocalEndPointClosed -> - throwIO (userError "Local endpoint closed") - withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseInvalid = throwIO + replyMVar <- newEmptyMVar + modifyRemoteState_ (ourEndPoint, theirEndPoint) RemoteStatePatternMatch + { caseValid = \vst -> do + let reqId = vst ^. nextCtrlRequestId + sendOn vst [encodeInt32 header, encodeInt32 reqId] + return ( RemoteEndPointValid + . (nextCtrlRequestId ^: (+ 1)) + . (pendingCtrlRequestsAt reqId ^= Just replyMVar) + $ vst + ) + -- Error cases + , caseInvalid = throwIO , caseInit = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" - , caseValid = \vst -> sendOn vst [encodeInt32 header, encodeInt32 reqId] , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - , caseClosed = throwIO $ userError "Remote endpoint closed" + , caseClosed = \_ -> throwIO $ userError "doRemoteRequest: Remote endpoint closed" } - takeMVar reply + mReply <- takeMVar replyMVar + case mReply of + Left err -> throwIO err + Right reply -> return reply -- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO () @@ -992,7 +1010,7 @@ data RemoteStatePatternMatch a = RemoteStatePatternMatch , caseInit :: MVar () -> Origin -> IO a , caseValid :: ValidRemoteEndPointState -> IO a , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a - , caseClosed :: IO a + , caseClosed :: Maybe IOException -> IO a } remoteStateIdentity :: RemoteStatePatternMatch RemoteState @@ -1002,7 +1020,7 @@ remoteStateIdentity = , caseInit = (return .) . RemoteEndPointInit , caseValid = return . RemoteEndPointValid , caseClosing = (return .) . RemoteEndPointClosing - , caseClosed = return RemoteEndPointClosed + , caseClosed = return . RemoteEndPointClosed } -- | Like modifyMVar, but if an I/O exception occurs don't restore the remote @@ -1042,8 +1060,8 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = (putMVar theirState st) putMVar theirState st' return a - RemoteEndPointClosed -> do - (st', a) <- onException (restore $ caseClosed match) + RemoteEndPointClosed err -> do + (st', a) <- onException (restore $ caseClosed match err) (putMVar theirState st) putMVar theirState st' return a @@ -1055,7 +1073,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = handleIOException ex vst = do tryCloseSocket (remoteSocket vst) removeRemoteEndPoint (ourEndPoint, theirEndPoint) - putMVar theirState RemoteEndPointClosed + putMVar theirState (RemoteEndPointClosed $ Just ex) let incoming = IntSet.elems $ vst ^. remoteIncoming code = EventConnectionLost (remoteAddress theirEndPoint) incoming err = TransportError code (show ex) @@ -1072,7 +1090,7 @@ modifyRemoteState_ (ourEndPoint, theirEndPoint) match = , caseInit = \resolved origin -> u $ caseInit match resolved origin , caseValid = u . caseValid match , caseClosing = \resolved vst -> u $ caseClosing match resolved vst - , caseClosed = u $ caseClosed match + , caseClosed = u . caseClosed match } where u :: IO a -> IO (a, ()) @@ -1097,9 +1115,9 @@ withRemoteState (ourEndPoint, theirEndPoint) match = , caseClosing = \resolved vst -> do a <- caseClosing match resolved vst return (RemoteEndPointClosing resolved vst, a) - , caseClosed = do - a <- caseClosed match - return (RemoteEndPointClosed, a) + , caseClosed = \err -> do + a <- caseClosed match err + return (RemoteEndPointClosed err, a) } -------------------------------------------------------------------------------- @@ -1139,12 +1157,9 @@ handleConnectionRequest transport sock = handle handleException $ do go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do - (theirEndPoint, isNew) <- + (theirEndPoint, _, crossed) <- findRemoteEndPoint ourEndPoint theirAddress Remote - - let ourAddress = localAddress ourEndPoint - crossed = isNothing isNew && ourAddress < theirAddress - + if crossed then do tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] @@ -1152,10 +1167,12 @@ handleConnectionRequest transport sock = handle handleException $ do return Nothing else do let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 } sendMany sock [encodeInt32 ConnectionRequestAccepted] resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) @@ -1176,19 +1193,20 @@ handleConnectionRequest transport sock = handle handleException $ do tryCloseSocket sock -- | Find a remote endpoint. If the remote endpoint does not yet exist we --- create it in Init state and return the corresponding 'resolved' MVar. +-- create it in Init state. Returns if the endpoint was new and, for requests +-- of Remote origin, whether the request crossed. findRemoteEndPoint :: LocalEndPoint -> EndPointAddress -> Origin - -> IO (RemoteEndPoint, Maybe (MVar ())) + -> IO (RemoteEndPoint, Bool, Bool) findRemoteEndPoint ourEndPoint theirAddress findOrigin = go where go = do (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of Just theirEndPoint -> - return (st, (theirEndPoint, Nothing)) + return (st, (theirEndPoint, False)) Nothing -> do resolved <- newEmptyMVar theirState <- newMVar (RemoteEndPointInit resolved findOrigin) @@ -1201,15 +1219,15 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go . (localConnectionTo theirAddress ^= Just theirEndPoint) . (nextRemoteId ^: (+ 1)) $ vst - , (theirEndPoint, Just resolved) + , (theirEndPoint, True) ) LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" - - case isNew of - Just resolved -> - return (theirEndPoint, Just resolved) - Nothing -> do + + if isNew + then + return (theirEndPoint, True, False) + else do let theirState = remoteState theirEndPoint snapshot <- modifyMVar theirState $ \st -> case st of RemoteEndPointValid vst -> @@ -1220,7 +1238,7 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go $ vst return (st', st') Remote -> - throwIO $ userError "Already connected" + return (st, st) _ -> return (st, st) -- The snapshot may no longer be up to date at this point, but if we @@ -1233,16 +1251,27 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go case (findOrigin, initOrigin) of (Local, Local) -> readMVar resolved >> go (Local, Remote) -> readMVar resolved >> go - (Remote, Local) -> return (theirEndPoint, Nothing) - (Remote, Remote) -> throwIO $ userError "Already connected" + (Remote, Local) -> return (theirEndPoint, False, ourAddress < theirAddress) + (Remote, Remote) -> throwIO $ userError "Already connected (B)" RemoteEndPointValid _ -> - return (theirEndPoint, Nothing) + -- We assume that the request crossed if we find the endpoint in + -- Valid state. It is possible that this is really an invalid + -- request, but only in the case of a broken client (we don't + -- maintain enough history to be able to tell the difference). + return (theirEndPoint, False, True) RemoteEndPointClosing resolved _ -> readMVar resolved >> go - RemoteEndPointClosed -> + RemoteEndPointClosed Nothing -> go + RemoteEndPointClosed (Just err) -> + throwIO err - ourState = localState ourEndPoint + ourState = localState ourEndPoint + ourAddress = localAddress ourEndPoint + + + + -- | Handle requests from a remote endpoint. -- @@ -1252,14 +1281,14 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do mSock <- withMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages" + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (invalid)" RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages" + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (init)" RemoteEndPointValid ep -> return . Just $ remoteSocket ep RemoteEndPointClosing _ ep -> return . Just $ remoteSocket ep - RemoteEndPointClosed -> + RemoteEndPointClosed _ -> -- The remote endpoint got closed before we got a chance to start -- dealing with incoming messages return Nothing @@ -1313,9 +1342,11 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> do vst <- case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:createNewConnection (invalid)" RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:createNewConnection (init)" RemoteEndPointValid vst -> return (remoteIncoming ^: IntSet.insert newId $ vst) RemoteEndPointClosing resolved vst -> do @@ -1327,8 +1358,11 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- to RemoteEndPointValid putMVar resolved () return (remoteIncoming ^= IntSet.singleton newId $ vst) - RemoteEndPointClosed -> - throwIO $ userError "Remote endpoint closed" + RemoteEndPointClosed (Just err) -> + throwIO err + RemoteEndPointClosed Nothing -> + relyViolation (ourEndPoint, theirEndPoint) + "createNewConnection (closed)" sendOn vst ( encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 newId] @@ -1343,20 +1377,31 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do readControlResponse :: N.Socket -> ControlRequestId -> IO () readControlResponse sock reqId = do response <- recvWithLength sock - mmvar <- modifyMVar ourState $ \st -> case st of - LocalEndPointValid vst -> - return ( LocalEndPointValid + mmvar <- modifyMVar theirState $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) + "readControlResponse (invalid)" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) + "readControlResponse (init)" + RemoteEndPointValid vst -> + return ( RemoteEndPointValid . (pendingCtrlRequestsAt reqId ^= Nothing) $ vst , vst ^. pendingCtrlRequestsAt reqId ) - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" + RemoteEndPointClosing _ _ -> + throwIO $ userError "Invalid control response" + RemoteEndPointClosed (Just err) -> + throwIO err + RemoteEndPointClosed Nothing -> + relyViolation (ourEndPoint, theirEndPoint) + "readControlResponse (closed)" case mmvar of Nothing -> throwIO $ userError "Invalid request ID" Just mvar -> - putMVar mvar response + putMVar mvar (Right response) -- Close a connection -- It is important that we verify that the connection is in fact open, @@ -1365,9 +1410,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do closeConnection cid = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "closeConnection" + relyViolation (ourEndPoint, theirEndPoint) "closeConnection (invalid)" RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "closeConnection" + relyViolation (ourEndPoint, theirEndPoint) "closeConnection (init)" RemoteEndPointValid vst -> do unless (IntSet.member cid (vst ^. remoteIncoming)) $ throwIO $ userError "Invalid CloseConnection" @@ -1380,8 +1425,10 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- far as we are concerned there are no incoming connections. This -- means that a CloseConnection request at this point is invalid. throwIO $ userError "Invalid CloseConnection request" - RemoteEndPointClosed -> - throwIO $ userError "Remote endpoint closed" + RemoteEndPointClosed (Just err) -> + throwIO err + RemoteEndPointClosed Nothing -> + relyViolation (ourEndPoint, theirEndPoint) "closeConnection (closed)" writeChan ourChannel (ConnectionClosed cid) closeIfUnused (ourEndPoint, theirEndPoint) @@ -1391,9 +1438,11 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:closeSocket (invalid)" RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:closeSocket (init)" RemoteEndPointValid vst -> do -- We regard a CloseSocket message as an (optimized) way for the -- remote endpoint to indicate that all its connections to us are @@ -1408,16 +1457,19 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Attempt to reply (but don't insist) tryIO $ sendOn vst' [encodeInt32 CloseSocket] tryCloseSocket sock - return (RemoteEndPointClosed, True) + return (RemoteEndPointClosed Nothing, True) else return (RemoteEndPointValid vst', False) RemoteEndPointClosing resolved _ -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) tryCloseSocket sock putMVar resolved () - return (RemoteEndPointClosed, True) - RemoteEndPointClosed -> - throwIO $ userError "Remote endpoint closed" + return (RemoteEndPointClosed Nothing, True) + RemoteEndPointClosed (Just err) -> + throwIO err + RemoteEndPointClosed Nothing -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:closeSocket (closed)" -- Read a message and output it on the endPoint's channel By rights we -- should verify that the connection ID is valid, but this is unnecessary @@ -1428,7 +1480,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Arguments ourChannel = localChannel ourEndPoint - ourState = localState ourEndPoint theirState = remoteState theirEndPoint theirAddr = remoteAddress theirEndPoint @@ -1448,12 +1499,13 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do (remoteAddress theirEndPoint) (IntSet.elems $ vst ^. remoteIncoming) writeChan ourChannel . ErrorEvent $ TransportError code (show err) - return RemoteEndPointClosed + forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) + return (RemoteEndPointClosed $ Just err) RemoteEndPointClosing resolved _ -> do putMVar resolved () - return RemoteEndPointClosed - RemoteEndPointClosed -> - return RemoteEndPointClosed + return (RemoteEndPointClosed $ Just err) + RemoteEndPointClosed err' -> + return (RemoteEndPointClosed err') -- | Get the next connection ID -- @@ -1488,12 +1540,6 @@ localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es } nextEndPointId :: Accessor ValidTransportState EndPointId nextEndPointId = accessor _nextEndPointId (\eid st -> st { _nextEndPointId = eid }) -pendingCtrlRequests :: Accessor ValidLocalEndPointState (IntMap (MVar [ByteString])) -pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) - -nextCtrlRequestId :: Accessor ValidLocalEndPointState ControlRequestId -nextCtrlRequestId = accessor _nextCtrlRequestId (\cid st -> st { _nextCtrlRequestId = cid }) - nextConnectionId :: Accessor ValidLocalEndPointState ConnectionId nextConnectionId = accessor _nextConnectionId (\cix st -> st { _nextConnectionId = cix }) @@ -1512,10 +1558,16 @@ remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = remoteIncoming :: Accessor ValidRemoteEndPointState IntSet remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) +pendingCtrlRequests :: Accessor ValidRemoteEndPointState (IntMap (MVar (Either IOException [ByteString]))) +pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) + +nextCtrlRequestId :: Accessor ValidRemoteEndPointState ControlRequestId +nextCtrlRequestId = accessor _nextCtrlRequestId (\cid st -> st { _nextCtrlRequestId = cid }) + localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr -pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidLocalEndPointState (Maybe (MVar [ByteString])) +pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidRemoteEndPointState (Maybe (MVar (Either IOException [ByteString]))) pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral ix) localConnectionTo :: EndPointAddress @@ -1534,4 +1586,8 @@ relyViolation (ourEndPoint, theirEndPoint) str = do elog :: EndPointPair -> String -> IO () elog (ourEndPoint, theirEndPoint) msg = do tid <- myThreadId - putStrLn $ show (localAddress ourEndPoint) ++ "/" ++ show (remoteAddress theirEndPoint) ++ "/" ++ show tid ++ ": " ++ msg + putStrLn $ show (localAddress ourEndPoint) + ++ "/" ++ show (remoteAddress theirEndPoint) + ++ "(" ++ show (remoteId theirEndPoint) ++ ")" + ++ "/" ++ show tid + ++ ": " ++ msg diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 768c8a99..5969ebf9 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -499,10 +499,12 @@ testUnnecessaryConnect nextPort numThreads = do case reply of ConnectionRequestAccepted -> putMVar gotAccepted () + -- We might get either Invalid or Crossed (the transport does not + -- maintain enough history to be able to tell) ConnectionRequestInvalid -> return () ConnectionRequestCrossed -> - throwIO $ userError "Unexpected response (Crossed)" + return () putMVar done () return done diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 2fbe9ffb..09ba5ea1 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -3,7 +3,7 @@ module TestTransport where import Prelude hiding (catch, (>>=), (>>), return, fail) import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) -import Control.Concurrent (forkIO, killThread) +import Control.Concurrent (forkIO, killThread, yield) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) import Control.Exception (evaluate, throw) import Control.Monad (replicateM, replicateM_, when, guard, forM, forM_) @@ -859,6 +859,45 @@ testKill newTransport numThreads = do takeMVar killerDone +-- | Set up conditions with a high likelyhood of "crossing" (for transports +-- that multiplex lightweight connections across heavyweight connections) +testCrossing :: Transport -> Int -> IO () +testCrossing transport numRepeats = do + [aAddr, bAddr] <- replicateM 2 newEmptyMVar + [aDone, bDone] <- replicateM 2 newEmptyMVar + go <- newEmptyMVar + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar aAddr (address endpoint) + theirAddress <- readMVar bAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + Right conn <- connect endpoint theirAddress ReliableOrdered + close conn + putMVar aDone () + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar bAddr (address endpoint) + theirAddress <- readMVar aAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + Right conn <- connect endpoint theirAddress ReliableOrdered + close conn + putMVar bDone () + + -- Driver + forM_ [1 .. numRepeats] $ \_i -> do + putMVar go () + putMVar go () + takeMVar aDone + takeMVar bDone + -- Transport tests testTransport :: IO (Either String Transport) -> IO () testTransport newTransport = do @@ -871,7 +910,8 @@ testTransport newTransport = do , ("CloseOneDirection", testCloseOneDirection transport numPings) , ("CloseReopen", testCloseReopen transport numPings) , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport 100) + , ("SendAfterClose", testSendAfterClose transport 1000) + , ("Crossing", testCrossing transport 1000) , ("CloseTwice", testCloseTwice transport 100) , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) From 61fb67610389073c16b405950a8c1cc90654c587 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 30 May 2012 12:00:50 +0100 Subject: [PATCH 0089/2357] Deal with stray ConnectionRequestCrossed messages --- src/Network/Transport/TCP.hs | 51 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index fe753053..aa49ca4b 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -733,7 +733,7 @@ requestConnectionTo :: LocalEndPoint requestConnectionTo ourEndPoint theirAddress = go where go = do - (theirEndPoint, isNew, _) <- mapIOException connectFailed $ + (theirEndPoint, isNew) <- mapIOException connectFailed $ findRemoteEndPoint ourEndPoint theirAddress Local if isNew @@ -774,6 +774,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do Right (sock, ConnectionRequestCrossed) -> do -- We leave the endpoint in Init state, handleConnectionRequest will -- take care of it + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointClosed Nothing) tryCloseSocket sock return False Left err -> do @@ -812,22 +813,22 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do theirState = remoteState theirEndPoint invalidAddress = TransportError ConnectNotFound --- TODO: Document --- TODO: is this what we want? modifyRemoteState only does something special --- when an exception is thrown in the Valid case. +-- | Resolve an endpoint currently in 'Init' state resolveInit :: EndPointPair -> RemoteState -> IO () resolveInit (ourEndPoint, theirEndPoint) newState = - modifyRemoteState_ (ourEndPoint, theirEndPoint) $ remoteStateIdentity - { caseInit = \resolved _ -> do - putMVar resolved () - return newState - , caseInvalid = \_ -> throwException - , caseValid = \_ -> throwException - , caseClosing = \_ _ -> throwException - , caseClosed = \_ -> throwException - } - where - throwException = throwIO $ userError "resolveInit: not in Init state" + modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit resolved _ -> do + putMVar resolved () + case newState of + RemoteEndPointClosed _ -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + _ -> + return () + return newState + RemoteEndPointClosed (Just ex) -> + throwIO ex + _ -> + relyViolation (ourEndPoint, theirEndPoint) "resolveInit" -- | Establish a connection to a remote endpoint socketToEndPoint :: EndPointAddress -- ^ Our address @@ -1157,10 +1158,10 @@ handleConnectionRequest transport sock = handle handleException $ do go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do - (theirEndPoint, _, crossed) <- + (theirEndPoint, isNew) <- findRemoteEndPoint ourEndPoint theirAddress Remote - if crossed + if not isNew then do tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] tryCloseSocket sock @@ -1193,13 +1194,12 @@ handleConnectionRequest transport sock = handle handleException $ do tryCloseSocket sock -- | Find a remote endpoint. If the remote endpoint does not yet exist we --- create it in Init state. Returns if the endpoint was new and, for requests --- of Remote origin, whether the request crossed. +-- create it in Init state. Returns if the endpoint was new. findRemoteEndPoint :: LocalEndPoint -> EndPointAddress -> Origin - -> IO (RemoteEndPoint, Bool, Bool) + -> IO (RemoteEndPoint, Bool) findRemoteEndPoint ourEndPoint theirAddress findOrigin = go where go = do @@ -1226,7 +1226,7 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go if isNew then - return (theirEndPoint, True, False) + return (theirEndPoint, True) else do let theirState = remoteState theirEndPoint snapshot <- modifyMVar theirState $ \st -> case st of @@ -1251,14 +1251,19 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go case (findOrigin, initOrigin) of (Local, Local) -> readMVar resolved >> go (Local, Remote) -> readMVar resolved >> go - (Remote, Local) -> return (theirEndPoint, False, ourAddress < theirAddress) + (Remote, Local) -> if ourAddress > theirAddress + then + -- Wait for the Crossed message + readMVar resolved >> go + else + return (theirEndPoint, False) (Remote, Remote) -> throwIO $ userError "Already connected (B)" RemoteEndPointValid _ -> -- We assume that the request crossed if we find the endpoint in -- Valid state. It is possible that this is really an invalid -- request, but only in the case of a broken client (we don't -- maintain enough history to be able to tell the difference). - return (theirEndPoint, False, True) + return (theirEndPoint, False) RemoteEndPointClosing resolved _ -> readMVar resolved >> go RemoteEndPointClosed Nothing -> From c22e5b30bac0628ca45ded0e8c08eadc29fa3d9b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 30 May 2012 13:30:21 +0100 Subject: [PATCH 0090/2357] Make testCloseTransport more robust (We relied on getting events in a certain order) --- src/Network/Transport.hs | 20 ++++++++++++-------- tests/TestTransport.hs | 12 ++++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 68171ab4..2b7646d4 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -77,7 +77,7 @@ data Event = | EndPointClosed -- | An error occurred | ErrorEvent (TransportError EventErrorCode) - deriving Show + deriving (Show, Eq) -- | Connection data ConnectHintsIDs enable receivers to distinguish one connection from another. type ConnectionId = Int @@ -87,7 +87,7 @@ data Reliability = ReliableOrdered | ReliableUnordered | Unreliable - deriving Show + deriving (Show, Eq) -- | Multicast group. data MulticastGroup = MulticastGroup { @@ -154,13 +154,17 @@ data TransportError error = TransportError error String -- exceptions. instance (Typeable err, Show err) => Exception (TransportError err) +-- | When comparing errors we ignore the human-readable strings +instance Eq error => Eq (TransportError error) where + TransportError err1 _ == TransportError err2 _ = err1 == err2 + -- | Errors during the creation of an endpoint data NewEndPointErrorCode = -- | Not enough resources NewEndPointInsufficientResources -- | Failed for some other reason | NewEndPointFailed - deriving (Show, Typeable) + deriving (Show, Typeable, Eq) -- | Connection failure data ConnectErrorCode = @@ -170,7 +174,7 @@ data ConnectErrorCode = | ConnectInsufficientResources -- | Failed for other reasons (including syntax error) | ConnectFailed - deriving (Show, Typeable) + deriving (Show, Typeable, Eq) -- | Failure during the creation of a new multicast group data NewMulticastGroupErrorCode = @@ -180,7 +184,7 @@ data NewMulticastGroupErrorCode = | NewMulticastGroupFailed -- | Not all transport implementations support multicast | NewMulticastGroupUnsupported - deriving (Show, Typeable) + deriving (Show, Typeable, Eq) -- | Failure during the resolution of a multicast group data ResolveMulticastGroupErrorCode = @@ -190,7 +194,7 @@ data ResolveMulticastGroupErrorCode = | ResolveMulticastGroupFailed -- | Not all transport implementations support multicast | ResolveMulticastGroupUnsupported - deriving (Show, Typeable) + deriving (Show, Typeable, Eq) -- | Failure during sending a message data SendErrorCode = @@ -198,7 +202,7 @@ data SendErrorCode = SendClosed -- | Send failed for some other reason | SendFailed - deriving (Show, Typeable) + deriving (Show, Typeable, Eq) -- | Error codes used when reporting errors to endpoints (through receive) data EventErrorCode = @@ -208,4 +212,4 @@ data EventErrorCode = | EventTransportFailed -- | Connection to a remote endpoint was lost | EventConnectionLost EndPointAddress [ConnectionId] - deriving Show + deriving (Show, Typeable, Eq) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 09ba5ea1..df94f28a 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -19,6 +19,7 @@ import Data.Map (Map) import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) import Data.String (fromString) import Data.Maybe (catMaybes) +import Data.List (permutations) import Traced -- | We overload connect to always pass the default hints @@ -679,10 +680,13 @@ testCloseTransport newTransport = do Right conn <- connect endpoint theirAddr2 ReliableOrdered send conn ["pong"] - -- Client now closes down its transport. We should receive connection closed messages - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid' == cid1 || cid' == cid2 - ConnectionClosed cid'' <- receive endpoint ; True <- return $ (cid'' == cid1 || cid'' == cid2) && cid'' /= cid' - ErrorEvent (TransportError (EventConnectionLost addr'' []) _) <- receive endpoint ; True <- return $ addr'' == theirAddr2 + -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) + evs <- replicateM 3 $ receive endpoint + let expected = [ ConnectionClosed cid1 + , ConnectionClosed cid2 + , ErrorEvent (TransportError (EventConnectionLost theirAddr2 []) "") + ] + True <- return $ any (== expected) (permutations evs) -- An attempt to send to the endpoint should now fail Left (TransportError SendFailed _) <- send conn ["pong2"] From c3bd4f0642d7ccc164f840fb5679ab42f15a805b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 30 May 2012 17:02:13 +0100 Subject: [PATCH 0091/2357] Fix testReconnect --- network-transport.cabal | 8 +-- src/Network/Transport.hs | 13 +++- src/Network/Transport/Internal.hs | 13 ++++ src/Network/Transport/TCP.hs | 69 +++++++++++-------- tests/TestTCP.hs | 20 +++--- tests/TestTransport.hs | 109 +++++++++++++++++------------- 6 files changed, 140 insertions(+), 92 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 05a61fcf..03351899 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -23,7 +23,7 @@ Library Network.Transport.Chan, Network.Transport.TCP, Network.Transport.Util - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src @@ -39,7 +39,7 @@ Test-Suite TestTCP transformers, ansi-terminal, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N HS-Source-Dirs: tests src @@ -54,7 +54,7 @@ Test-Suite TestMulticastInMemory transformers, ansi-terminal, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -69,6 +69,6 @@ Test-Suite TestInMemory transformers, ansi-terminal, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, EmptyDataDecls, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 2b7646d4..c7f2fd32 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -10,7 +10,7 @@ module Network.Transport ( -- * Types , EndPointAddress(..) , MulticastAddress(..) -- * Hints - , ConnectHints + , ConnectHints(..) , defaultConnectHints -- * Error codes , TransportError(..) @@ -129,11 +129,16 @@ instance Show MulticastAddress where -------------------------------------------------------------------------------- -- Hints used by 'connect' -data ConnectHints +data ConnectHints = ConnectHints { + -- Timeout + connectTimeout :: Maybe Int + } -- Default hints for connecting defaultConnectHints :: ConnectHints -defaultConnectHints = undefined +defaultConnectHints = ConnectHints { + connectTimeout = Nothing + } -------------------------------------------------------------------------------- -- Error codes -- @@ -172,6 +177,8 @@ data ConnectErrorCode = ConnectNotFound -- | Insufficient resources (for instance, no more sockets available) | ConnectInsufficientResources + -- | Timeout + | ConnectTimeout -- | Failed for other reasons (including syntax error) | ConnectFailed deriving (Show, Typeable, Eq) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index e03cc86e..610ac774 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -9,6 +9,7 @@ module Network.Transport.Internal ( -- * Encoders/decoders , mapIOException , tryIO , tryToEnum + , timeoutMaybe -- * Replicated functionality from "base" , void , forkIOWithUnmask @@ -35,6 +36,7 @@ import Control.Exception ( IOException ) import Control.Concurrent (ThreadId, forkIO) import GHC.IO (unsafeUnmask) +import System.Timeout (timeout) --import Control.Concurrent (myThreadId) foreign import ccall unsafe "htonl" htonl :: CInt -> CInt @@ -111,3 +113,14 @@ tryToEnum = go minBound maxBound where go :: Enum b => b -> b -> Int -> Maybe b go lo hi n = if fromEnum lo <= n && n <= fromEnum hi then Just (toEnum n) else Nothing + +-- | If the timeout value is not Nothing, wrap the given computation with a +-- timeout and it if times out throw the specified exception. Identity +-- otherwise. +timeoutMaybe :: Exception e => Maybe Int -> e -> IO a -> IO a +timeoutMaybe Nothing _ f = f +timeoutMaybe (Just n) e f = do + ma <- timeout n f + case ma of + Nothing -> throwIO e + Just a -> return a diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index aa49ca4b..c81048c3 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -40,6 +40,7 @@ import Network.Transport.Internal ( encodeInt32 , tryIO , tryToEnum , void + , timeoutMaybe ) import qualified Network.Socket as N ( HostName , ServiceName @@ -84,7 +85,6 @@ import Control.Exception ( IOException , mask_ , onException , fromException - , catch ) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) @@ -473,12 +473,13 @@ apiConnect :: LocalEndPoint -- ^ Local end point -> Reliability -- ^ Reliability (ignored) -> ConnectHints -- ^ Hints (ignored for now) -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect ourEndPoint theirAddress _reliability _hints = +apiConnect ourEndPoint theirAddress _reliability hints = do if localAddress ourEndPoint == theirAddress then connectToSelf ourEndPoint else try $ do - resetIfInvalid ourEndPoint theirAddress - (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress + resetRemoteEndPoint ourEndPoint theirAddress + (theirEndPoint, connId) <- + requestConnectionTo ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True @@ -669,13 +670,13 @@ internalSocketBetween transport ourAddress theirAddress = do -- | Reset a remote endpoint if it is in Invalid mode -- -- If a user calls the API function 'connect' and the remote endpoint is --- currently in Invalid state, we remove the remote endpoint first because a +-- currently in broken state, we remove the remote endpoint first because a -- new attempt to connect might succeed even if the previous one failed. -- -- Throws a TransportError ConnectFailed exception if the local endpoint is -- closed. -resetIfInvalid :: LocalEndPoint -> EndPointAddress -> IO () -resetIfInvalid ourEndPoint theirAddress = do +resetRemoteEndPoint :: LocalEndPoint -> EndPointAddress -> IO () +resetRemoteEndPoint ourEndPoint theirAddress = do mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of LocalEndPointValid vst -> return (vst ^. localConnectionTo theirAddress) @@ -685,6 +686,8 @@ resetIfInvalid ourEndPoint theirAddress = do withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) + RemoteEndPointClosed (Just _) -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) _ -> return () @@ -729,8 +732,9 @@ createLocalEndPoint transport = do -- May throw a TransportError ConnectErrorCode exception. requestConnectionTo :: LocalEndPoint -> EndPointAddress + -> ConnectHints -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo ourEndPoint theirAddress = go +requestConnectionTo ourEndPoint theirAddress hints = go where go = do (theirEndPoint, isNew) <- mapIOException connectFailed $ @@ -738,9 +742,8 @@ requestConnectionTo ourEndPoint theirAddress = go if isNew then do - void . forkEndPointThread ourEndPoint $ - catch (setupRemoteEndPoint (ourEndPoint, theirEndPoint)) - (\err -> let _ = err :: SomeException in return ()) + void . forkEndPointThread ourEndPoint . handle absorbAllExceptions $ + setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints go else do reply <- mapIOException connectFailed $ @@ -749,10 +752,14 @@ requestConnectionTo ourEndPoint theirAddress = go connectFailed = TransportError ConnectFailed . show + absorbAllExceptions :: SomeException -> IO () + absorbAllExceptions _ex = + return () + -- | Set up a remote endpoint -setupRemoteEndPoint :: EndPointPair -> IO () -setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do - didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress) +setupRemoteEndPoint :: EndPointPair -> ConnectHints -> IO () +setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints = do + didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress hints) onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do @@ -767,7 +774,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) = do resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return True Right (sock, ConnectionRequestInvalid) -> do - let err = invalidAddress "Invalid endpoint" + let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) tryCloseSocket sock return False @@ -820,22 +827,25 @@ resolveInit (ourEndPoint, theirEndPoint) newState = RemoteEndPointInit resolved _ -> do putMVar resolved () case newState of - RemoteEndPointClosed _ -> + RemoteEndPointClosed Nothing -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) _ -> return () return newState - RemoteEndPointClosed (Just ex) -> + RemoteEndPointClosed (Just ex) -> throwIO ex _ -> relyViolation (ourEndPoint, theirEndPoint) "resolveInit" -- | Establish a connection to a remote endpoint +-- +-- Maybe throw a TransportError socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address + -> ConnectHints -- ^ Connection hints -> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse)) -socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do +socketToEndPoint (EndPointAddress ourAddress) theirAddress hints = try $ do (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of Nothing -> throwIO (failed . userError $ "Could not parse") Just dec -> return dec @@ -843,7 +853,9 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do N.getAddrInfo Nothing (Just host) (Just port) bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 - mapIOException invalidAddress $ N.connect sock (N.addrAddress addr) + mapIOException invalidAddress $ + timeoutMaybe (connectTimeout hints) timeoutError $ + N.connect sock (N.addrAddress addr) response <- mapIOException failed $ do sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) recvInt32 sock @@ -858,6 +870,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do invalidAddress = TransportError ConnectNotFound . show insufficientResources = TransportError ConnectInsufficientResources . show failed = TransportError ConnectFailed . show + timeoutError = TransportError ConnectTimeout "Timed out" -- | Remove reference to a remote endpoint from a local endpoint -- @@ -1073,7 +1086,6 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = handleIOException :: IOException -> ValidRemoteEndPointState -> IO () handleIOException ex vst = do tryCloseSocket (remoteSocket vst) - removeRemoteEndPoint (ourEndPoint, theirEndPoint) putMVar theirState (RemoteEndPointClosed $ Just ex) let incoming = IntSet.elems $ vst ^. remoteIncoming code = EventConnectionLost (remoteAddress theirEndPoint) incoming @@ -1148,7 +1160,7 @@ handleConnectionRequest transport sock = handle handleException $ do case vst ^. localEndPointAt ourAddress of Nothing -> do sendMany sock [encodeInt32 ConnectionRequestInvalid] - throwIO $ userError "Invalid endpoint" + throwIO $ userError "handleConnectionRequest: Invalid endpoint" Just ourEndPoint -> return ourEndPoint TransportClosed -> @@ -1157,7 +1169,8 @@ handleConnectionRequest transport sock = handle handleException $ do where go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do - mEndPoint <- handle ((>> return Nothing) . invalidEndPoint) $ do + mEndPoint <- handle ((>> return Nothing) . handleException) $ do + resetRemoteEndPoint ourEndPoint theirAddress (theirEndPoint, isNew) <- findRemoteEndPoint ourEndPoint theirAddress Remote @@ -1185,12 +1198,9 @@ handleConnectionRequest transport sock = handle handleException $ do -- exception from this point forward. forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint - handleException :: IOException -> IO () - handleException _ = tryCloseSocket sock - - invalidEndPoint :: IOException -> IO () - invalidEndPoint _ = do - tryIO $ sendMany sock [encodeInt32 ConnectionRequestInvalid] + handleException :: SomeException -> IO () + handleException _ex = + -- putStrLn $ "handleConnectionRequest " ++ show _ex tryCloseSocket sock -- | Find a remote endpoint. If the remote endpoint does not yet exist we @@ -1268,7 +1278,7 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go readMVar resolved >> go RemoteEndPointClosed Nothing -> go - RemoteEndPointClosed (Just err) -> + RemoteEndPointClosed (Just err) -> throwIO err ourState = localState ourEndPoint @@ -1491,7 +1501,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Deal with a premature exit prematureExit :: N.Socket -> IOException -> IO () prematureExit sock err = do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) tryCloseSocket sock modifyMVar_ theirState $ \st -> case st of diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 5969ebf9..66253766 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -160,7 +160,7 @@ testEarlyDisconnect nextPort = do N.sClose sock -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints -- Request a new connection, but don't wait for the response let reqId = 0 :: Int32 @@ -273,7 +273,7 @@ testEarlyCloseSocket nextPort = do N.sClose sock -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints -- Request a new connection, but don't wait for the response let reqId = 0 :: Int32 @@ -360,7 +360,7 @@ testIgnoreCloseSocket nextPort = do let ourAddress = address endpoint -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints -- Request a new connection tlog "Requesting connection" @@ -440,7 +440,7 @@ testBlockAfterCloseSocket nextPort = do let ourAddress = address endpoint -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints -- Request a new connection tlog "Requesting connection" @@ -495,7 +495,7 @@ testUnnecessaryConnect nextPort numThreads = do dones <- replicateM numThreads $ do done <- newEmptyMVar forkTry $ do - Right (_, reply) <- readMVar serverAddr >>= socketToEndPoint ourAddress + Right (_, reply) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints case reply of ConnectionRequestAccepted -> putMVar gotAccepted () @@ -601,7 +601,7 @@ testReconnect nextPort = do let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 -- The first attempt will fail because no endpoint is yet set up - Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + -- Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints putMVar firstAttempt () -- The second attempt will fail because the server closes the socket before we can request a connection @@ -612,7 +612,8 @@ testReconnect nextPort = do case resultConnect of Nothing -> return () Just (Left (TransportError ConnectFailed _)) -> return () - _ -> fail "testReconnect" + Just (Left err) -> throwIO err + Just (Right _) -> throwIO $ userError "testConnect: unexpected connect success" -- The third attempt succeeds Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -757,7 +758,7 @@ main = do portMVar <- newEmptyMVar forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show let nextPort = takeMVar portMVar - tryIO $ runTests + tcpResult <- tryIO $ runTests [ ("EarlyDisconnect", testEarlyDisconnect nextPort) , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) @@ -772,3 +773,6 @@ main = do , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") + case tcpResult of + Left err -> throwIO err + Right () -> return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index df94f28a..f02cf743 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -4,12 +4,11 @@ module TestTransport where import Prelude hiding (catch, (>>=), (>>), return, fail) import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) import Control.Concurrent (forkIO, killThread, yield) -import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar) -import Control.Exception (evaluate, throw) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar) +import Control.Exception (evaluate, throw, throwIO) import Control.Monad (replicateM, replicateM_, when, guard, forM, forM_) import Control.Monad.Error () -import Network.Transport hiding (connect) -import qualified Network.Transport as NT +import Network.Transport import Network.Transport.Internal (tlog) import Network.Transport.Util (spawn) import System.Random (randomIO) @@ -22,10 +21,6 @@ import Data.Maybe (catMaybes) import Data.List (permutations) import Traced --- | We overload connect to always pass the default hints -connect :: EndPoint -> EndPointAddress -> Reliability -> IO (Either (TransportError ConnectErrorCode) Connection) -connect ep addr rel = NT.connect ep addr rel defaultConnectHints - -- | Server that echoes messages straight back to the origin endpoint. echoServer :: EndPoint -> IO () echoServer endpoint = do @@ -37,7 +32,7 @@ echoServer endpoint = do case event of ConnectionOpened cid rel addr -> do tlog $ "Opened new connection " ++ show cid - Right conn <- connect endpoint addr rel + Right conn <- connect endpoint addr rel defaultConnectHints go (Map.insert cid conn cs) Received cid payload -> do send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload @@ -59,7 +54,7 @@ ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () ping endpoint server numPings msg = do -- Open connection to the server tlog "Connect to echo server" - Right conn <- connect endpoint server ReliableOrdered + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints -- Wait for the server to open reply connection tlog "Wait for ConnectionOpened message" @@ -126,10 +121,10 @@ testConnections transport numPings = do Right endpoint <- newEndPoint transport -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv1 _ _ <- receive endpoint - Right conn2 <- connect endpoint server ReliableOrdered + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv2 _ _ <- receive endpoint -- One thread to send "pingA" on the first connection @@ -164,10 +159,10 @@ testCloseOneConnection transport numPings = do Right endpoint <- newEndPoint transport -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv1 _ _ <- receive endpoint - Right conn2 <- connect endpoint server ReliableOrdered + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv2 _ _ <- receive endpoint -- One thread to send "pingA" on the first connection @@ -212,7 +207,7 @@ testCloseOneDirection transport numPings = do -- Connect to B tlog "Connect to B" - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints -- Wait for B to connect to us tlog "Wait for B" @@ -252,7 +247,7 @@ testCloseOneDirection transport numPings = do -- Connect to A tlog "Connect to A" - Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered + Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints -- Wait for A's pings tlog "Wait for pings from A" @@ -331,7 +326,7 @@ testCloseReopen transport numPings = do forM_ [1 .. numRepeats] $ \i -> do tlog "A connecting" -- Connect to B - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints tlog "A pinging" -- Say hi @@ -368,7 +363,7 @@ testParallelConnects transport numPings = do -- Spawn lots of clients forM_ [1 .. numPings] $ \i -> forkTry $ do - Right conn <- connect endpoint server ReliableOrdered + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints send conn [pack $ "ping" ++ show i] send conn [pack $ "ping" ++ show i] close conn @@ -393,8 +388,8 @@ testSendAfterClose transport numRepeats = do -- We request two lightweight connections replicateM numRepeats $ do - Right conn1 <- connect endpoint server ReliableOrdered - Right conn2 <- connect endpoint server ReliableOrdered + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints -- Close the second, but leave the first open; then output on the second -- connection (i.e., on a closed connection while there is still another @@ -424,8 +419,8 @@ testCloseTwice transport numRepeats = do replicateM numRepeats $ do -- We request two lightweight connections - Right conn1 <- connect endpoint server ReliableOrdered - Right conn2 <- connect endpoint server ReliableOrdered + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints -- Close the second one twice close conn2 @@ -455,7 +450,7 @@ testConnectToSelf transport numPings = do Right endpoint <- newEndPoint transport tlog "Creating self-connection" - Right conn <- connect endpoint (address endpoint) ReliableOrdered + Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints tlog "Talk to myself" @@ -496,8 +491,8 @@ testConnectToSelfTwice transport numPings = do Right endpoint <- newEndPoint transport tlog "Creating self-connection" - Right conn1 <- connect endpoint (address endpoint) ReliableOrdered - Right conn2 <- connect endpoint (address endpoint) ReliableOrdered + Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints tlog "Talk to myself" @@ -541,9 +536,9 @@ testCloseSelf newTransport = do Right transport <- newTransport Right endpoint1 <- newEndPoint transport Right endpoint2 <- newEndPoint transport - Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered - Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered - Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered + Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints -- Close the conneciton and try to send close conn1 @@ -554,13 +549,13 @@ testCloseSelf newTransport = do -- to the second endpoint should still be fine closeEndPoint endpoint1 Left (TransportError SendFailed _) <- send conn2 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered + Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints Right () <- send conn3 ["ping"] -- Close the transport; now the second should no longer work closeTransport transport Left (TransportError SendFailed _) <- send conn3 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered + Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints return () @@ -592,7 +587,7 @@ testCloseEndPoint transport _ = do ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - Right conn <- connect endpoint theirAddr ReliableOrdered + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints send conn ["pong"] ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' @@ -614,7 +609,7 @@ testCloseEndPoint transport _ = do putMVar clientAddr1 (address endpoint) -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint theirAddr ReliableOrdered + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints closeEndPoint endpoint EndPointClosed <- receive endpoint return () @@ -624,7 +619,7 @@ testCloseEndPoint transport _ = do Right endpoint <- newEndPoint transport putMVar clientAddr2 (address endpoint) - Right conn <- connect endpoint theirAddr ReliableOrdered + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints send conn ["ping"] -- Reply from the server @@ -642,7 +637,7 @@ testCloseEndPoint transport _ = do () <- close conn -- And so should an attempt to connect - Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints return () @@ -677,7 +672,7 @@ testCloseTransport newTransport = do ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 - Right conn <- connect endpoint theirAddr2 ReliableOrdered + Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints send conn ["pong"] -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) @@ -703,13 +698,13 @@ testCloseTransport newTransport = do putMVar clientAddr1 (address endpoint1) -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint1 theirAddr ReliableOrdered + Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints -- Set up an endpoint with one outgoing and out incoming connection Right endpoint2 <- newEndPoint transport putMVar clientAddr2 (address endpoint2) - Right conn <- connect endpoint2 theirAddr ReliableOrdered + Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints send conn ["ping"] -- Reply from the server @@ -730,8 +725,8 @@ testCloseTransport newTransport = do () <- close conn -- And so should an attempt to connect on either endpoint - Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered - Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered + Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints -- And finally, so should an attempt to create a new endpoint Left (TransportError NewEndPointFailed _) <- newEndPoint transport @@ -760,7 +755,7 @@ testConnectClosedEndPoint transport = do Right endpoint <- newEndPoint transport readMVar serverClosed - Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered + Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints putMVar clientDone () @@ -794,7 +789,7 @@ testSendException newTransport = do Right endpoint2 <- newEndPoint transport -- Connect endpoint1 to endpoint2 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints ConnectionOpened _ _ _ <- receive endpoint2 -- Send an exceptional value @@ -807,7 +802,7 @@ testSendException newTransport = do ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 -- A new connection will re-establish the connection - Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered + Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints send conn2 ["ping"] close conn2 @@ -836,7 +831,7 @@ testKill newTransport numThreads = do done <- newEmptyMVar tid <- forkIO $ do randomThreadDelay 10 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints randomThreadDelay 10 Right () <- send conn ["ping"] randomThreadDelay 10 @@ -869,8 +864,13 @@ testCrossing :: Transport -> Int -> IO () testCrossing transport numRepeats = do [aAddr, bAddr] <- replicateM 2 newEmptyMVar [aDone, bDone] <- replicateM 2 newEmptyMVar + [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar go <- newEmptyMVar + let hints = defaultConnectHints { + connectTimeout = Just 5000000 + } + -- A forkTry $ do Right endpoint <- newEndPoint transport @@ -879,8 +879,16 @@ testCrossing transport numRepeats = do replicateM_ numRepeats $ do takeMVar go >> yield - Right conn <- connect endpoint theirAddress ReliableOrdered - close conn + -- Because we are creating lots of connections, it's possible that + -- connect times out (for instance, in the TCP transport, + -- Network.Socket.connect may time out). We shouldn't regard this as an + -- error in the Transport, though. + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar aTimeout () + Left (TransportError ConnectFailed _) -> readMVar bTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err putMVar aDone () -- B @@ -891,12 +899,19 @@ testCrossing transport numRepeats = do replicateM_ numRepeats $ do takeMVar go >> yield - Right conn <- connect endpoint theirAddress ReliableOrdered - close conn + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar bTimeout () + Left (TransportError ConnectFailed _) -> readMVar aTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err putMVar bDone () -- Driver forM_ [1 .. numRepeats] $ \_i -> do + -- putStrLn $ "Round " ++ show _i + tryTakeMVar aTimeout + tryTakeMVar bTimeout putMVar go () putMVar go () takeMVar aDone From b64c0de8e75613a087115081759f64b70c9a4fc1 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 31 May 2012 09:23:32 +0100 Subject: [PATCH 0092/2357] Increase timeout for slow machines --- tests/TestAuxiliary.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index 4a8035ec..d912ee6e 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -40,7 +40,7 @@ runTest :: String -> IO () -> IO Bool runTest description test = do putStr $ "Running " ++ show description ++ ": " hFlush stdout - done <- try . timeout 30000000 $ test -- 30 seconds + done <- try . timeout 60000000 $ test -- 60 seconds case done of Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" Right Nothing -> failed $ "(timeout)" From b02b236f062c0307741ade2394c0f6a3b88971fe Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 31 May 2012 10:12:07 +0100 Subject: [PATCH 0093/2357] Weaken testKill to send kill during send only --- tests/TestTCP.hs | 2 ++ tests/TestTransport.hs | 50 +++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 66253766..c15b17d8 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -772,7 +772,9 @@ main = do , ("UnidirectionalError", testUnidirectionalError nextPort) , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] + -- Run the generic tests even if the TCP specific tests failed.. testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") + -- ..but if the generic tests pass, still fail if the specific tests did case tcpResult of Left err -> throwIO err Right () -> return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index f02cf743..35557f53 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -6,7 +6,7 @@ import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) import Control.Concurrent (forkIO, killThread, yield) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar) import Control.Exception (evaluate, throw, throwIO) -import Control.Monad (replicateM, replicateM_, when, guard, forM, forM_) +import Control.Monad (replicateM, replicateM_, when, guard, forM, forM_, unless) import Control.Monad.Error () import Network.Transport import Network.Transport.Internal (tlog) @@ -17,7 +17,6 @@ import Data.ByteString.Char8 (pack) import Data.Map (Map) import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) import Data.String (fromString) -import Data.Maybe (catMaybes) import Data.List (permutations) import Traced @@ -816,44 +815,39 @@ testSendException newTransport = do -- should not affect other threads. -- -- The intention of this test is to see what happens when a asynchronous --- exception happes _while executing a send, connect, or close_. This is --- exceedingly difficult to guarantee, however. Hence we run a large number of --- tests and insert random thread delays -- and even then it might not happen. --- Moreover, it will only happen when we run on multiple cores. +-- exception happes _while executing a send_. This is exceedingly difficult to +-- guarantee, however. Hence we run a large number of tests and insert random +-- thread delays -- and even then it might not happen. Moreover, it will only +-- happen when we run on multiple cores. testKill :: IO (Either String Transport) -> Int -> IO () testKill newTransport numThreads = do Right transport1 <- newTransport Right transport2 <- newTransport Right endpoint1 <- newEndPoint transport1 Right endpoint2 <- newEndPoint transport2 + + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + ConnectionOpened _ _ _ <- receive endpoint2 - threads <- forM [1 .. numThreads] $ \_ -> do - done <- newEmptyMVar - tid <- forkIO $ do - randomThreadDelay 10 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - randomThreadDelay 10 - Right () <- send conn ["ping"] - randomThreadDelay 10 - close conn - putMVar done () - - return (tid, done) + threads <- replicateM numThreads . forkIO $ do + randomThreadDelay 10 + Right () <- send conn ["ping"] + return () -- Kill half of those threads, and wait on the rest killerDone <- newEmptyMVar forkIO $ do - wait <- forM threads $ \(tid, done) -> do + killed <- forM threads $ \tid -> do shouldKill <- randomIO - if shouldKill - then do - randomThreadDelay 30 - killThread tid - return Nothing - else - return (Just done) - - mapM_ takeMVar (catMaybes wait) + when shouldKill $ randomThreadDelay 10 >> killThread tid + return shouldKill + + -- We should receive at least the pings from the threads that we didn't + -- kill (we might get more, depending on when exactly the kill happens) + forM_ killed $ \wasKilled -> unless wasKilled $ do + Received _ ["ping"] <- receive endpoint2 + return () + putMVar killerDone () takeMVar killerDone From f471f3e41306f3bc7a94243c39175bf94f2e8658 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 31 May 2012 11:42:59 +0100 Subject: [PATCH 0094/2357] Code cleanup --- src/Network/Transport/TCP.hs | 94 +++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index c81048c3..1e9fb9db 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -473,7 +473,7 @@ apiConnect :: LocalEndPoint -- ^ Local end point -> Reliability -- ^ Reliability (ignored) -> ConnectHints -- ^ Hints (ignored for now) -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect ourEndPoint theirAddress _reliability hints = do +apiConnect ourEndPoint theirAddress _reliability hints = if localAddress ourEndPoint == theirAddress then connectToSelf ourEndPoint else try $ do @@ -562,24 +562,25 @@ apiCloseEndPoint transport evs ourEndPoint = do where -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO () - tryCloseRemoteSocket theirEndPoint = + tryCloseRemoteSocket theirEndPoint = do -- We make an attempt to close the connection nicely -- (by sending a CloseSocket first) + let closed = RemoteEndPointClosed .Just . userError $ "apiCloseEndPoint" modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> return st RemoteEndPointInit resolved _ -> do putMVar resolved () - return $ RemoteEndPointClosed (Just . userError $ "apiCloseEndPoint") + return closed RemoteEndPointValid conn -> do tryIO $ sendOn conn [encodeInt32 CloseSocket] tryCloseSocket (remoteSocket conn) - return $ RemoteEndPointClosed (Just . userError $ "apiCloseEndPoint") + return closed RemoteEndPointClosing resolved conn -> do putMVar resolved () tryCloseSocket (remoteSocket conn) - return $ RemoteEndPointClosed (Just . userError $ "apiCloseEndPoint") + return closed RemoteEndPointClosed err -> return $ RemoteEndPointClosed err @@ -760,33 +761,32 @@ requestConnectionTo ourEndPoint theirAddress hints = go setupRemoteEndPoint :: EndPointPair -> ConnectHints -> IO () setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints = do didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress hints) - onError $ \result -> - case result of - Right (sock, ConnectionRequestAccepted) -> do - let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 - } - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) - return True - Right (sock, ConnectionRequestInvalid) -> do - let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - tryCloseSocket sock - return False - Right (sock, ConnectionRequestCrossed) -> do - -- We leave the endpoint in Init state, handleConnectionRequest will - -- take care of it - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointClosed Nothing) - tryCloseSocket sock - return False - Left err -> do - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - return False + onError $ \result -> case result of + Right (sock, ConnectionRequestAccepted) -> do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 + } + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) + return True + Right (sock, ConnectionRequestInvalid) -> do + let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + tryCloseSocket sock + return False + Right (sock, ConnectionRequestCrossed) -> do + -- We leave the endpoint in Init state, handleConnectionRequest will + -- take care of it + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointClosed Nothing) + tryCloseSocket sock + return False + Left err -> do + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + return False -- If we get to this point without an exception, then -- @@ -794,7 +794,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints = do -- if didAccept is True, the socket has been stored as part of the remote -- state so we no longer need to worry about closing it when an -- asynchronous exception occurs - when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) + when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) where -- If an asynchronous exception occurs while we set up the remote endpoint -- we need to make sure to close the socket. It is also useful to @@ -857,7 +857,7 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress hints = try $ do timeoutMaybe (connectTimeout hints) timeoutError $ N.connect sock (N.addrAddress addr) response <- mapIOException failed $ do - sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) + sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) recvInt32 sock case tryToEnum response of Nothing -> throwIO (failed . userError $ "Unexpected response") @@ -947,10 +947,14 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do $ vst ) -- Error cases - , caseInvalid = throwIO - , caseInit = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" - , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - , caseClosed = \_ -> throwIO $ userError "doRemoteRequest: Remote endpoint closed" + , caseInvalid = + throwIO + , caseInit = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" + , caseClosing = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" + , caseClosed = \_ -> + throwIO $ userError "doRemoteRequest: Remote endpoint closed" } mReply <- takeMVar replyMVar case mReply of @@ -1014,7 +1018,7 @@ forkEndPointThread ourEndPoint p = ourState = localState ourEndPoint -------------------------------------------------------------------------------- --- As soon as a remote connection fails, we want to put notify our endpoint -- +-- As soon as a remote connection fails, we want to put notify our endpoint -- -- and put it into a closed state. Since this may happen in many places, we -- -- provide some abstractions. -- -------------------------------------------------------------------------------- @@ -1296,9 +1300,11 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do mSock <- withMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (invalid)" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages (invalid)" RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (init)" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages (init)" RemoteEndPointValid ep -> return . Just $ remoteSocket ep RemoteEndPointClosing _ ep -> @@ -1505,9 +1511,11 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:prematureExit" RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do let code = EventConnectionLost (remoteAddress theirEndPoint) From 8df232a92fef8681752f619312dff63f874e5490 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 31 May 2012 15:45:14 +0100 Subject: [PATCH 0095/2357] Obsolete comments --- src/Network/Transport/TCP.hs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 1e9fb9db..8af24ad8 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -471,7 +471,7 @@ apiNewEndPoint transport = try $ do apiConnect :: LocalEndPoint -- ^ Local end point -> EndPointAddress -- ^ Remote address -> Reliability -- ^ Reliability (ignored) - -> ConnectHints -- ^ Hints (ignored for now) + -> ConnectHints -- ^ Hints -> IO (Either (TransportError ConnectErrorCode) Connection) apiConnect ourEndPoint theirAddress _reliability hints = if localAddress ourEndPoint == theirAddress @@ -779,8 +779,6 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints = do tryCloseSocket sock return False Right (sock, ConnectionRequestCrossed) -> do - -- We leave the endpoint in Init state, handleConnectionRequest will - -- take care of it resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointClosed Nothing) tryCloseSocket sock return False From c2a1f98baf5381741019cfc1391a6ead1ce3288a Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 1 Jun 2012 14:47:52 +0100 Subject: [PATCH 0096/2357] Better names --- src/Network/Transport/TCP.hs | 167 +++++++++++++++++++++-------------- 1 file changed, 100 insertions(+), 67 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 8af24ad8..0de43cf0 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -324,15 +324,23 @@ data RemoteEndPoint = RemoteEndPoint , remoteId :: Int } -data Origin = Local | Remote +data RequestedBy = RequestedByUs | RequestedByThem deriving (Eq, Show) data RemoteState = + -- | Invalid remote endpoint (for example, invalid address) RemoteEndPointInvalid (TransportError ConnectErrorCode) - | RemoteEndPointInit (MVar ()) Origin + -- | The remote endpoint is being initialized + | RemoteEndPointInit (MVar ()) RequestedBy + -- | "Normal" working endpoint | RemoteEndPointValid ValidRemoteEndPointState + -- | The remote endpoint is being closed (garbage collected) | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState - | RemoteEndPointClosed (Maybe IOException) + -- | The remote endpoint has been closed (garbage collected) + | RemoteEndPointClosed + -- | The remote endpoint has failed, or has been forcefully shutdown + -- using a closeTransport or closeEndPoint API call + | RemoteEndPointFailed IOException data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int @@ -477,7 +485,7 @@ apiConnect ourEndPoint theirAddress _reliability hints = if localAddress ourEndPoint == theirAddress then connectToSelf ourEndPoint else try $ do - resetRemoteEndPoint ourEndPoint theirAddress + resetIfBroken ourEndPoint theirAddress (theirEndPoint, connId) <- requestConnectionTo ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected @@ -530,7 +538,12 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = if alive then relyViolation (ourEndPoint, theirEndPoint) "apiSend" else throwIO $ TransportError SendClosed "Connection closed" - , caseClosed = \err -> do + , caseClosed = do + alive <- readIORef connAlive + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + else throwIO $ TransportError SendClosed "Connection closed" + , caseFailed = \err -> do alive <- readIORef connAlive if alive then throwIO $ TransportError SendFailed (show err) @@ -565,7 +578,7 @@ apiCloseEndPoint transport evs ourEndPoint = do tryCloseRemoteSocket theirEndPoint = do -- We make an attempt to close the connection nicely -- (by sending a CloseSocket first) - let closed = RemoteEndPointClosed .Just . userError $ "apiCloseEndPoint" + let closed = RemoteEndPointFailed . userError $ "apiCloseEndPoint" modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> @@ -581,8 +594,10 @@ apiCloseEndPoint transport evs ourEndPoint = do putMVar resolved () tryCloseSocket (remoteSocket conn) return closed - RemoteEndPointClosed err -> - return $ RemoteEndPointClosed err + RemoteEndPointClosed -> + return st + RemoteEndPointFailed err -> + return $ RemoteEndPointFailed err -- | Special case of 'apiConnect': connect an endpoint to itself connectToSelf :: LocalEndPoint @@ -659,10 +674,12 @@ internalSocketBetween transport ourAddress theirAddress = do return $ remoteSocket vst RemoteEndPointClosing _ vst -> return $ remoteSocket vst - RemoteEndPointClosed _ -> + RemoteEndPointClosed -> throwIO $ userError "Remote endpoint closed" - RemoteEndPointInvalid _ -> - throwIO $ userError "Remote endpoint invalid" + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointFailed err -> + throwIO err -------------------------------------------------------------------------------- -- Lower level functionality -- @@ -676,8 +693,8 @@ internalSocketBetween transport ourAddress theirAddress = do -- -- Throws a TransportError ConnectFailed exception if the local endpoint is -- closed. -resetRemoteEndPoint :: LocalEndPoint -> EndPointAddress -> IO () -resetRemoteEndPoint ourEndPoint theirAddress = do +resetIfBroken :: LocalEndPoint -> EndPointAddress -> IO () +resetIfBroken ourEndPoint theirAddress = do mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of LocalEndPointValid vst -> return (vst ^. localConnectionTo theirAddress) @@ -687,7 +704,7 @@ resetRemoteEndPoint ourEndPoint theirAddress = do withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) - RemoteEndPointClosed (Just _) -> + RemoteEndPointFailed _ -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) _ -> return () @@ -739,7 +756,7 @@ requestConnectionTo ourEndPoint theirAddress hints = go where go = do (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress Local + findRemoteEndPoint ourEndPoint theirAddress RequestedByUs if isNew then do @@ -779,7 +796,7 @@ setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints = do tryCloseSocket sock return False Right (sock, ConnectionRequestCrossed) -> do - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointClosed Nothing) + resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed tryCloseSocket sock return False Left err -> do @@ -825,12 +842,12 @@ resolveInit (ourEndPoint, theirEndPoint) newState = RemoteEndPointInit resolved _ -> do putMVar resolved () case newState of - RemoteEndPointClosed Nothing -> + RemoteEndPointClosed -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) _ -> return () return newState - RemoteEndPointClosed (Just ex) -> + RemoteEndPointFailed ex -> throwIO ex _ -> relyViolation (ourEndPoint, theirEndPoint) "resolveInit" @@ -951,8 +968,10 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" , caseClosing = \_ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - , caseClosed = \_ -> - throwIO $ userError "doRemoteRequest: Remote endpoint closed" + , caseClosed = + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" + , caseFailed = + throwIO } mReply <- takeMVar replyMVar case mReply of @@ -1023,10 +1042,11 @@ forkEndPointThread ourEndPoint p = data RemoteStatePatternMatch a = RemoteStatePatternMatch { caseInvalid :: TransportError ConnectErrorCode -> IO a - , caseInit :: MVar () -> Origin -> IO a + , caseInit :: MVar () -> RequestedBy -> IO a , caseValid :: ValidRemoteEndPointState -> IO a , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a - , caseClosed :: Maybe IOException -> IO a + , caseClosed :: IO a + , caseFailed :: IOException -> IO a } remoteStateIdentity :: RemoteStatePatternMatch RemoteState @@ -1036,7 +1056,8 @@ remoteStateIdentity = , caseInit = (return .) . RemoteEndPointInit , caseValid = return . RemoteEndPointValid , caseClosing = (return .) . RemoteEndPointClosing - , caseClosed = return . RemoteEndPointClosed + , caseClosed = return RemoteEndPointClosed + , caseFailed = return . RemoteEndPointFailed } -- | Like modifyMVar, but if an I/O exception occurs don't restore the remote @@ -1076,8 +1097,13 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = (putMVar theirState st) putMVar theirState st' return a - RemoteEndPointClosed err -> do - (st', a) <- onException (restore $ caseClosed match err) + RemoteEndPointClosed -> do + (st', a) <- onException (restore $ caseClosed match) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointFailed err -> do + (st', a) <- onException (restore $ caseFailed match err) (putMVar theirState st) putMVar theirState st' return a @@ -1088,7 +1114,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = handleIOException :: IOException -> ValidRemoteEndPointState -> IO () handleIOException ex vst = do tryCloseSocket (remoteSocket vst) - putMVar theirState (RemoteEndPointClosed $ Just ex) + putMVar theirState (RemoteEndPointFailed ex) let incoming = IntSet.elems $ vst ^. remoteIncoming code = EventConnectionLost (remoteAddress theirEndPoint) incoming err = TransportError code (show ex) @@ -1105,7 +1131,8 @@ modifyRemoteState_ (ourEndPoint, theirEndPoint) match = , caseInit = \resolved origin -> u $ caseInit match resolved origin , caseValid = u . caseValid match , caseClosing = \resolved vst -> u $ caseClosing match resolved vst - , caseClosed = u . caseClosed match + , caseClosed = u $ caseClosed match + , caseFailed = u . caseFailed match } where u :: IO a -> IO (a, ()) @@ -1130,9 +1157,12 @@ withRemoteState (ourEndPoint, theirEndPoint) match = , caseClosing = \resolved vst -> do a <- caseClosing match resolved vst return (RemoteEndPointClosing resolved vst, a) - , caseClosed = \err -> do - a <- caseClosed match err - return (RemoteEndPointClosed err, a) + , caseClosed = do + a <- caseClosed match + return (RemoteEndPointClosed, a) + , caseFailed = \err -> do + a <- caseFailed match err + return (RemoteEndPointFailed err, a) } -------------------------------------------------------------------------------- @@ -1172,9 +1202,9 @@ handleConnectionRequest transport sock = handle handleException $ do go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do mEndPoint <- handle ((>> return Nothing) . handleException) $ do - resetRemoteEndPoint ourEndPoint theirAddress + resetIfBroken ourEndPoint theirAddress (theirEndPoint, isNew) <- - findRemoteEndPoint ourEndPoint theirAddress Remote + findRemoteEndPoint ourEndPoint theirAddress RequestedByThem if not isNew then do @@ -1210,7 +1240,7 @@ handleConnectionRequest transport sock = handle handleException $ do findRemoteEndPoint :: LocalEndPoint -> EndPointAddress - -> Origin + -> RequestedBy -> IO (RemoteEndPoint, Bool) findRemoteEndPoint ourEndPoint theirAddress findOrigin = go where @@ -1244,12 +1274,12 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go snapshot <- modifyMVar theirState $ \st -> case st of RemoteEndPointValid vst -> case findOrigin of - Local -> do + RequestedByUs -> do let st' = RemoteEndPointValid . (remoteOutgoing ^: (+ 1)) $ vst return (st', st') - Remote -> + RequestedByThem -> return (st, st) _ -> return (st, st) @@ -1261,15 +1291,19 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go throwIO err RemoteEndPointInit resolved initOrigin -> case (findOrigin, initOrigin) of - (Local, Local) -> readMVar resolved >> go - (Local, Remote) -> readMVar resolved >> go - (Remote, Local) -> if ourAddress > theirAddress - then - -- Wait for the Crossed message - readMVar resolved >> go - else - return (theirEndPoint, False) - (Remote, Remote) -> throwIO $ userError "Already connected (B)" + (RequestedByUs, RequestedByUs) -> + readMVar resolved >> go + (RequestedByUs, RequestedByThem) -> + readMVar resolved >> go + (RequestedByThem, RequestedByUs) -> + if ourAddress > theirAddress + then + -- Wait for the Crossed message + readMVar resolved >> go + else + return (theirEndPoint, False) + (RequestedByThem, RequestedByThem) -> + throwIO $ userError "Already connected (B)" RemoteEndPointValid _ -> -- We assume that the request crossed if we find the endpoint in -- Valid state. It is possible that this is really an invalid @@ -1278,18 +1312,14 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go return (theirEndPoint, False) RemoteEndPointClosing resolved _ -> readMVar resolved >> go - RemoteEndPointClosed Nothing -> + RemoteEndPointClosed -> go - RemoteEndPointClosed (Just err) -> + RemoteEndPointFailed err -> throwIO err ourState = localState ourEndPoint ourAddress = localAddress ourEndPoint - - - - -- | Handle requests from a remote endpoint. -- -- Returns only if the remote party closes the socket or if an error occurs. @@ -1307,9 +1337,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do return . Just $ remoteSocket ep RemoteEndPointClosing _ ep -> return . Just $ remoteSocket ep - RemoteEndPointClosed _ -> - -- The remote endpoint got closed before we got a chance to start - -- dealing with incoming messages + RemoteEndPointClosed -> + return Nothing + RemoteEndPointFailed _ -> return Nothing forM_ mSock $ \sock -> @@ -1377,9 +1407,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- to RemoteEndPointValid putMVar resolved () return (remoteIncoming ^= IntSet.singleton newId $ vst) - RemoteEndPointClosed (Just err) -> + RemoteEndPointFailed err -> throwIO err - RemoteEndPointClosed Nothing -> + RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "createNewConnection (closed)" sendOn vst ( encodeInt32 ControlResponse @@ -1411,9 +1441,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do ) RemoteEndPointClosing _ _ -> throwIO $ userError "Invalid control response" - RemoteEndPointClosed (Just err) -> + RemoteEndPointFailed err -> throwIO err - RemoteEndPointClosed Nothing -> + RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "readControlResponse (closed)" case mmvar of @@ -1444,9 +1474,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- far as we are concerned there are no incoming connections. This -- means that a CloseConnection request at this point is invalid. throwIO $ userError "Invalid CloseConnection request" - RemoteEndPointClosed (Just err) -> + RemoteEndPointFailed err -> throwIO err - RemoteEndPointClosed Nothing -> + RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (closed)" writeChan ourChannel (ConnectionClosed cid) closeIfUnused (ourEndPoint, theirEndPoint) @@ -1476,17 +1506,17 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Attempt to reply (but don't insist) tryIO $ sendOn vst' [encodeInt32 CloseSocket] tryCloseSocket sock - return (RemoteEndPointClosed Nothing, True) + return (RemoteEndPointClosed, True) else return (RemoteEndPointValid vst', False) RemoteEndPointClosing resolved _ -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) tryCloseSocket sock putMVar resolved () - return (RemoteEndPointClosed Nothing, True) - RemoteEndPointClosed (Just err) -> + return (RemoteEndPointClosed, True) + RemoteEndPointFailed err -> throwIO err - RemoteEndPointClosed Nothing -> + RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (closed)" @@ -1520,12 +1550,15 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do (IntSet.elems $ vst ^. remoteIncoming) writeChan ourChannel . ErrorEvent $ TransportError code (show err) forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) - return (RemoteEndPointClosed $ Just err) + return (RemoteEndPointFailed err) RemoteEndPointClosing resolved _ -> do putMVar resolved () - return (RemoteEndPointClosed $ Just err) - RemoteEndPointClosed err' -> - return (RemoteEndPointClosed err') + return (RemoteEndPointFailed err) + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:prematureExit" + RemoteEndPointFailed err' -> + return (RemoteEndPointFailed err') -- | Get the next connection ID -- From c4f777f01f71262b38b12c0fe686835a32984f59 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 1 Jun 2012 15:06:58 +0100 Subject: [PATCH 0097/2357] Update documentation --- src/Network/Transport/TCP.hs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 0de43cf0..89b8498f 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -251,21 +251,22 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- -- Remote endpoints (basically, TCP connections) have the following lifecycle: -- --- Init ----+---> Invalid +-- Init ---+---> Invalid -- | --- | /----------------\ --- | | | --- | v | --- +---> Valid ----+---> Closing --- | | | --- | | v --- \---------------+---> Closed +-- | /--------------\ +-- | | | +-- | v | +-- +---> Valid ---+---> Closing ---+---> Closed +-- | | | | +-- | | | v +-- \-------+--------------+------------> Failed -- -- Init: There are two places where we create new remote endpoints: in -- requestConnectionTo (in response to an API 'connect' call) and in -- handleConnectionRequest (when a remote node tries to connect to us). -- 'Init' carries an MVar () 'resolved' which concurrent threads can use to --- wait for the remote endpoint to finish initialization. +-- wait for the remote endpoint to finish initialization. We record who +-- requested the connection (the local endpoint or the remote endpoint). -- -- Invalid: We put the remote endpoint in invalid state only during -- requestConnectionTo when we fail to connect. @@ -281,8 +282,11 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- the request. -- -- Closed: The endpoint is put in Closed state after a successful garbage --- collection, when the endpoint (or the whole transport) is closed manually, --- or when an IO exception occurs during communication. +-- collection. +-- +-- Failed: If the connection to the remote endpoint is lost, or the local +-- endpoint (or the whole transport) is closed manually, the remote endpoint is +-- put in Failed state, and we record the reason. -- -- Invariants for dealing with remote endpoints: -- @@ -290,7 +294,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- interleaving bits of payload). -- -- INV-CLOSE: Local endpoints should never point to remote endpoint in closed --- state. Whenever we put an endpoint in closed state we remove that +-- state. Whenever we put an endpoint in Closed state we remove that -- endpoint from localConnections first, so that if a concurrent thread reads -- the MVar, finds RemoteEndPointClosed, and then looks up the endpoint in -- localConnections it is guaranteed to either find a different remote From 19e8f6645ca4e2dc1ea3eea1dcdd3735de31577d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 1 Jun 2012 15:50:36 +0100 Subject: [PATCH 0098/2357] Start support for TCP parameters --- src/Network/Transport/Internal/TCP.hs | 7 +- src/Network/Transport/TCP.hs | 125 +++++++++++++++++--------- tests/TestTCP.hs | 47 +++++----- 3 files changed, 113 insertions(+), 66 deletions(-) diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index e368f837..0133b154 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -27,7 +27,7 @@ import qualified Network.Socket as N ( HostName ) import qualified Network.Socket.ByteString as NBS (recv) import Control.Concurrent (ThreadId) -import Control.Monad (forever) +import Control.Monad (forever, when) import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) import Control.Applicative ((<$>)) import Data.ByteString (ByteString) @@ -54,17 +54,18 @@ import Data.Int (Int32) forkServer :: N.HostName -- ^ Host -> N.ServiceName -- ^ Port -> Int -- ^ Backlog (maximum number of queued connections) + -> Bool -- ^ Set ReuseAddr option? -> (SomeException -> IO ()) -- ^ Termination handler -> (N.Socket -> IO ()) -- ^ Request handler -> IO ThreadId -forkServer host port backlog terminationHandler requestHandler = do +forkServer host port backlog reuseAddr terminationHandler requestHandler = do -- Resolve the specified address. By specification, getAddrInfo will never -- return an empty list (but will throw an exception instead) and will return -- the "best" address first, whatever that means addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just host) (Just port) bracketOnError (N.socket (N.addrFamily addr) N.Stream N.defaultProtocol) tryCloseSocket $ \sock -> do - N.setSocketOption sock N.ReuseAddr 1 + when reuseAddr $ N.setSocketOption sock N.ReuseAddr 1 N.bindSocket sock (N.addrAddress addr) N.listen sock backlog mask_ $ forkIOWithUnmask $ \unmask -> diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 89b8498f..2a613cd6 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -12,6 +12,8 @@ -- compatibility (see "Network.Socket"). module Network.Transport.TCP ( -- * Main API createTransport + , TCPParameters(..) + , defaultTCPParameters -- * Internals (exposed for unit tests) , createTransportExposeInternals , TransportInternals(..) @@ -216,9 +218,10 @@ import Data.Foldable (forM_, mapM_) -- ValidRemoteEndPointState). data TCPTransport = TCPTransport - { transportHost :: N.HostName - , transportPort :: N.ServiceName - , transportState :: MVar TransportState + { transportHost :: N.HostName + , transportPort :: N.ServiceName + , transportState :: MVar TransportState + , transportParams :: TCPParameters } data TransportState = @@ -381,6 +384,19 @@ data ConnectionRequestResponse = | ConnectionRequestCrossed deriving (Enum, Bounded, Show) +-- Parameters for setting up the TCP transport +data TCPParameters = TCPParameters { + -- | Backlog for 'listen'. + -- Defaults to SOMAXCONN. + tcpBacklog :: Int + -- | Should we set SO_REUSEADDR on the server socket? + -- Defaults to True. + , tcpReuseServerAddr :: Bool + -- | Should we set SO_REUSEADDR on client sockets? + -- Defaults to True. + , tcpReuseClientAddr :: Bool + } + -- Internal functionality we expose for unit testing data TransportInternals = TransportInternals { -- | The ID of the thread that listens for new incoming connections @@ -398,42 +414,54 @@ data TransportInternals = TransportInternals -- | Create a TCP transport createTransport :: N.HostName -> N.ServiceName + -> TCPParameters -> IO (Either IOException Transport) -createTransport host port = - either Left (Right . fst) <$> createTransportExposeInternals host port +createTransport host port params = + either Left (Right . fst) <$> createTransportExposeInternals host port params -- | You should probably not use this function (used for unit testing only) createTransportExposeInternals :: N.HostName -> N.ServiceName + -> TCPParameters -> IO (Either IOException (Transport, TransportInternals)) -createTransportExposeInternals host port = do +createTransportExposeInternals host port params = do state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty , _nextEndPointId = 0 } - let transport = TCPTransport { transportState = state - , transportHost = host - , transportPort = port + let transport = TCPTransport { transportState = state + , transportHost = host + , transportPort = port + , transportParams = params } - tryIO $ do - tid <- forkServer host port N.sOMAXCONN - (terminationHandler transport) - (handleConnectionRequest transport) - return - ( Transport - { newEndPoint = apiNewEndPoint transport - , closeTransport = let evs = [ EndPointClosed - , throw $ userError "Transport closed" - ] in - apiCloseTransport transport (Just tid) evs - } - , TransportInternals - { transportThread = tid - , socketBetween = internalSocketBetween transport - } - ) + tryIO $ bracketOnError (forkServer + host + port + (tcpBacklog params) + (tcpReuseServerAddr params) + (terminationHandler transport) + (handleConnectionRequest transport)) + killThread + (mkTransport transport) where + mkTransport :: TCPTransport + -> ThreadId + -> IO (Transport, TransportInternals) + mkTransport transport tid = return + ( Transport + { newEndPoint = apiNewEndPoint transport + , closeTransport = let evs = [ EndPointClosed + , throw $ userError "Transport closed" + ] in + apiCloseTransport transport (Just tid) evs + } + , TransportInternals + { transportThread = tid + , socketBetween = internalSocketBetween transport + } + ) + terminationHandler :: TCPTransport -> SomeException -> IO () terminationHandler transport ex = do let evs = [ ErrorEvent (TransportError EventTransportFailed (show ex)) @@ -441,6 +469,14 @@ createTransportExposeInternals host port = do ] apiCloseTransport transport Nothing evs +-- | Default TCP parameters +defaultTCPParameters :: TCPParameters +defaultTCPParameters = TCPParameters { + tcpBacklog = N.sOMAXCONN + , tcpReuseServerAddr = True + , tcpReuseClientAddr = True + } + -------------------------------------------------------------------------------- -- API functions -- -------------------------------------------------------------------------------- @@ -465,7 +501,7 @@ apiNewEndPoint transport = try $ do return EndPoint { receive = readChan (localChannel ourEndPoint) , address = localAddress ourEndPoint - , connect = apiConnect ourEndPoint + , connect = apiConnect (transportParams transport) ourEndPoint , closeEndPoint = let evs = [ EndPointClosed , throw $ userError "Endpoint closed" ] in @@ -480,18 +516,19 @@ apiNewEndPoint transport = try $ do TransportError ResolveMulticastGroupUnsupported "Multicast not supported" -- | Connnect to an endpoint -apiConnect :: LocalEndPoint -- ^ Local end point +apiConnect :: TCPParameters -- ^ Parameters + -> LocalEndPoint -- ^ Local end point -> EndPointAddress -- ^ Remote address -> Reliability -- ^ Reliability (ignored) -> ConnectHints -- ^ Hints -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect ourEndPoint theirAddress _reliability hints = +apiConnect params ourEndPoint theirAddress _reliability hints = if localAddress ourEndPoint == theirAddress then connectToSelf ourEndPoint else try $ do resetIfBroken ourEndPoint theirAddress (theirEndPoint, connId) <- - requestConnectionTo ourEndPoint theirAddress hints + requestConnectionTo params ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True @@ -752,11 +789,12 @@ createLocalEndPoint transport = do -- additionally block until that is resolved. -- -- May throw a TransportError ConnectErrorCode exception. -requestConnectionTo :: LocalEndPoint +requestConnectionTo :: TCPParameters + -> LocalEndPoint -> EndPointAddress -> ConnectHints -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo ourEndPoint theirAddress hints = go +requestConnectionTo params ourEndPoint theirAddress hints = go where go = do (theirEndPoint, isNew) <- mapIOException connectFailed $ @@ -765,7 +803,7 @@ requestConnectionTo ourEndPoint theirAddress hints = go if isNew then do void . forkEndPointThread ourEndPoint . handle absorbAllExceptions $ - setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints + setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints go else do reply <- mapIOException connectFailed $ @@ -779,9 +817,13 @@ requestConnectionTo ourEndPoint theirAddress hints = go return () -- | Set up a remote endpoint -setupRemoteEndPoint :: EndPointPair -> ConnectHints -> IO () -setupRemoteEndPoint (ourEndPoint, theirEndPoint) hints = do - didAccept <- bracketOnError (socketToEndPoint ourAddress theirAddress hints) +setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () +setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do + didAccept <- bracketOnError (socketToEndPoint + ourAddress + theirAddress + (tcpReuseClientAddr params) + (connectTimeout hints)) onError $ \result -> case result of Right (sock, ConnectionRequestAccepted) -> do let vst = ValidRemoteEndPointState @@ -861,19 +903,22 @@ resolveInit (ourEndPoint, theirEndPoint) newState = -- Maybe throw a TransportError socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address - -> ConnectHints -- ^ Connection hints + -> Bool -- ^ Use SO_REUSEADDR? + -> Maybe Int -- ^ Timeout for connect -> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse)) -socketToEndPoint (EndPointAddress ourAddress) theirAddress hints = try $ do +socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = + try $ do (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of Nothing -> throwIO (failed . userError $ "Could not parse") Just dec -> return dec addr:_ <- mapIOException invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port) bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do - mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 + when reuseAddr $ + mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 mapIOException invalidAddress $ - timeoutMaybe (connectTimeout hints) timeoutError $ + timeoutMaybe timeout timeoutError $ N.connect sock (N.addrAddress addr) response <- mapIOException failed $ do sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index c15b17d8..df115b38 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -10,6 +10,7 @@ import Network.Transport.TCP ( createTransport , createTransportExposeInternals , TransportInternals(..) , encodeEndPointAddress + , defaultTCPParameters ) import Data.Int (Int32) import Control.Concurrent (threadDelay, killThread) @@ -82,7 +83,7 @@ testEarlyDisconnect nextPort = do server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () server serverAddr clientAddr serverDone = do tlog "Server" - Right transport <- nextPort >>= createTransport "127.0.0.1" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) theirAddr <- readMVar clientAddr @@ -134,7 +135,7 @@ testEarlyDisconnect nextPort = do putMVar clientAddr ourAddress -- Listen for incoming messages - forkServer "127.0.0.1" clientPort 5 throwIO $ \sock -> do + forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do -- Initial setup 0 <- recvInt32 sock :: IO Int _ <- recvWithLength sock @@ -160,7 +161,7 @@ testEarlyDisconnect nextPort = do N.sClose sock -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing -- Request a new connection, but don't wait for the response let reqId = 0 :: Int32 @@ -186,7 +187,7 @@ testEarlyCloseSocket nextPort = do server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () server serverAddr clientAddr serverDone = do tlog "Server" - Right transport <- nextPort >>= createTransport "127.0.0.1" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) theirAddr <- readMVar clientAddr @@ -245,7 +246,7 @@ testEarlyCloseSocket nextPort = do putMVar clientAddr ourAddress -- Listen for incoming messages - forkServer "127.0.0.1" clientPort 5 throwIO $ \sock -> do + forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do -- Initial setup 0 <- recvInt32 sock :: IO Int _ <- recvWithLength sock @@ -273,7 +274,7 @@ testEarlyCloseSocket nextPort = do N.sClose sock -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing -- Request a new connection, but don't wait for the response let reqId = 0 :: Int32 @@ -287,14 +288,14 @@ testEarlyCloseSocket nextPort = do -- | Test the creation of a transport with an invalid address testInvalidAddress :: IO N.ServiceName -> IO () testInvalidAddress nextPort = do - Left _ <- nextPort >>= createTransport "invalidHostName" + Left _ <- nextPort >>= \port -> createTransport "invalidHostName" port defaultTCPParameters return () -- | Test connecting to invalid or non-existing endpoints testInvalidConnect :: IO N.ServiceName -> IO () testInvalidConnect nextPort = do port <- nextPort - Right transport <- createTransport "127.0.0.1" port + Right transport <- createTransport "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport -- Syntax error in the endpoint address @@ -322,7 +323,7 @@ testIgnoreCloseSocket :: IO N.ServiceName -> IO () testIgnoreCloseSocket nextPort = do serverAddr <- newEmptyMVar clientDone <- newEmptyMVar - Right transport <- nextPort >>= createTransport "127.0.0.1" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters forkTry $ server transport serverAddr forkTry $ client transport serverAddr clientDone @@ -360,7 +361,7 @@ testIgnoreCloseSocket nextPort = do let ourAddress = address endpoint -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing -- Request a new connection tlog "Requesting connection" @@ -405,7 +406,7 @@ testBlockAfterCloseSocket nextPort = do clientAddr <- newEmptyMVar clientDone <- newEmptyMVar port <- nextPort - Right transport <- createTransport "127.0.0.1" port + Right transport <- createTransport "127.0.0.1" port defaultTCPParameters forkTry $ server transport serverAddr clientAddr forkTry $ client transport serverAddr clientAddr clientDone @@ -440,7 +441,7 @@ testBlockAfterCloseSocket nextPort = do let ourAddress = address endpoint -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing -- Request a new connection tlog "Requesting connection" @@ -482,7 +483,7 @@ testUnnecessaryConnect nextPort numThreads = do serverAddr <- newEmptyMVar forkTry $ do - Right transport <- nextPort >>= createTransport "127.0.0.1" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) @@ -495,7 +496,7 @@ testUnnecessaryConnect nextPort numThreads = do dones <- replicateM numThreads $ do done <- newEmptyMVar forkTry $ do - Right (_, reply) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr defaultConnectHints + Right (_, reply) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing case reply of ConnectionRequestAccepted -> putMVar gotAccepted () @@ -516,11 +517,11 @@ testUnnecessaryConnect nextPort numThreads = do -- | Test that we can create "many" transport instances testMany :: IO N.ServiceName -> IO () testMany nextPort = do - Right masterTransport <- nextPort >>= createTransport "127.0.0.1" + Right masterTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters Right masterEndPoint <- newEndPoint masterTransport replicateM_ 10 $ do - mTransport <- nextPort >>= createTransport "127.0.0.1" + mTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters case mTransport of Left ex -> do putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex) @@ -537,7 +538,7 @@ testMany nextPort = do -- | Test what happens when the transport breaks completely testBreakTransport :: IO N.ServiceName -> IO () testBreakTransport nextPort = do - Right (transport, internals) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport killThread (transportThread internals) -- Uh oh @@ -567,7 +568,7 @@ testReconnect nextPort = do counter <- newMVar (0 :: Int) - forkServer "127.0.0.1" serverPort 5 throwIO $ \sock -> do + forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do -- Accept the connection Right 0 <- tryIO $ (recvInt32 sock :: IO Int) Right _ <- tryIO $ recvWithLength sock @@ -596,7 +597,7 @@ testReconnect nextPort = do -- Client forkTry $ do - Right transport <- nextPort >>= createTransport "127.0.0.1" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 @@ -642,7 +643,7 @@ testUnidirectionalError nextPort = do serverGotPing <- newEmptyMVar -- Server - forkServer "127.0.0.1" serverPort 5 throwIO $ \sock -> do + forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do -- We accept connections, but when an exception occurs we don't do -- anything (in particular, we don't close the socket). This is important -- because when we shutdown one direction of the socket a recv here will @@ -663,7 +664,7 @@ testUnidirectionalError nextPort = do -- Client forkTry $ do - Right (transport, internals) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters Right endpoint <- newEndPoint transport let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 @@ -718,7 +719,7 @@ testUnidirectionalError nextPort = do testInvalidCloseConnection :: IO N.ServiceName -> IO () testInvalidCloseConnection nextPort = do - Right (transport, internals) <- nextPort >>= createTransportExposeInternals "127.0.0.1" + Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters serverAddr <- newEmptyMVar clientDone <- newEmptyMVar serverDone <- newEmptyMVar @@ -773,7 +774,7 @@ main = do , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] -- Run the generic tests even if the TCP specific tests failed.. - testTransport (either (Left . show) (Right) <$> nextPort >>= createTransport "127.0.0.1") + testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) -- ..but if the generic tests pass, still fail if the specific tests did case tcpResult of Left err -> throwIO err From e4b69419829a315d9ad1c5a9389370335a800a90 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 4 Jun 2012 16:21:45 +0100 Subject: [PATCH 0099/2357] Start making the transport async-exception safe --- network-transport.cabal | 8 +-- src/Network/Transport/Internal.hs | 24 +++++++ src/Network/Transport/TCP.hs | 56 ++++++++------- tests/TestTCP.hs | 2 +- tests/TestTransport.hs | 113 ++++++++++++++++-------------- 5 files changed, 120 insertions(+), 83 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 03351899..7567e3f3 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -23,7 +23,7 @@ Library Network.Transport.Chan, Network.Transport.TCP, Network.Transport.Util - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src @@ -39,7 +39,7 @@ Test-Suite TestTCP transformers, ansi-terminal, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N HS-Source-Dirs: tests src @@ -54,7 +54,7 @@ Test-Suite TestMulticastInMemory transformers, ansi-terminal, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -69,6 +69,6 @@ Test-Suite TestInMemory transformers, ansi-terminal, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 610ac774..a655c457 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -10,6 +10,7 @@ module Network.Transport.Internal ( -- * Encoders/decoders , tryIO , tryToEnum , timeoutMaybe + , asyncWhenCancelled -- * Replicated functionality from "base" , void , forkIOWithUnmask @@ -28,13 +29,17 @@ import qualified Data.ByteString.Internal as BSI ( unsafeCreate , inlinePerformIO) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Exception ( IOException + , SomeException + , AsyncException , Exception , catch , try , throw , throwIO + , mask_ ) import Control.Concurrent (ThreadId, forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar) import GHC.IO (unsafeUnmask) import System.Timeout (timeout) --import Control.Concurrent (myThreadId) @@ -124,3 +129,22 @@ timeoutMaybe (Just n) e f = do case ma of Nothing -> throwIO e Just a -> return a + +-- | @asyncWhenCancelled g f@ runs f in a separate thread and waits for it +-- to complete. If f throws an exception we catch it and rethrow it in the +-- current thread. If the current thread is interrupted before f completes, +-- we run the specified clean up handler (if f throws an exception we assume +-- that no cleanup is necessary). +asyncWhenCancelled :: forall a. (a -> IO ()) -> IO a -> IO a +asyncWhenCancelled g f = mask_ $ do + mvar <- newEmptyMVar + forkIO $ try f >>= putMVar mvar + -- takeMVar is interruptible (even inside a mask_) + catch (takeMVar mvar) (exceptionHandler mvar) >>= either throwIO return + where + exceptionHandler :: MVar (Either SomeException a) + -> AsyncException + -> IO (Either SomeException a) + exceptionHandler mvar ex = do + forkIO $ takeMVar mvar >>= either (const $ return ()) g + throwIO ex diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 2a613cd6..7af25011 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -43,6 +43,7 @@ import Network.Transport.Internal ( encodeInt32 , tryToEnum , void , timeoutMaybe + , asyncWhenCancelled ) import qualified Network.Socket as N ( HostName , ServiceName @@ -525,36 +526,37 @@ apiConnect :: TCPParameters -- ^ Parameters apiConnect params ourEndPoint theirAddress _reliability hints = if localAddress ourEndPoint == theirAddress then connectToSelf ourEndPoint - else try $ do - resetIfBroken ourEndPoint theirAddress - (theirEndPoint, connId) <- - requestConnectionTo params ourEndPoint theirAddress hints - -- connAlive can be an IORef rather than an MVar because it is protected - -- by the remoteState MVar. We don't need the overhead of locking twice. - connAlive <- newIORef True - return Connection - { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive - , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive - } + else try . asyncWhenCancelled close $ do + resetIfBroken ourEndPoint theirAddress + (theirEndPoint, connId) <- + requestConnectionTo params ourEndPoint theirAddress hints + -- connAlive can be an IORef rather than an MVar because it is protected + -- by the remoteState MVar. We don't need the overhead of locking twice. + connAlive <- newIORef True + return Connection + { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive + , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive + } -- | Close a connection apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () -apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO $ do - modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity - { caseValid = \vst -> do - alive <- readIORef connAlive - if alive - then do - writeIORef connAlive False - sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] - return ( RemoteEndPointValid - . (remoteOutgoing ^: (\x -> x - 1)) - $ vst - ) - else - return (RemoteEndPointValid vst) - } - closeIfUnused (ourEndPoint, theirEndPoint) +apiClose (ourEndPoint, theirEndPoint) connId connAlive = + void . tryIO . asyncWhenCancelled return $ do + modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity + { caseValid = \vst -> do + alive <- readIORef connAlive + if alive + then do + writeIORef connAlive False + sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] + return ( RemoteEndPointValid + . (remoteOutgoing ^: (\x -> x - 1)) + $ vst + ) + else + return (RemoteEndPointValid vst) + } + closeIfUnused (ourEndPoint, theirEndPoint) -- | Send data across a connection apiSend :: EndPointPair -- ^ Local and remote endpoint diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index df115b38..c066d704 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -775,7 +775,7 @@ main = do ] -- Run the generic tests even if the TCP specific tests failed.. testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) - -- ..but if the generic tests pass, still fail if the specific tests did + -- ..but if the generic tests pass, still fail if the specific tests did not case tcpResult of Left err -> throwIO err Right () -> return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 35557f53..1c3f9ac6 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -4,12 +4,13 @@ module TestTransport where import Prelude hiding (catch, (>>=), (>>), return, fail) import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) import Control.Concurrent (forkIO, killThread, yield) -import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar) -import Control.Exception (evaluate, throw, throwIO) -import Control.Monad (replicateM, replicateM_, when, guard, forM, forM_, unless) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) +import Control.Exception (evaluate, throw, throwIO, bracket) +import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) import Control.Monad.Error () +import Control.Applicative ((<$>)) import Network.Transport -import Network.Transport.Internal (tlog) +import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) import Network.Transport.Util (spawn) import System.Random (randomIO) import Data.ByteString (ByteString) @@ -271,30 +272,37 @@ testCloseOneDirection transport numPings = do mapM_ takeMVar [doneA, doneB] --- | Collect a given number of events and order them by connection ID -collect :: EndPoint -> Int -> IO [(ConnectionId, [[ByteString]])] -collect endPoint numEvents = go numEvents Map.empty Map.empty +-- | Collect events and order them by connection ID +collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] +collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty where -- TODO: for more serious use of this function we'd need to make these arguments strict - go 0 open closed = if Map.null open - then return . Map.toList . Map.map reverse $ closed - else fail "Open connections" + go (Just 0) open closed = finish open closed go n open closed = do - event <- receive endPoint - case event of - ConnectionOpened cid _ _ -> - go (n - 1) (Map.insert cid [] open) closed - ConnectionClosed cid -> - let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in - go (n - 1) (Map.delete cid open) (Map.insert cid list closed) - Received cid msg -> - go (n - 1) (Map.adjust (msg :) cid open) closed - ReceivedMulticast _ _ -> - fail "Unexpected multicast" - ErrorEvent _ -> - fail "Unexpected error" - EndPointClosed -> - fail "Unexpected endpoint closure" + mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint + case mEvent of + Left _ -> finish open closed + Right event -> do + let n' = (\x -> x - 1) <$> n + case event of + ConnectionOpened cid _ _ -> + go n' (Map.insert cid [] open) closed + ConnectionClosed cid -> + let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in + go n' (Map.delete cid open) (Map.insert cid list closed) + Received cid msg -> + go n' (Map.adjust (msg :) cid open) closed + ReceivedMulticast _ _ -> + fail "Unexpected multicast" + ErrorEvent _ -> + fail "Unexpected error" + EndPointClosed -> + fail "Unexpected endpoint closure" + + finish open closed = + if Map.null open + then return . Map.toList . Map.map reverse $ closed + else fail "Open connections" -- | Open connection, close it, then reopen it -- (In the TCP transport this means the socket will be closed, then reopened) @@ -342,7 +350,7 @@ testCloseReopen transport numPings = do Right endpoint <- newEndPoint transport putMVar addrB (address endpoint) - eventss <- collect endpoint (numRepeats * (numPings + 2)) + eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do forM_ (zip [1 .. numPings] events) $ \(j, event) -> do @@ -368,7 +376,7 @@ testParallelConnects transport numPings = do close conn forkTry $ do - eventss <- collect endpoint (numPings * 4) + eventss <- collect endpoint (Just (numPings * 4)) Nothing -- Check that no pings got sent to the wrong connection forM_ eventss $ \(_, [[ping1], [ping2]]) -> guard (ping1 == ping2) @@ -519,7 +527,7 @@ testConnectToSelfTwice transport numPings = do forkTry $ do tlog $ "reading" - [(_, events1), (_, events2)] <- collect endpoint (2 * (numPings + 2)) + [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing True <- return $ events1 == replicate numPings ["pingA"] True <- return $ events2 == replicate numPings ["pingB"] @@ -826,31 +834,34 @@ testKill newTransport numThreads = do Right endpoint1 <- newEndPoint transport1 Right endpoint2 <- newEndPoint transport2 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - ConnectionOpened _ _ _ <- receive endpoint2 - threads <- replicateM numThreads . forkIO $ do - randomThreadDelay 10 - Right () <- send conn ["ping"] - return () - - -- Kill half of those threads, and wait on the rest - killerDone <- newEmptyMVar - forkIO $ do - killed <- forM threads $ \tid -> do - shouldKill <- randomIO - when shouldKill $ randomThreadDelay 10 >> killThread tid - return shouldKill - - -- We should receive at least the pings from the threads that we didn't - -- kill (we might get more, depending on when exactly the kill happens) - forM_ killed $ \wasKilled -> unless wasKilled $ do - Received _ ["ping"] <- receive endpoint2 - return () - - putMVar killerDone () + randomThreadDelay 100 + bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) + (\(Right conn) -> randomThreadDelay 100 >> close conn) + (\(Right conn) -> randomThreadDelay 100 >> do Right () <- send conn ["ping"] ; return ()) + + numAlive <- newMVar (0 :: Int) + + -- Kill half of those threads + forkIO . forM_ threads $ \tid -> do + shouldKill <- randomIO + if shouldKill + then randomThreadDelay 600 >> killThread tid + else modifyMVar_ numAlive (return . (+ 1)) + + -- Since it is impossible to predict when the kill exactly happens, we don't + -- know how many connects were opened and how many pings were sent. But we + -- should not have any open connections (if we do, collect will throw an + -- error) and we should have at least the number of pings equal to the number + -- of threads we did *not* kill + eventss <- collect endpoint2 Nothing (Just 1000000) + let actualPings = sum . map (length . snd) $ eventss + expectedPings <- takeMVar numAlive + unless (actualPings >= expectedPings) $ + throwIO (userError "Missing pings") + +-- print (actualPings, expectedPings) - takeMVar killerDone -- | Set up conditions with a high likelyhood of "crossing" (for transports -- that multiplex lightweight connections across heavyweight connections) From bb40915805b838528bc3732289984ae0e5f2ea10 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 4 Jun 2012 17:31:08 +0100 Subject: [PATCH 0100/2357] Simplify some of the code using non-interruptable assumptions --- src/Network/Transport/TCP.hs | 240 +++++++++++++---------------------- tests/TestTransport.hs | 13 +- tests/Traced.hs | 2 +- 3 files changed, 99 insertions(+), 156 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 7af25011..106b6d91 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -72,7 +72,6 @@ import Control.Concurrent.MVar ( MVar , putMVar , newEmptyMVar , withMVar - , tryPutMVar ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) @@ -85,7 +84,6 @@ import Control.Exception ( IOException , try , bracketOnError , mask - , mask_ , onException , fromException ) @@ -247,7 +245,6 @@ data LocalEndPointState = data ValidLocalEndPointState = ValidLocalEndPointState { _nextConnectionId :: !ConnectionId , _localConnections :: Map EndPointAddress RemoteEndPoint - , _internalThreads :: [ThreadId] , _nextRemoteId :: !Int } @@ -529,7 +526,7 @@ apiConnect params ourEndPoint theirAddress _reliability hints = else try . asyncWhenCancelled close $ do resetIfBroken ourEndPoint theirAddress (theirEndPoint, connId) <- - requestConnectionTo params ourEndPoint theirAddress hints + uRequestConnectionTo params ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True @@ -611,9 +608,7 @@ apiCloseEndPoint transport evs ourEndPoint = do LocalEndPointClosed -> return (LocalEndPointClosed, Nothing) forM_ mOurState $ \vst -> do - -- Close all endpoints and kill all threads forM_ (vst ^. localConnections) tryCloseRemoteSocket - forM_ (vst ^. internalThreads) killThread forM_ evs $ writeChan (localChannel ourEndPoint) where -- Close the remote socket and return the set of all incoming connections @@ -762,7 +757,6 @@ createLocalEndPoint transport = do state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState { _nextConnectionId = firstNonReservedConnectionId , _localConnections = Map.empty - , _internalThreads = [] , _nextRemoteId = 0 } modifyMVar (transportState transport) $ \st -> case st of @@ -784,104 +778,6 @@ createLocalEndPoint transport = do TransportClosed -> throwIO (TransportError NewEndPointFailed "Transport closed") --- | Request a connection to a remote endpoint --- --- This will block until we get a connection ID from the remote endpoint; if --- the remote endpoint was in 'RemoteEndPointClosing' state then we will --- additionally block until that is resolved. --- --- May throw a TransportError ConnectErrorCode exception. -requestConnectionTo :: TCPParameters - -> LocalEndPoint - -> EndPointAddress - -> ConnectHints - -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo params ourEndPoint theirAddress hints = go - where - go = do - (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress RequestedByUs - - if isNew - then do - void . forkEndPointThread ourEndPoint . handle absorbAllExceptions $ - setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints - go - else do - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - return (theirEndPoint, decodeInt32 . BS.concat $ reply) - - connectFailed = TransportError ConnectFailed . show - - absorbAllExceptions :: SomeException -> IO () - absorbAllExceptions _ex = - return () - --- | Set up a remote endpoint -setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () -setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do - didAccept <- bracketOnError (socketToEndPoint - ourAddress - theirAddress - (tcpReuseClientAddr params) - (connectTimeout hints)) - onError $ \result -> case result of - Right (sock, ConnectionRequestAccepted) -> do - let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 - } - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) - return True - Right (sock, ConnectionRequestInvalid) -> do - let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - tryCloseSocket sock - return False - Right (sock, ConnectionRequestCrossed) -> do - resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed - tryCloseSocket sock - return False - Left err -> do - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - return False - - -- If we get to this point without an exception, then - -- - -- if didAccept is False the socket has already been closed - -- if didAccept is True, the socket has been stored as part of the remote - -- state so we no longer need to worry about closing it when an - -- asynchronous exception occurs - when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) - where - -- If an asynchronous exception occurs while we set up the remote endpoint - -- we need to make sure to close the socket. It is also useful to - -- initialize the remote state (to "invalid") so that concurrent threads - -- that are blocked on reading the remote state are unblocked. It is - -- possible, however, that the exception occurred after we already - -- initialized the remote state, which is why we use tryPutMVar here. - onError :: Either (TransportError ConnectErrorCode) - (N.Socket, ConnectionRequestResponse) - -> IO () - onError result = - case result of - Left err -> - void $ tryPutMVar theirState (RemoteEndPointInvalid err) - Right (sock, _) -> do - let err = failed "setupRemoteEndPoint failed" - tryPutMVar theirState (RemoteEndPointInvalid err) - tryCloseSocket sock - - failed = TransportError ConnectFailed - ourAddress = localAddress ourEndPoint - theirAddress = remoteAddress theirEndPoint - theirState = remoteState theirEndPoint - invalidAddress = TransportError ConnectNotFound -- | Resolve an endpoint currently in 'Init' state resolveInit :: EndPointPair -> RemoteState -> IO () @@ -1043,48 +939,6 @@ closeIfUnused (ourEndPoint, theirEndPoint) = return $ RemoteEndPointValid vst } --- | Fork a new thread and store its ID as part of the transport state --- --- If the local end point is closed this function does nothing (no thread is --- spawned). Returns whether or not a thread was spawned. -forkEndPointThread :: LocalEndPoint -> IO () -> IO Bool -forkEndPointThread ourEndPoint p = - -- We use an explicit mask_ because we don't want to be interrupted until - -- we have registered the thread. In particular, modifyMVar is not good - -- enough because if we get an asynchronous exception after the fork but - -- before the argument to modifyMVar returns we don't want to simply put - -- the old value of the mvar back. - mask_ $ do - st <- takeMVar ourState - case st of - LocalEndPointValid vst -> do - threadRegistered <- newEmptyMVar - tid <- forkIO (takeMVar threadRegistered >> p >> removeThread) - putMVar ourState ( LocalEndPointValid - . (internalThreads ^: (tid :)) - $ vst - ) - putMVar threadRegistered () - return True - LocalEndPointClosed -> do - putMVar ourState st - return False - where - removeThread :: IO () - removeThread = do - tid <- myThreadId - modifyMVar_ ourState $ \st -> case st of - LocalEndPointValid vst -> - return ( LocalEndPointValid - . (internalThreads ^: filter (/= tid)) - $ vst - ) - LocalEndPointClosed -> - return LocalEndPointClosed - - ourState :: MVar LocalEndPointState - ourState = localState ourEndPoint - -------------------------------------------------------------------------------- -- As soon as a remote connection fails, we want to put notify our endpoint -- -- and put it into a closed state. Since this may happen in many places, we -- @@ -1248,7 +1102,7 @@ handleConnectionRequest transport sock = handle handleException $ do return ourEndPoint TransportClosed -> throwIO $ userError "Transport closed" - void . forkEndPointThread ourEndPoint $ go ourEndPoint theirAddress + void . forkIO $ go ourEndPoint theirAddress where go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do @@ -1626,6 +1480,93 @@ getNextConnectionId ourEndpoint = LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" +-------------------------------------------------------------------------------- +-- Uninterruptable auxiliary functions -- +-- -- +-- All these functions assume they are running in a thread which will never -- +-- be killed, and are designed to be used in conjunction with -- +-- 'asyncWhenCancelled'. To be explicit about this assumption the function -- +-- names are prefixed with 'u'. -- +-------------------------------------------------------------------------------- + +-- | Request a connection to a remote endpoint +-- +-- This will block until we get a connection ID from the remote endpoint; if +-- the remote endpoint was in 'RemoteEndPointClosing' state then we will +-- additionally block until that is resolved. +-- +-- May throw a TransportError ConnectErrorCode exception. +uRequestConnectionTo :: TCPParameters + -> LocalEndPoint + -> EndPointAddress + -> ConnectHints + -> IO (RemoteEndPoint, ConnectionId) +uRequestConnectionTo params ourEndPoint theirAddress hints = go + where + go = do + (theirEndPoint, isNew) <- mapIOException connectFailed $ + findRemoteEndPoint ourEndPoint theirAddress RequestedByUs + + if isNew + then do + forkIO . handle absorbAllExceptions $ + uSetupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints + go + else do + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + return (theirEndPoint, decodeInt32 . BS.concat $ reply) + + connectFailed = TransportError ConnectFailed . show + + absorbAllExceptions :: SomeException -> IO () + absorbAllExceptions _ex = + return () + +-- | Set up a remote endpoint +uSetupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () +uSetupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do + result <- socketToEndPoint ourAddress + theirAddress + (tcpReuseClientAddr params) + (connectTimeout hints) + didAccept <- case result of + Right (sock, ConnectionRequestAccepted) -> do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 + } + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) + return True + Right (sock, ConnectionRequestInvalid) -> do + let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + tryCloseSocket sock + return False + Right (sock, ConnectionRequestCrossed) -> do + resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed + tryCloseSocket sock + return False + Left err -> do + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + return False + + -- If we get to this point without an exception, then + -- + -- if didAccept is False the socket has already been closed + -- if didAccept is True, the socket has been stored as part of the remote + -- state so we no longer need to worry about closing it when an + -- asynchronous exception occurs + when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) + where + ourAddress = localAddress ourEndPoint + theirAddress = remoteAddress theirEndPoint + invalidAddress = TransportError ConnectNotFound + -------------------------------------------------------------------------------- -- Constants -- -------------------------------------------------------------------------------- @@ -1650,9 +1591,6 @@ nextConnectionId = accessor _nextConnectionId (\cix st -> st { _nextConnectionId localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) -internalThreads :: Accessor ValidLocalEndPointState [ThreadId] -internalThreads = accessor _internalThreads (\ts st -> st { _internalThreads = ts }) - nextRemoteId :: Accessor ValidLocalEndPointState Int nextRemoteId = accessor _nextRemoteId (\rid st -> st { _nextRemoteId = rid }) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 1c3f9ac6..43d49fc4 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -302,7 +302,7 @@ collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty finish open closed = if Map.null open then return . Map.toList . Map.map reverse $ closed - else fail "Open connections" + else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) -- | Open connection, close it, then reopen it -- (In the TCP transport this means the socket will be closed, then reopened) @@ -837,8 +837,13 @@ testKill newTransport numThreads = do threads <- replicateM numThreads . forkIO $ do randomThreadDelay 100 bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) - (\(Right conn) -> randomThreadDelay 100 >> close conn) - (\(Right conn) -> randomThreadDelay 100 >> do Right () <- send conn ["ping"] ; return ()) + -- Note that we should not insert a randomThreadDelay into the + -- exception handler itself as this means that the exception handler + -- could be interrupted and we might not close + (\(Right conn) -> close conn) + (\(Right conn) -> do randomThreadDelay 100 + Right () <- send conn ["ping"] + randomThreadDelay 100) numAlive <- newMVar (0 :: Int) @@ -935,7 +940,7 @@ testTransport newTransport = do , ("CloseReopen", testCloseReopen transport numPings) , ("ParallelConnects", testParallelConnects transport numPings) , ("SendAfterClose", testSendAfterClose transport 1000) - , ("Crossing", testCrossing transport 1000) + , ("Crossing", testCrossing transport 100) , ("CloseTwice", testCloseTwice transport 100) , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) diff --git a/tests/Traced.hs b/tests/Traced.hs index b2d6fc09..a7735efa 100644 --- a/tests/Traced.hs +++ b/tests/Traced.hs @@ -176,7 +176,7 @@ instance MonadS IO where instance Show TracedException where show (TracedException ts ex) = - show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (reverse ts))) + show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) traceHandlers :: Traceable a => a -> [Handler b] traceHandlers a = case trace a of From 6f17a517f0d2674312061109fd6b9ad1b98c4a2e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 5 Jun 2012 13:38:52 +0100 Subject: [PATCH 0101/2357] Code cleanup --- src/Network/Transport/Internal/TCP.hs | 6 + src/Network/Transport/TCP.hs | 1322 +++++++++++++------------ 2 files changed, 670 insertions(+), 658 deletions(-) diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs index 0133b154..851564e7 100644 --- a/src/Network/Transport/Internal/TCP.hs +++ b/src/Network/Transport/Internal/TCP.hs @@ -68,6 +68,12 @@ forkServer host port backlog reuseAddr terminationHandler requestHandler = do when reuseAddr $ N.setSocketOption sock N.ReuseAddr 1 N.bindSocket sock (N.addrAddress addr) N.listen sock backlog + -- We start listening for incoming requests in a separate thread. When + -- that thread is killed, we close the server socket and the termination + -- handler. We have to make sure that the exception handler is installed + -- /before/ any asynchronous exception occurs. So we mask_, then fork + -- (the child thread inherits the masked state from the parent), then + -- unmask only inside the catch. mask_ $ forkIOWithUnmask $ \unmask -> catch (unmask (forever $ acceptRequest sock)) $ \ex -> do tryCloseSocket sock diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 106b6d91..e4c9f9ec 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -78,6 +78,7 @@ import Control.Applicative ((<$>)) import Control.Monad (when, unless) import Control.Exception ( IOException , SomeException + , AsyncException , handle , throw , throwIO @@ -203,7 +204,7 @@ import Data.Foldable (forM_, mapM_) -- Internal datatypes -- -------------------------------------------------------------------------------- --- We use underscores for fields that we might update (using accessores) +-- We use underscores for fields that we might update (using accessors) -- -- All data types follow the same structure: -- @@ -254,9 +255,11 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- -- Init ---+---> Invalid -- | --- | /--------------\ --- | | | --- | v | +-- +---------------------------------------\ +-- | | +-- | /--------------\ | +-- | | | | +-- | v | v -- +---> Valid ---+---> Closing ---+---> Closed -- | | | | -- | | | v @@ -282,6 +285,11 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- CloseSocket) or be put back in 'Valid' state if the remote endpoint denies -- the request. -- +-- We also put the endpoint in Closed state, directly from Init, if we our +-- outbound connection request crossed an inbound connection request and we +-- decide to keep the inbound (i.e., the remote endpoint sent us a +-- ConnectionRequestCrossed message). +-- -- Closed: The endpoint is put in Closed state after a successful garbage -- collection. -- @@ -481,32 +489,34 @@ defaultTCPParameters = TCPParameters { -- | Close the transport apiCloseTransport :: TCPTransport -> Maybe ThreadId -> [Event] -> IO () -apiCloseTransport transport mTransportThread evs = do - mTSt <- modifyMVar (transportState transport) $ \st -> case st of - TransportValid vst -> return (TransportClosed, Just vst) - TransportClosed -> return (TransportClosed, Nothing) - forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) - -- This will invoke the termination handler, which in turn will call - -- apiCloseTransport again, but then the transport will already be closed and - -- we won't be passed a transport thread, so we terminate immmediate - forM_ mTransportThread killThread +apiCloseTransport transport mTransportThread evs = + asyncWhenCancelled return $ do + mTSt <- modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> return (TransportClosed, Just vst) + TransportClosed -> return (TransportClosed, Nothing) + forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) + -- This will invoke the termination handler, which in turn will call + -- apiCloseTransport again, but then the transport will already be closed + -- and we won't be passed a transport thread, so we terminate immmediate + forM_ mTransportThread killThread -- | Create a new endpoint apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) -apiNewEndPoint transport = try $ do - ourEndPoint <- createLocalEndPoint transport - return EndPoint - { receive = readChan (localChannel ourEndPoint) - , address = localAddress ourEndPoint - , connect = apiConnect (transportParams transport) ourEndPoint - , closeEndPoint = let evs = [ EndPointClosed - , throw $ userError "Endpoint closed" - ] in - apiCloseEndPoint transport evs ourEndPoint - , newMulticastGroup = return . Left $ newMulticastGroupError - , resolveMulticastGroup = return . Left . const resolveMulticastGroupError - } +apiNewEndPoint transport = + try . asyncWhenCancelled closeEndPoint $ do + ourEndPoint <- createLocalEndPoint transport + return EndPoint + { receive = readChan (localChannel ourEndPoint) + , address = localAddress ourEndPoint + , connect = apiConnect (transportParams transport) ourEndPoint + , closeEndPoint = let evs = [ EndPointClosed + , throw $ userError "Endpoint closed" + ] in + apiCloseEndPoint transport evs ourEndPoint + , newMulticastGroup = return . Left $ newMulticastGroupError + , resolveMulticastGroup = return . Left . const resolveMulticastGroupError + } where newMulticastGroupError = TransportError NewMulticastGroupUnsupported "Multicast not supported" @@ -521,12 +531,13 @@ apiConnect :: TCPParameters -- ^ Parameters -> ConnectHints -- ^ Hints -> IO (Either (TransportError ConnectErrorCode) Connection) apiConnect params ourEndPoint theirAddress _reliability hints = - if localAddress ourEndPoint == theirAddress - then connectToSelf ourEndPoint - else try . asyncWhenCancelled close $ do + try . asyncWhenCancelled close $ + if localAddress ourEndPoint == theirAddress + then connectToSelf ourEndPoint + else do resetIfBroken ourEndPoint theirAddress (theirEndPoint, connId) <- - uRequestConnectionTo params ourEndPoint theirAddress hints + requestConnectionTo params ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True @@ -562,6 +573,7 @@ apiSend :: EndPointPair -- ^ Local and remote endpoint -> [ByteString] -- ^ Payload -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = + -- We don't need the overhead of asyncWhenCancelled here try . mapIOException sendFailed $ withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch { caseInvalid = \_ -> @@ -597,19 +609,20 @@ apiCloseEndPoint :: TCPTransport -- ^ Transport -> [Event] -- ^ Events used to report closure -> LocalEndPoint -- ^ Local endpoint -> IO () -apiCloseEndPoint transport evs ourEndPoint = do - -- Remove the reference from the transport state - removeLocalEndPoint transport ourEndPoint - -- Close the local endpoint - mOurState <- modifyMVar (localState ourEndPoint) $ \st -> - case st of - LocalEndPointValid vst -> - return (LocalEndPointClosed, Just vst) - LocalEndPointClosed -> - return (LocalEndPointClosed, Nothing) - forM_ mOurState $ \vst -> do - forM_ (vst ^. localConnections) tryCloseRemoteSocket - forM_ evs $ writeChan (localChannel ourEndPoint) +apiCloseEndPoint transport evs ourEndPoint = + asyncWhenCancelled return $ do + -- Remove the reference from the transport state + removeLocalEndPoint transport ourEndPoint + -- Close the local endpoint + mOurState <- modifyMVar (localState ourEndPoint) $ \st -> + case st of + LocalEndPointValid vst -> + return (LocalEndPointClosed, Just vst) + LocalEndPointClosed -> + return (LocalEndPointClosed, Nothing) + forM_ mOurState $ \vst -> do + forM_ (vst ^. localConnections) tryCloseRemoteSocket + forM_ evs $ writeChan (localChannel ourEndPoint) where -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO () @@ -637,445 +650,143 @@ apiCloseEndPoint transport evs ourEndPoint = do RemoteEndPointFailed err -> return $ RemoteEndPointFailed err --- | Special case of 'apiConnect': connect an endpoint to itself -connectToSelf :: LocalEndPoint - -> IO (Either (TransportError ConnectErrorCode) Connection) -connectToSelf ourEndPoint = try . mapIOException connectFailed $ do - connAlive <- newIORef True -- Protected by the local endpoint lock - connId <- getNextConnectionId ourEndPoint - writeChan ourChan $ - ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) - return Connection - { send = selfSend connAlive connId - , close = selfClose connAlive connId - } - where - selfSend :: IORef Bool - -> ConnectionId - -> [ByteString] - -> IO (Either (TransportError SendErrorCode) ()) - selfSend connAlive connId msg = - try . withMVar ourState $ \st -> case st of - LocalEndPointValid _ -> do - alive <- readIORef connAlive - if alive - then writeChan ourChan (Received connId msg) - else throwIO $ TransportError SendClosed "Connection closed" - LocalEndPointClosed -> - throwIO $ TransportError SendFailed "Endpoint closed" +-------------------------------------------------------------------------------- +-- As soon as a remote connection fails, we want to put notify our endpoint -- +-- and put it into a closed state. Since this may happen in many places, we -- +-- provide some abstractions. -- +-------------------------------------------------------------------------------- - selfClose :: IORef Bool -> ConnectionId -> IO () - selfClose connAlive connId = - withMVar ourState $ \st -> case st of - LocalEndPointValid _ -> do - alive <- readIORef connAlive - when alive $ do - writeChan ourChan (ConnectionClosed connId) - writeIORef connAlive False - LocalEndPointClosed -> - return () +data RemoteStatePatternMatch a = RemoteStatePatternMatch + { caseInvalid :: TransportError ConnectErrorCode -> IO a + , caseInit :: MVar () -> RequestedBy -> IO a + , caseValid :: ValidRemoteEndPointState -> IO a + , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a + , caseClosed :: IO a + , caseFailed :: IOException -> IO a + } - ourChan = localChannel ourEndPoint - ourState = localState ourEndPoint - connectFailed = TransportError ConnectFailed . show +remoteStateIdentity :: RemoteStatePatternMatch RemoteState +remoteStateIdentity = + RemoteStatePatternMatch + { caseInvalid = return . RemoteEndPointInvalid + , caseInit = (return .) . RemoteEndPointInit + , caseValid = return . RemoteEndPointValid + , caseClosing = (return .) . RemoteEndPointClosing + , caseClosed = return RemoteEndPointClosed + , caseFailed = return . RemoteEndPointFailed + } --------------------------------------------------------------------------------- --- Functions from TransportInternals -- --------------------------------------------------------------------------------- +-- | Like modifyMVar, but if an I/O exception occurs don't restore the remote +-- endpoint to its original value but close it instead +modifyRemoteState :: EndPointPair + -> RemoteStatePatternMatch (RemoteState, a) + -> IO a +modifyRemoteState (ourEndPoint, theirEndPoint) match = + mask $ \restore -> do + st <- takeMVar theirState + case st of + RemoteEndPointValid vst -> do + mResult <- try $ restore (caseValid match vst) + case mResult of + Right (st', a) -> do + putMVar theirState st' + return a + Left ex -> do + case fromException ex of + Just ioEx -> handleIOException ioEx vst + Nothing -> putMVar theirState st + throwIO ex + -- The other cases are less interesting, because unless the endpoint is + -- in Valid state we're not supposed to do any IO on it + RemoteEndPointInit resolved origin -> do + (st', a) <- onException (restore $ caseInit match resolved origin) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointClosing resolved vst -> do + (st', a) <- onException (restore $ caseClosing match resolved vst) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointInvalid err -> do + (st', a) <- onException (restore $ caseInvalid match err) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointClosed -> do + (st', a) <- onException (restore $ caseClosed match) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointFailed err -> do + (st', a) <- onException (restore $ caseFailed match err) + (putMVar theirState st) + putMVar theirState st' + return a + where + theirState :: MVar RemoteState + theirState = remoteState theirEndPoint --- Find a socket between two endpoints --- --- Throws an IO exception if the socket could not be found. -internalSocketBetween :: TCPTransport -- ^ Transport - -> EndPointAddress -- ^ Local endpoint - -> EndPointAddress -- ^ Remote endpoint - -> IO N.Socket -internalSocketBetween transport ourAddress theirAddress = do - ourEndPoint <- withMVar (transportState transport) $ \st -> case st of - TransportClosed -> - throwIO $ userError "Transport closed" - TransportValid vst -> - case vst ^. localEndPointAt ourAddress of - Nothing -> throwIO $ userError "Local endpoint not found" - Just ep -> return ep - theirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Nothing -> throwIO $ userError "Remote endpoint not found" - Just ep -> return ep - withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit _ _ -> - throwIO $ userError "Remote endpoint not yet initialized" - RemoteEndPointValid vst -> - return $ remoteSocket vst - RemoteEndPointClosing _ vst -> - return $ remoteSocket vst - RemoteEndPointClosed -> - throwIO $ userError "Remote endpoint closed" - RemoteEndPointInvalid err -> - throwIO err - RemoteEndPointFailed err -> - throwIO err + handleIOException :: IOException -> ValidRemoteEndPointState -> IO () + handleIOException ex vst = do + tryCloseSocket (remoteSocket vst) + putMVar theirState (RemoteEndPointFailed ex) + let incoming = IntSet.elems $ vst ^. remoteIncoming + code = EventConnectionLost (remoteAddress theirEndPoint) incoming + err = TransportError code (show ex) + writeChan (localChannel ourEndPoint) $ ErrorEvent err + +-- | Like 'modifyRemoteState' but without a return value +modifyRemoteState_ :: EndPointPair + -> RemoteStatePatternMatch RemoteState + -> IO () +modifyRemoteState_ (ourEndPoint, theirEndPoint) match = + modifyRemoteState (ourEndPoint, theirEndPoint) + RemoteStatePatternMatch + { caseInvalid = u . caseInvalid match + , caseInit = \resolved origin -> u $ caseInit match resolved origin + , caseValid = u . caseValid match + , caseClosing = \resolved vst -> u $ caseClosing match resolved vst + , caseClosed = u $ caseClosed match + , caseFailed = u . caseFailed match + } + where + u :: IO a -> IO (a, ()) + u p = p >>= \a -> return (a, ()) + +-- | Like 'modifyRemoteState' but without the ability to change the state +withRemoteState :: EndPointPair + -> RemoteStatePatternMatch a + -> IO a +withRemoteState (ourEndPoint, theirEndPoint) match = + modifyRemoteState (ourEndPoint, theirEndPoint) + RemoteStatePatternMatch + { caseInvalid = \err -> do + a <- caseInvalid match err + return (RemoteEndPointInvalid err, a) + , caseInit = \resolved origin -> do + a <- caseInit match resolved origin + return (RemoteEndPointInit resolved origin, a) + , caseValid = \vst -> do + a <- caseValid match vst + return (RemoteEndPointValid vst, a) + , caseClosing = \resolved vst -> do + a <- caseClosing match resolved vst + return (RemoteEndPointClosing resolved vst, a) + , caseClosed = do + a <- caseClosed match + return (RemoteEndPointClosed, a) + , caseFailed = \err -> do + a <- caseFailed match err + return (RemoteEndPointFailed err, a) + } -------------------------------------------------------------------------------- --- Lower level functionality -- +-- Incoming requests -- -------------------------------------------------------------------------------- --- | Reset a remote endpoint if it is in Invalid mode --- --- If a user calls the API function 'connect' and the remote endpoint is --- currently in broken state, we remove the remote endpoint first because a --- new attempt to connect might succeed even if the previous one failed. --- --- Throws a TransportError ConnectFailed exception if the local endpoint is --- closed. -resetIfBroken :: LocalEndPoint -> EndPointAddress -> IO () -resetIfBroken ourEndPoint theirAddress = do - mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> - return (vst ^. localConnectionTo theirAddress) - LocalEndPointClosed -> - throwIO $ TransportError ConnectFailed "Endpoint closed" - forM_ mTheirEndPoint $ \theirEndPoint -> - withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInvalid _ -> - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - RemoteEndPointFailed _ -> - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - _ -> - return () - --- | Create a new local endpoint --- --- May throw a TransportError NewEndPointErrorCode exception if the transport --- is closed. -createLocalEndPoint :: TCPTransport -> IO LocalEndPoint -createLocalEndPoint transport = do - chan <- newChan - state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState - { _nextConnectionId = firstNonReservedConnectionId - , _localConnections = Map.empty - , _nextRemoteId = 0 - } - modifyMVar (transportState transport) $ \st -> case st of - TransportValid vst -> do - let ix = vst ^. nextEndPointId - let addr = encodeEndPointAddress (transportHost transport) - (transportPort transport) - ix - let localEndPoint = LocalEndPoint { localAddress = addr - , localChannel = chan - , localState = state - } - return ( TransportValid - . (localEndPointAt addr ^= Just localEndPoint) - . (nextEndPointId ^= ix + 1) - $ vst - , localEndPoint - ) - TransportClosed -> - throwIO (TransportError NewEndPointFailed "Transport closed") - - --- | Resolve an endpoint currently in 'Init' state -resolveInit :: EndPointPair -> RemoteState -> IO () -resolveInit (ourEndPoint, theirEndPoint) newState = - modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit resolved _ -> do - putMVar resolved () - case newState of - RemoteEndPointClosed -> - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - _ -> - return () - return newState - RemoteEndPointFailed ex -> - throwIO ex - _ -> - relyViolation (ourEndPoint, theirEndPoint) "resolveInit" - --- | Establish a connection to a remote endpoint --- --- Maybe throw a TransportError -socketToEndPoint :: EndPointAddress -- ^ Our address - -> EndPointAddress -- ^ Their address - -> Bool -- ^ Use SO_REUSEADDR? - -> Maybe Int -- ^ Timeout for connect - -> IO (Either (TransportError ConnectErrorCode) - (N.Socket, ConnectionRequestResponse)) -socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = - try $ do - (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of - Nothing -> throwIO (failed . userError $ "Could not parse") - Just dec -> return dec - addr:_ <- mapIOException invalidAddress $ - N.getAddrInfo Nothing (Just host) (Just port) - bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do - when reuseAddr $ - mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 - mapIOException invalidAddress $ - timeoutMaybe timeout timeoutError $ - N.connect sock (N.addrAddress addr) - response <- mapIOException failed $ do - sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) - recvInt32 sock - case tryToEnum response of - Nothing -> throwIO (failed . userError $ "Unexpected response") - Just r -> return (sock, r) - where - createSocket :: N.AddrInfo -> IO N.Socket - createSocket addr = mapIOException insufficientResources $ - N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - - invalidAddress = TransportError ConnectNotFound . show - insufficientResources = TransportError ConnectInsufficientResources . show - failed = TransportError ConnectFailed . show - timeoutError = TransportError ConnectTimeout "Timed out" - --- | Remove reference to a remote endpoint from a local endpoint --- --- If the local endpoint is closed, do nothing -removeRemoteEndPoint :: EndPointPair -> IO () -removeRemoteEndPoint (ourEndPoint, theirEndPoint) = - modifyMVar_ ourState $ \st -> case st of - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Nothing -> - return st - Just remoteEndPoint' -> - if remoteId remoteEndPoint' == remoteId theirEndPoint - then return - ( LocalEndPointValid - . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) - $ vst - ) - else return st - LocalEndPointClosed -> - return LocalEndPointClosed - where - ourState = localState ourEndPoint - theirAddress = remoteAddress theirEndPoint - --- | Remove reference to a local endpoint from the transport state --- --- Does nothing if the transport is closed -removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () -removeLocalEndPoint transport ourEndPoint = - modifyMVar_ (transportState transport) $ \st -> case st of - TransportValid vst -> - return ( TransportValid - . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) - $ vst - ) - TransportClosed -> - return TransportClosed - --- | Encode end point address -encodeEndPointAddress :: N.HostName - -> N.ServiceName - -> EndPointId - -> EndPointAddress -encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ - host ++ ":" ++ port ++ ":" ++ show ix - --- | Decode end point address -decodeEndPointAddress :: EndPointAddress - -> Maybe (N.HostName, N.ServiceName, EndPointId) -decodeEndPointAddress (EndPointAddress bs) = - case map BSC.unpack $ BSC.split ':' bs of - [host, port, endPointIdStr] -> - case reads endPointIdStr of - [(endPointId, "")] -> Just (host, port, endPointId) - _ -> Nothing - _ -> - Nothing - --- | Do a (blocking) remote request --- --- May throw IO (user) exception if the local or the remote endpoint is closed, --- or if the send fails. -doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] -doRemoteRequest (ourEndPoint, theirEndPoint) header = do - replyMVar <- newEmptyMVar - modifyRemoteState_ (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseValid = \vst -> do - let reqId = vst ^. nextCtrlRequestId - sendOn vst [encodeInt32 header, encodeInt32 reqId] - return ( RemoteEndPointValid - . (nextCtrlRequestId ^: (+ 1)) - . (pendingCtrlRequestsAt reqId ^= Just replyMVar) - $ vst - ) - -- Error cases - , caseInvalid = - throwIO - , caseInit = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" - , caseClosing = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - , caseClosed = - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" - , caseFailed = - throwIO - } - mReply <- takeMVar replyMVar - case mReply of - Left err -> throwIO err - Right reply -> return reply - --- | Send a CloseSocket request if the remote endpoint is unused -closeIfUnused :: EndPointPair -> IO () -closeIfUnused (ourEndPoint, theirEndPoint) = - modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity - { caseValid = \vst -> - if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) - then do - sendOn vst [encodeInt32 CloseSocket] - resolved <- newEmptyMVar - return $ RemoteEndPointClosing resolved vst - else - return $ RemoteEndPointValid vst - } - --------------------------------------------------------------------------------- --- As soon as a remote connection fails, we want to put notify our endpoint -- --- and put it into a closed state. Since this may happen in many places, we -- --- provide some abstractions. -- --------------------------------------------------------------------------------- - -data RemoteStatePatternMatch a = RemoteStatePatternMatch - { caseInvalid :: TransportError ConnectErrorCode -> IO a - , caseInit :: MVar () -> RequestedBy -> IO a - , caseValid :: ValidRemoteEndPointState -> IO a - , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a - , caseClosed :: IO a - , caseFailed :: IOException -> IO a - } - -remoteStateIdentity :: RemoteStatePatternMatch RemoteState -remoteStateIdentity = - RemoteStatePatternMatch - { caseInvalid = return . RemoteEndPointInvalid - , caseInit = (return .) . RemoteEndPointInit - , caseValid = return . RemoteEndPointValid - , caseClosing = (return .) . RemoteEndPointClosing - , caseClosed = return RemoteEndPointClosed - , caseFailed = return . RemoteEndPointFailed - } - --- | Like modifyMVar, but if an I/O exception occurs don't restore the remote --- endpoint to its original value but close it instead -modifyRemoteState :: EndPointPair - -> RemoteStatePatternMatch (RemoteState, a) - -> IO a -modifyRemoteState (ourEndPoint, theirEndPoint) match = - mask $ \restore -> do - st <- takeMVar theirState - case st of - RemoteEndPointValid vst -> do - mResult <- try $ restore (caseValid match vst) - case mResult of - Right (st', a) -> do - putMVar theirState st' - return a - Left ex -> do - case fromException ex of - Just ioEx -> handleIOException ioEx vst - Nothing -> putMVar theirState st - throwIO ex - -- The other cases are less interesting, because unless the endpoint is - -- in Valid state we're not supposed to do any IO on it - RemoteEndPointInit resolved origin -> do - (st', a) <- onException (restore $ caseInit match resolved origin) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointClosing resolved vst -> do - (st', a) <- onException (restore $ caseClosing match resolved vst) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointInvalid err -> do - (st', a) <- onException (restore $ caseInvalid match err) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointClosed -> do - (st', a) <- onException (restore $ caseClosed match) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointFailed err -> do - (st', a) <- onException (restore $ caseFailed match err) - (putMVar theirState st) - putMVar theirState st' - return a - where - theirState :: MVar RemoteState - theirState = remoteState theirEndPoint - - handleIOException :: IOException -> ValidRemoteEndPointState -> IO () - handleIOException ex vst = do - tryCloseSocket (remoteSocket vst) - putMVar theirState (RemoteEndPointFailed ex) - let incoming = IntSet.elems $ vst ^. remoteIncoming - code = EventConnectionLost (remoteAddress theirEndPoint) incoming - err = TransportError code (show ex) - writeChan (localChannel ourEndPoint) $ ErrorEvent err - --- | Like 'modifyRemoteState' but without a return value -modifyRemoteState_ :: EndPointPair - -> RemoteStatePatternMatch RemoteState - -> IO () -modifyRemoteState_ (ourEndPoint, theirEndPoint) match = - modifyRemoteState (ourEndPoint, theirEndPoint) - RemoteStatePatternMatch - { caseInvalid = u . caseInvalid match - , caseInit = \resolved origin -> u $ caseInit match resolved origin - , caseValid = u . caseValid match - , caseClosing = \resolved vst -> u $ caseClosing match resolved vst - , caseClosed = u $ caseClosed match - , caseFailed = u . caseFailed match - } - where - u :: IO a -> IO (a, ()) - u p = p >>= \a -> return (a, ()) - --- | Like 'modifyRemoteState' but without the ability to change the state -withRemoteState :: EndPointPair - -> RemoteStatePatternMatch a - -> IO a -withRemoteState (ourEndPoint, theirEndPoint) match = - modifyRemoteState (ourEndPoint, theirEndPoint) - RemoteStatePatternMatch - { caseInvalid = \err -> do - a <- caseInvalid match err - return (RemoteEndPointInvalid err, a) - , caseInit = \resolved origin -> do - a <- caseInit match resolved origin - return (RemoteEndPointInit resolved origin, a) - , caseValid = \vst -> do - a <- caseValid match vst - return (RemoteEndPointValid vst, a) - , caseClosing = \resolved vst -> do - a <- caseClosing match resolved vst - return (RemoteEndPointClosing resolved vst, a) - , caseClosed = do - a <- caseClosed match - return (RemoteEndPointClosed, a) - , caseFailed = \err -> do - a <- caseFailed match err - return (RemoteEndPointFailed err, a) - } - --------------------------------------------------------------------------------- --- Incoming requests -- --------------------------------------------------------------------------------- - --- | Handle a connection request (that is, a remote endpoint that is trying to --- establish a TCP connection with us) +-- | Handle a connection request (that is, a remote endpoint that is trying to +-- establish a TCP connection with us) -- -- 'handleConnectionRequest' runs in the context of the transport thread, which -- can be killed asynchronously by 'closeTransport'. We fork a separate thread @@ -1106,6 +817,7 @@ handleConnectionRequest transport sock = handle handleException $ do where go :: LocalEndPoint -> EndPointAddress -> IO () go ourEndPoint theirAddress = do + -- This runs in a thread that will never be killed mEndPoint <- handle ((>> return Nothing) . handleException) $ do resetIfBroken ourEndPoint theirAddress (theirEndPoint, isNew) <- @@ -1131,103 +843,22 @@ handleConnectionRequest transport sock = handle handleException $ do -- If we left the scope of the exception handler with a return value of -- Nothing then the socket is already closed; otherwise, the socket has -- been recorded as part of the remote endpoint. Either way, we no longer - -- have to worry about closing the socket on receiving an asynchronous - -- exception from this point forward. - forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint - - handleException :: SomeException -> IO () - handleException _ex = - -- putStrLn $ "handleConnectionRequest " ++ show _ex - tryCloseSocket sock - --- | Find a remote endpoint. If the remote endpoint does not yet exist we --- create it in Init state. Returns if the endpoint was new. -findRemoteEndPoint - :: LocalEndPoint - -> EndPointAddress - -> RequestedBy - -> IO (RemoteEndPoint, Bool) -findRemoteEndPoint ourEndPoint theirAddress findOrigin = go - where - go = do - (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of - LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of - Just theirEndPoint -> - return (st, (theirEndPoint, False)) - Nothing -> do - resolved <- newEmptyMVar - theirState <- newMVar (RemoteEndPointInit resolved findOrigin) - let theirEndPoint = RemoteEndPoint - { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId - } - return ( LocalEndPointValid - . (localConnectionTo theirAddress ^= Just theirEndPoint) - . (nextRemoteId ^: (+ 1)) - $ vst - , (theirEndPoint, True) - ) - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - - if isNew - then - return (theirEndPoint, True) - else do - let theirState = remoteState theirEndPoint - snapshot <- modifyMVar theirState $ \st -> case st of - RemoteEndPointValid vst -> - case findOrigin of - RequestedByUs -> do - let st' = RemoteEndPointValid - . (remoteOutgoing ^: (+ 1)) - $ vst - return (st', st') - RequestedByThem -> - return (st, st) - _ -> - return (st, st) - -- The snapshot may no longer be up to date at this point, but if we - -- increased the refcount then it can only either be valid or closed - -- (after an explicit call to 'closeEndPoint' or 'closeTransport') - case snapshot of - RemoteEndPointInvalid err -> - throwIO err - RemoteEndPointInit resolved initOrigin -> - case (findOrigin, initOrigin) of - (RequestedByUs, RequestedByUs) -> - readMVar resolved >> go - (RequestedByUs, RequestedByThem) -> - readMVar resolved >> go - (RequestedByThem, RequestedByUs) -> - if ourAddress > theirAddress - then - -- Wait for the Crossed message - readMVar resolved >> go - else - return (theirEndPoint, False) - (RequestedByThem, RequestedByThem) -> - throwIO $ userError "Already connected (B)" - RemoteEndPointValid _ -> - -- We assume that the request crossed if we find the endpoint in - -- Valid state. It is possible that this is really an invalid - -- request, but only in the case of a broken client (we don't - -- maintain enough history to be able to tell the difference). - return (theirEndPoint, False) - RemoteEndPointClosing resolved _ -> - readMVar resolved >> go - RemoteEndPointClosed -> - go - RemoteEndPointFailed err -> - throwIO err - - ourState = localState ourEndPoint - ourAddress = localAddress ourEndPoint + -- have to worry about closing the socket on receiving an asynchronous + -- exception from this point forward. + forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint + + handleException :: SomeException -> IO () + handleException ex = do + tryCloseSocket sock + rethrowIfAsync (fromException ex) + + rethrowIfAsync :: Maybe AsyncException -> IO () + rethrowIfAsync = mapM_ throwIO -- | Handle requests from a remote endpoint. -- -- Returns only if the remote party closes the socket or if an error occurs. +-- This runs in a thread that will never be killed. handleIncomingMessages :: EndPointPair -> IO () handleIncomingMessages (ourEndPoint, theirEndPoint) = do mSock <- withMVar theirState $ \st -> @@ -1258,11 +889,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- (because a 'send' failed) -- the individual handlers below will throw a -- user exception which is then caught and handled the same way as an -- exception thrown by 'recv'. - -- - -- Note: modifyRemoteState closes the socket before putting the remote - -- endpoint in closing state, so it is not possible that modifyRemoteState - -- puts the remote endpoint in Closing state only for it to be reset to - -- Valid by the RequestConnectionId handler below. go :: N.Socket -> IO () go sock = do connId <- recvInt32 sock @@ -1290,8 +916,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Create a new connection createNewConnection :: ControlRequestId -> IO () createNewConnection reqId = do - -- getNextConnectionId throws an exception if ourEndPoint is closed; but - -- if this endpoint is closed, this thread will soon die anyway newId <- getNextConnectionId ourEndPoint modifyMVar_ theirState $ \st -> do vst <- case st of @@ -1321,9 +945,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do : encodeInt32 reqId : prependLength [encodeInt32 newId] ) - -- We add the new connection ID to the list of open connections only - -- once the endpoint has been notified of the new connection (sendOn - -- may fail) return (RemoteEndPointValid vst) writeChan ourChannel (ConnectionOpened newId ReliableOrdered theirAddr) @@ -1425,7 +1046,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (closed)" - -- Read a message and output it on the endPoint's channel By rights we + -- Read a message and output it on the endPoint's channel. By rights we -- should verify that the connection ID is valid, but this is unnecessary -- overhead readMessage :: N.Socket -> ConnectionId -> IO () @@ -1465,6 +1086,221 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointFailed err' -> return (RemoteEndPointFailed err') +-------------------------------------------------------------------------------- +-- Uninterruptable auxiliary functions -- +-- -- +-- All these functions assume they are running in a thread which will never -- +-- be killed. +-------------------------------------------------------------------------------- + +-- | Request a connection to a remote endpoint +-- +-- This will block until we get a connection ID from the remote endpoint; if +-- the remote endpoint was in 'RemoteEndPointClosing' state then we will +-- additionally block until that is resolved. +-- +-- May throw a TransportError ConnectErrorCode exception. +requestConnectionTo :: TCPParameters + -> LocalEndPoint + -> EndPointAddress + -> ConnectHints + -> IO (RemoteEndPoint, ConnectionId) +requestConnectionTo params ourEndPoint theirAddress hints = go + where + go = do + (theirEndPoint, isNew) <- mapIOException connectFailed $ + findRemoteEndPoint ourEndPoint theirAddress RequestedByUs + + if isNew + then do + forkIO . handle absorbAllExceptions $ + setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints + go + else do + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + return (theirEndPoint, decodeInt32 . BS.concat $ reply) + + connectFailed :: IOException -> TransportError ConnectErrorCode + connectFailed = TransportError ConnectFailed . show + + absorbAllExceptions :: SomeException -> IO () + absorbAllExceptions _ex = + return () + +-- | Set up a remote endpoint +setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () +setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do + result <- socketToEndPoint ourAddress + theirAddress + (tcpReuseClientAddr params) + (connectTimeout hints) + didAccept <- case result of + Right (sock, ConnectionRequestAccepted) -> do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 + } + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) + return True + Right (sock, ConnectionRequestInvalid) -> do + let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + tryCloseSocket sock + return False + Right (sock, ConnectionRequestCrossed) -> do + resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed + tryCloseSocket sock + return False + Left err -> do + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + return False + + when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) + where + ourAddress = localAddress ourEndPoint + theirAddress = remoteAddress theirEndPoint + invalidAddress = TransportError ConnectNotFound + +-- | Do a (blocking) remote request +-- +-- May throw IO (user) exception if the local or the remote endpoint is closed, +-- if the send fails, or if the remote endpoint fails before it replies. +doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] +doRemoteRequest (ourEndPoint, theirEndPoint) header = do + replyMVar <- newEmptyMVar + modifyRemoteState_ (ourEndPoint, theirEndPoint) RemoteStatePatternMatch + { caseValid = \vst -> do + let reqId = vst ^. nextCtrlRequestId + sendOn vst [encodeInt32 header, encodeInt32 reqId] + return ( RemoteEndPointValid + . (nextCtrlRequestId ^: (+ 1)) + . (pendingCtrlRequestsAt reqId ^= Just replyMVar) + $ vst + ) + -- Error cases + , caseInvalid = + throwIO + , caseInit = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" + , caseClosing = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" + , caseClosed = + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" + , caseFailed = + throwIO + } + mReply <- takeMVar replyMVar + case mReply of + Left err -> throwIO err + Right reply -> return reply + +-- | Send a CloseSocket request if the remote endpoint is unused +closeIfUnused :: EndPointPair -> IO () +closeIfUnused (ourEndPoint, theirEndPoint) = + modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity + { caseValid = \vst -> + if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + then do + sendOn vst [encodeInt32 CloseSocket] + resolved <- newEmptyMVar + return $ RemoteEndPointClosing resolved vst + else + return $ RemoteEndPointValid vst + } + +-- | Reset a remote endpoint if it is in Invalid mode +-- +-- If the remote endpoint is currently in broken state, and +-- +-- - a user calls the API function 'connect', or and the remote endpoint is +-- - an inbound connection request comes in from this remote address +-- +-- we remove the remote endpoint first. +-- +-- Throws a TransportError ConnectFailed exception if the local endpoint is +-- closed. +resetIfBroken :: LocalEndPoint -> EndPointAddress -> IO () +resetIfBroken ourEndPoint theirAddress = do + mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> + return (vst ^. localConnectionTo theirAddress) + LocalEndPointClosed -> + throwIO $ TransportError ConnectFailed "Endpoint closed" + forM_ mTheirEndPoint $ \theirEndPoint -> + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInvalid _ -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + RemoteEndPointFailed _ -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + _ -> + return () + +-- | Special case of 'apiConnect': connect an endpoint to itself +-- +-- May throw a TransportError ConnectErrorCode (if the local endpoint is closed) +connectToSelf :: LocalEndPoint + -> IO Connection +connectToSelf ourEndPoint = do + connAlive <- newIORef True -- Protected by the local endpoint lock + connId <- mapIOException connectFailed $ getNextConnectionId ourEndPoint + writeChan ourChan $ + ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) + return Connection + { send = selfSend connAlive connId + , close = selfClose connAlive connId + } + where + selfSend :: IORef Bool + -> ConnectionId + -> [ByteString] + -> IO (Either (TransportError SendErrorCode) ()) + selfSend connAlive connId msg = + try . withMVar ourState $ \st -> case st of + LocalEndPointValid _ -> do + alive <- readIORef connAlive + if alive + then writeChan ourChan (Received connId msg) + else throwIO $ TransportError SendClosed "Connection closed" + LocalEndPointClosed -> + throwIO $ TransportError SendFailed "Endpoint closed" + + selfClose :: IORef Bool -> ConnectionId -> IO () + selfClose connAlive connId = + withMVar ourState $ \st -> case st of + LocalEndPointValid _ -> do + alive <- readIORef connAlive + when alive $ do + writeChan ourChan (ConnectionClosed connId) + writeIORef connAlive False + LocalEndPointClosed -> + return () + + ourChan = localChannel ourEndPoint + ourState = localState ourEndPoint + connectFailed = TransportError ConnectFailed . show + +-- | Resolve an endpoint currently in 'Init' state +resolveInit :: EndPointPair -> RemoteState -> IO () +resolveInit (ourEndPoint, theirEndPoint) newState = + modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit resolved _ -> do + putMVar resolved () + case newState of + RemoteEndPointClosed -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + _ -> + return () + return newState + RemoteEndPointFailed ex -> + throwIO ex + _ -> + relyViolation (ourEndPoint, theirEndPoint) "resolveInit" + -- | Get the next connection ID -- -- Throws an IO exception when the endpoint is closed. @@ -1476,96 +1312,266 @@ getNextConnectionId ourEndpoint = return ( LocalEndPointValid . (nextConnectionId ^= connId + 1) $ vst - , connId) - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" + , connId) + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + +-- | Create a new local endpoint +-- +-- May throw a TransportError NewEndPointErrorCode exception if the transport +-- is closed. +createLocalEndPoint :: TCPTransport -> IO LocalEndPoint +createLocalEndPoint transport = do + chan <- newChan + state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState + { _nextConnectionId = firstNonReservedConnectionId + , _localConnections = Map.empty + , _nextRemoteId = 0 + } + modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> do + let ix = vst ^. nextEndPointId + let addr = encodeEndPointAddress (transportHost transport) + (transportPort transport) + ix + let localEndPoint = LocalEndPoint { localAddress = addr + , localChannel = chan + , localState = state + } + return ( TransportValid + . (localEndPointAt addr ^= Just localEndPoint) + . (nextEndPointId ^= ix + 1) + $ vst + , localEndPoint + ) + TransportClosed -> + throwIO (TransportError NewEndPointFailed "Transport closed") + + +-- | Remove reference to a remote endpoint from a local endpoint +-- +-- If the local endpoint is closed, do nothing +removeRemoteEndPoint :: EndPointPair -> IO () +removeRemoteEndPoint (ourEndPoint, theirEndPoint) = + modifyMVar_ ourState $ \st -> case st of + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Nothing -> + return st + Just remoteEndPoint' -> + if remoteId remoteEndPoint' == remoteId theirEndPoint + then return + ( LocalEndPointValid + . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) + $ vst + ) + else return st + LocalEndPointClosed -> + return LocalEndPointClosed + where + ourState = localState ourEndPoint + theirAddress = remoteAddress theirEndPoint + +-- | Remove reference to a local endpoint from the transport state +-- +-- Does nothing if the transport is closed +removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () +removeLocalEndPoint transport ourEndPoint = + modifyMVar_ (transportState transport) $ \st -> case st of + TransportValid vst -> + return ( TransportValid + . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) + $ vst + ) + TransportClosed -> + return TransportClosed + +-- | Find a remote endpoint. If the remote endpoint does not yet exist we +-- create it in Init state. Returns if the endpoint was new. +findRemoteEndPoint + :: LocalEndPoint + -> EndPointAddress + -> RequestedBy + -> IO (RemoteEndPoint, Bool) +findRemoteEndPoint ourEndPoint theirAddress findOrigin = go + where + go = do + (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of + LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of + Just theirEndPoint -> + return (st, (theirEndPoint, False)) + Nothing -> do + resolved <- newEmptyMVar + theirState <- newMVar (RemoteEndPointInit resolved findOrigin) + let theirEndPoint = RemoteEndPoint + { remoteAddress = theirAddress + , remoteState = theirState + , remoteId = vst ^. nextRemoteId + } + return ( LocalEndPointValid + . (localConnectionTo theirAddress ^= Just theirEndPoint) + . (nextRemoteId ^: (+ 1)) + $ vst + , (theirEndPoint, True) + ) + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + + if isNew + then + return (theirEndPoint, True) + else do + let theirState = remoteState theirEndPoint + snapshot <- modifyMVar theirState $ \st -> case st of + RemoteEndPointValid vst -> + case findOrigin of + RequestedByUs -> do + let st' = RemoteEndPointValid + . (remoteOutgoing ^: (+ 1)) + $ vst + return (st', st') + RequestedByThem -> + return (st, st) + _ -> + return (st, st) + -- The snapshot may no longer be up to date at this point, but if we + -- increased the refcount then it can only either be Valid or Failed + -- (after an explicit call to 'closeEndPoint' or 'closeTransport') + case snapshot of + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointInit resolved initOrigin -> + case (findOrigin, initOrigin) of + (RequestedByUs, RequestedByUs) -> + readMVar resolved >> go + (RequestedByUs, RequestedByThem) -> + readMVar resolved >> go + (RequestedByThem, RequestedByUs) -> + if ourAddress > theirAddress + then + -- Wait for the Crossed message + readMVar resolved >> go + else + return (theirEndPoint, False) + (RequestedByThem, RequestedByThem) -> + throwIO $ userError "Already connected" + RemoteEndPointValid _ -> + -- We assume that the request crossed if we find the endpoint in + -- Valid state. It is possible that this is really an invalid + -- request, but only in the case of a broken client (we don't + -- maintain enough history to be able to tell the difference). + return (theirEndPoint, False) + RemoteEndPointClosing resolved _ -> + readMVar resolved >> go + RemoteEndPointClosed -> + go + RemoteEndPointFailed err -> + throwIO err + + ourState = localState ourEndPoint + ourAddress = localAddress ourEndPoint -------------------------------------------------------------------------------- --- Uninterruptable auxiliary functions -- --- -- --- All these functions assume they are running in a thread which will never -- --- be killed, and are designed to be used in conjunction with -- --- 'asyncWhenCancelled'. To be explicit about this assumption the function -- --- names are prefixed with 'u'. -- +-- "Stateless" (MVar free) functions -- -------------------------------------------------------------------------------- --- | Request a connection to a remote endpoint --- --- This will block until we get a connection ID from the remote endpoint; if --- the remote endpoint was in 'RemoteEndPointClosing' state then we will --- additionally block until that is resolved. +-- | Establish a connection to a remote endpoint -- --- May throw a TransportError ConnectErrorCode exception. -uRequestConnectionTo :: TCPParameters - -> LocalEndPoint - -> EndPointAddress - -> ConnectHints - -> IO (RemoteEndPoint, ConnectionId) -uRequestConnectionTo params ourEndPoint theirAddress hints = go +-- Maybe throw a TransportError +socketToEndPoint :: EndPointAddress -- ^ Our address + -> EndPointAddress -- ^ Their address + -> Bool -- ^ Use SO_REUSEADDR? + -> Maybe Int -- ^ Timeout for connect + -> IO (Either (TransportError ConnectErrorCode) + (N.Socket, ConnectionRequestResponse)) +socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = + try $ do + (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of + Nothing -> throwIO (failed . userError $ "Could not parse") + Just dec -> return dec + addr:_ <- mapIOException invalidAddress $ + N.getAddrInfo Nothing (Just host) (Just port) + bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do + when reuseAddr $ + mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 + mapIOException invalidAddress $ + timeoutMaybe timeout timeoutError $ + N.connect sock (N.addrAddress addr) + response <- mapIOException failed $ do + sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) + recvInt32 sock + case tryToEnum response of + Nothing -> throwIO (failed . userError $ "Unexpected response") + Just r -> return (sock, r) where - go = do - (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress RequestedByUs + createSocket :: N.AddrInfo -> IO N.Socket + createSocket addr = mapIOException insufficientResources $ + N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - if isNew - then do - forkIO . handle absorbAllExceptions $ - uSetupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints - go - else do - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - return (theirEndPoint, decodeInt32 . BS.concat $ reply) + invalidAddress = TransportError ConnectNotFound . show + insufficientResources = TransportError ConnectInsufficientResources . show + failed = TransportError ConnectFailed . show + timeoutError = TransportError ConnectTimeout "Timed out" - connectFailed = TransportError ConnectFailed . show +-- | Encode end point address +encodeEndPointAddress :: N.HostName + -> N.ServiceName + -> EndPointId + -> EndPointAddress +encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ + host ++ ":" ++ port ++ ":" ++ show ix - absorbAllExceptions :: SomeException -> IO () - absorbAllExceptions _ex = - return () +-- | Decode end point address +decodeEndPointAddress :: EndPointAddress + -> Maybe (N.HostName, N.ServiceName, EndPointId) +decodeEndPointAddress (EndPointAddress bs) = + case map BSC.unpack $ BSC.split ':' bs of + [host, port, endPointIdStr] -> + case reads endPointIdStr of + [(endPointId, "")] -> Just (host, port, endPointId) + _ -> Nothing + _ -> + Nothing --- | Set up a remote endpoint -uSetupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () -uSetupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do - result <- socketToEndPoint ourAddress - theirAddress - (tcpReuseClientAddr params) - (connectTimeout hints) - didAccept <- case result of - Right (sock, ConnectionRequestAccepted) -> do - let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 - } - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) - return True - Right (sock, ConnectionRequestInvalid) -> do - let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - tryCloseSocket sock - return False - Right (sock, ConnectionRequestCrossed) -> do - resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed - tryCloseSocket sock - return False - Left err -> do - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - return False - - -- If we get to this point without an exception, then - -- - -- if didAccept is False the socket has already been closed - -- if didAccept is True, the socket has been stored as part of the remote - -- state so we no longer need to worry about closing it when an - -- asynchronous exception occurs - when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) - where - ourAddress = localAddress ourEndPoint - theirAddress = remoteAddress theirEndPoint - invalidAddress = TransportError ConnectNotFound +-------------------------------------------------------------------------------- +-- Functions from TransportInternals -- +-------------------------------------------------------------------------------- + +-- Find a socket between two endpoints +-- +-- Throws an IO exception if the socket could not be found. +internalSocketBetween :: TCPTransport -- ^ Transport + -> EndPointAddress -- ^ Local endpoint + -> EndPointAddress -- ^ Remote endpoint + -> IO N.Socket +internalSocketBetween transport ourAddress theirAddress = do + ourEndPoint <- withMVar (transportState transport) $ \st -> case st of + TransportClosed -> + throwIO $ userError "Transport closed" + TransportValid vst -> + case vst ^. localEndPointAt ourAddress of + Nothing -> throwIO $ userError "Local endpoint not found" + Just ep -> return ep + theirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Nothing -> throwIO $ userError "Remote endpoint not found" + Just ep -> return ep + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit _ _ -> + throwIO $ userError "Remote endpoint not yet initialized" + RemoteEndPointValid vst -> + return $ remoteSocket vst + RemoteEndPointClosing _ vst -> + return $ remoteSocket vst + RemoteEndPointClosed -> + throwIO $ userError "Remote endpoint closed" + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointFailed err -> + throwIO err -------------------------------------------------------------------------------- -- Constants -- From d7cf32030ebdaae5e4aa949da0e011105e879c04 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 5 Jun 2012 13:45:30 +0100 Subject: [PATCH 0102/2357] tidy up comment --- src/Network/Transport/TCP.hs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index e4c9f9ec..b8dae7a0 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -255,15 +255,15 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- -- Init ---+---> Invalid -- | --- +---------------------------------------\ --- | | --- | /--------------\ | --- | | | | --- | v | v --- +---> Valid ---+---> Closing ---+---> Closed --- | | | | --- | | | v --- \-------+--------------+------------> Failed +-- +-------------------------------\ +-- | | +-- | /----------\ | +-- | | | | +-- | v | v +-- +---> Valid ---> Closing ---> Closed +-- | | | | +-- | | | v +-- \-------+----------+--------> Failed -- -- Init: There are two places where we create new remote endpoints: in -- requestConnectionTo (in response to an API 'connect' call) and in From 4f36a5b044c09e75e33b92fece4dcb76a6d3406c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 6 Jun 2012 10:06:10 +0100 Subject: [PATCH 0103/2357] More robust test --- tests/TestTCP.hs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index c066d704..bbf46f57 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -496,15 +496,20 @@ testUnnecessaryConnect nextPort numThreads = do dones <- replicateM numThreads $ do done <- newEmptyMVar forkTry $ do - Right (_, reply) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - case reply of - ConnectionRequestAccepted -> + -- It is possible that the remote endpoint just rejects the request by closing the socket + -- immediately (depending on far the remote endpoint got with the initialization) + response <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + case response of + Right (_, ConnectionRequestAccepted) -> + -- We don't close this socket because we want to keep this connection open putMVar gotAccepted () -- We might get either Invalid or Crossed (the transport does not -- maintain enough history to be able to tell) - ConnectionRequestInvalid -> - return () - ConnectionRequestCrossed -> + Right (sock, ConnectionRequestInvalid) -> + N.sClose sock + Right (sock, ConnectionRequestCrossed) -> + N.sClose sock + Left _ -> return () putMVar done () return done From cff38dcfa5b786323ccbd07bfbbd2fe386c58730 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 6 Jun 2012 17:35:09 +0100 Subject: [PATCH 0104/2357] Ping-example with new CH layer almost working --- network-transport.cabal | 14 ++++++++++---- src/Network/Transport.hs | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/network-transport.cabal b/network-transport.cabal index 7567e3f3..db933017 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -18,12 +18,15 @@ Library data-accessor, network, mtl, + binary, transformers Exposed-modules: Network.Transport, Network.Transport.Chan, Network.Transport.TCP, Network.Transport.Util - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables + Network.Transport.Internal + Network.Transport.Internal.TCP + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src @@ -38,8 +41,9 @@ Test-Suite TestTCP mtl, transformers, ansi-terminal, + binary, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N HS-Source-Dirs: tests src @@ -53,8 +57,9 @@ Test-Suite TestMulticastInMemory mtl, transformers, ansi-terminal, + binary, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src @@ -68,7 +73,8 @@ Test-Suite TestInMemory mtl, transformers, ansi-terminal, + binary, random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, ExistentialQuantification, FlexibleInstances, OverlappingInstances + extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving, ExistentialQuantification, FlexibleInstances, OverlappingInstances ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: tests src diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index c7f2fd32..a4480fb3 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -25,6 +25,7 @@ module Network.Transport ( -- * Types import Data.ByteString (ByteString) import Control.Exception (Exception) import Data.Typeable (Typeable) +import Data.Binary (Binary) -------------------------------------------------------------------------------- -- Main API -- @@ -109,7 +110,7 @@ data MulticastGroup = MulticastGroup { -- | EndPointAddress of an endpoint. newtype EndPointAddress = EndPointAddress { endPointAddressToByteString :: ByteString } - deriving (Eq, Ord) + deriving (Eq, Ord, Typeable, Binary) instance Show EndPointAddress where show = show . endPointAddressToByteString From 65a0666149bf562ca171128a1336994c157cf200 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 7 Jun 2012 16:49:11 +0100 Subject: [PATCH 0105/2357] Add a 'unique' component to LocalProcessId --- src/Network/Transport/Internal.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index a655c457..7545c014 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -61,9 +61,9 @@ decodeInt32 :: Num a => ByteString -> a decodeInt32 bs | BS.length bs /= 4 = throw $ userError "decodeInt32: Invalid length" | otherwise = BSI.inlinePerformIO $ do - let (fp, _, _) = BSI.toForeignPtr bs + let (fp, offset, _) = BSI.toForeignPtr bs withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p offset return (fromIntegral . ntohl $ w32) -- | Serialize 16-bit to network byte order @@ -78,9 +78,9 @@ decodeInt16 :: Num a => ByteString -> a decodeInt16 bs | BS.length bs /= 2 = throw $ userError "decodeInt16: Invalid length" | otherwise = BSI.inlinePerformIO $ do - let (fp, _, _) = BSI.toForeignPtr bs + let (fp, offset, _) = BSI.toForeignPtr bs withForeignPtr fp $ \p -> do - w16 <- peekByteOff p 0 + w16 <- peekByteOff p offset return (fromIntegral . ntohs $ w16) -- | Prepend a list of bytestrings with their total length From 277c4e051c0fd5e1990520e30b7ba75fb38c200d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 13 Jun 2012 17:16:08 +0100 Subject: [PATCH 0106/2357] Optional EndPointAddress in EventConnectionLost --- src/Network/Transport.hs | 6 ++++-- src/Network/Transport/TCP.hs | 4 ++-- tests/TestTCP.hs | 6 +++--- tests/TestTransport.hs | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index a4480fb3..46e66e2b 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -218,6 +218,8 @@ data EventErrorCode = EventEndPointFailed -- | Transport-wide fatal error | EventTransportFailed - -- | Connection to a remote endpoint was lost - | EventConnectionLost EndPointAddress [ConnectionId] + -- | Some incoming connections were closed abruptly. + -- If an endpoint address is specified, then all connections to and + -- from that endpoint are now lost + | EventConnectionLost (Maybe EndPointAddress) [ConnectionId] deriving (Show, Typeable, Eq) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index b8dae7a0..5bf8a9e4 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -732,7 +732,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = tryCloseSocket (remoteSocket vst) putMVar theirState (RemoteEndPointFailed ex) let incoming = IntSet.elems $ vst ^. remoteIncoming - code = EventConnectionLost (remoteAddress theirEndPoint) incoming + code = EventConnectionLost (Just $ remoteAddress theirEndPoint) incoming err = TransportError code (show ex) writeChan (localChannel ourEndPoint) $ ErrorEvent err @@ -1072,7 +1072,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do let code = EventConnectionLost - (remoteAddress theirEndPoint) + (Just $ remoteAddress theirEndPoint) (IntSet.elems $ vst ^. remoteIncoming) writeChan ourChannel . ErrorEvent $ TransportError code (show err) forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index bbf46f57..f087145d 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -93,7 +93,7 @@ testEarlyDisconnect nextPort = do ConnectionOpened cid _ addr <- receive endpoint True <- return $ addr == theirAddr - ErrorEvent (TransportError (EventConnectionLost addr' [cid']) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid']) _) <- receive endpoint True <- return $ addr' == theirAddr && cid' == cid return () @@ -116,7 +116,7 @@ testEarlyDisconnect nextPort = do Received cid' ["pong"] <- receive endpoint True <- return $ cid == cid' - ErrorEvent (TransportError (EventConnectionLost addr' [cid'']) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid'']) _) <- receive endpoint True <- return $ addr' == theirAddr && cid'' == cid return () @@ -227,7 +227,7 @@ testEarlyCloseSocket nextPort = do ConnectionClosed cid'' <- receive endpoint True <- return $ cid'' == cid - ErrorEvent (TransportError (EventConnectionLost addr' []) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint True <- return $ addr' == theirAddr return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 43d49fc4..e528e327 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -598,7 +598,7 @@ testCloseEndPoint transport _ = do send conn ["pong"] ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' - ErrorEvent (TransportError (EventConnectionLost addr' []) _) <- receive endpoint ; True <- return $ addr' == theirAddr + ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint ; True <- return $ addr' == theirAddr Left (TransportError SendFailed _) <- send conn ["pong2"] @@ -686,7 +686,7 @@ testCloseTransport newTransport = do evs <- replicateM 3 $ receive endpoint let expected = [ ConnectionClosed cid1 , ConnectionClosed cid2 - , ErrorEvent (TransportError (EventConnectionLost theirAddr2 []) "") + , ErrorEvent (TransportError (EventConnectionLost (Just theirAddr2) []) "") ] True <- return $ any (== expected) (permutations evs) From 5208c43d9d5c037b476ad40819fa53ae08fc3601 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 14 Jun 2012 17:16:48 +0100 Subject: [PATCH 0107/2357] Support unlink/unmonitor --- src/Network/Transport.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 46e66e2b..684e45ab 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -23,6 +23,7 @@ module Network.Transport ( -- * Types ) where import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as BSC (unpack) import Control.Exception (Exception) import Data.Typeable (Typeable) import Data.Binary (Binary) @@ -113,7 +114,7 @@ newtype EndPointAddress = EndPointAddress { endPointAddressToByteString :: ByteS deriving (Eq, Ord, Typeable, Binary) instance Show EndPointAddress where - show = show . endPointAddressToByteString + show = BSC.unpack . endPointAddressToByteString -- | EndPointAddress of a multicast group. newtype MulticastAddress = MulticastAddress { multicastAddressToByteString :: ByteString } From 22bdea66bab44713c63bc3546660ee2cc38b44f0 Mon Sep 17 00:00:00 2001 From: ghc704 Date: Fri, 6 Jul 2012 19:25:31 +0100 Subject: [PATCH 0108/2357] Split Network.Transport. THIS BREAKS THE CH BUILD. Starting to prepare for release. Have not yet updated the CH build to reflect the changes. --- LICENSE | 31 + Setup.hs | 2 + network-transport-tcp.cabal | 49 + src/Network/Transport/TCP.hs | 1649 +++++++++++++++++++++++++ src/Network/Transport/TCP/Internal.hs | 116 ++ tests/TestAuxiliary.hs | 108 ++ tests/TestTCP.hs | 786 ++++++++++++ tests/TestTransport.hs | 956 ++++++++++++++ tests/Traced.hs | 191 +++ 9 files changed, 3888 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 network-transport-tcp.cabal create mode 100644 src/Network/Transport/TCP.hs create mode 100644 src/Network/Transport/TCP/Internal.hs create mode 100644 tests/TestAuxiliary.hs create mode 100644 tests/TestTCP.hs create mode 100644 tests/TestTransport.hs create mode 100644 tests/Traced.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f3459e44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011-2012 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal new file mode 100644 index 00000000..a739aeed --- /dev/null +++ b/network-transport-tcp.cabal @@ -0,0 +1,49 @@ +Name: network-transport-tcp +Version: 0.2.0 +Cabal-Version: >=1.8 +Build-Type: Simple +License: BSD3 +License-file: LICENSE +Copyright: Well-Typed LLP +Author: Duncan Coutts, Nicolas Wu, Edsko de Vries +Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process +Bug-Reports: mailto:edsko@well-typed.com +Synopsis: TCP instantation of Network.Transport +Description: TCP instantation of Network.Transport +Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 +Category: Network + +Library + Build-Depends: base >= 4.3 && < 5, + network-transport >= 0.2 && < 0.3, + data-accessor >= 0.2 && < 0.3, + containers >= 0.4 && < 0.5, + bytestring >= 0.9 && < 0.10, + network >= 2.3 && < 2.4 + Exposed-modules: Network.Transport.TCP, + Network.Transport.TCP.Internal + ghc-options: -Wall -fno-warn-unused-do-bind + HS-Source-Dirs: src + +Test-Suite TestTCP + Type: exitcode-stdio-1.0 + Main-Is: TestTCP.hs + Build-Depends: base >= 4.3 && < 5, + network-transport >= 0.2 && < 0.3, + data-accessor >= 0.2 && < 0.3, + containers >= 0.4 && < 0.5, + bytestring >= 0.9 && < 0.10, + network >= 2.3 && < 2.4, + random >= 1.0 && < 1.1, + ansi-terminal >= 0.5 && < 0.6, + mtl >= 2.0 && < 2.2 + ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N + Extensions: ExistentialQuantification, + FlexibleInstances, + DeriveDataTypeable, + RankNTypes, + OverlappingInstances, + OverloadedStrings + HS-Source-Dirs: tests src diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs new file mode 100644 index 00000000..dd203313 --- /dev/null +++ b/src/Network/Transport/TCP.hs @@ -0,0 +1,1649 @@ +-- | TCP implementation of the transport layer. +-- +-- The TCP implementation guarantees that only a single TCP connection (socket) +-- will be used between endpoints, provided that the addresses specified are +-- canonical. If /A/ connects to /B/ and reports its address as +-- @192.168.0.1:8080@ and /B/ subsequently connects tries to connect to /A/ as +-- @client1.local:http-alt@ then the transport layer will not realize that the +-- TCP connection can be reused. +-- +-- Applications that use the TCP transport should use +-- 'Network.Socket.withSocketsDo' in their main function for Windows +-- compatibility (see "Network.Socket"). +module Network.Transport.TCP ( -- * Main API + createTransport + , TCPParameters(..) + , defaultTCPParameters + -- * Internals (exposed for unit tests) + , createTransportExposeInternals + , TransportInternals(..) + , EndPointId + , encodeEndPointAddress + , decodeEndPointAddress + , ControlHeader(..) + , ConnectionRequestResponse(..) + , firstNonReservedConnectionId + , socketToEndPoint + -- * Design notes + -- $design + ) where + +import Prelude hiding (catch, mapM_) +import Network.Transport +import Network.Transport.TCP.Internal ( forkServer + , recvWithLength + , recvInt32 + , tryCloseSocket + ) +import Network.Transport.Internal ( encodeInt32 + , decodeInt32 + , prependLength + , mapIOException + , tryIO + , tryToEnum + , void + , timeoutMaybe + , asyncWhenCancelled + ) +import qualified Network.Socket as N ( HostName + , ServiceName + , Socket + , getAddrInfo + , socket + , addrFamily + , addrAddress + , SocketType(Stream) + , defaultProtocol + , setSocketOption + , SocketOption(ReuseAddr) + , connect + , sOMAXCONN + , AddrInfo + ) +import Network.Socket.ByteString (sendMany) +import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) +import Control.Concurrent.MVar ( MVar + , newMVar + , modifyMVar + , modifyMVar_ + , readMVar + , takeMVar + , putMVar + , newEmptyMVar + , withMVar + ) +import Control.Category ((>>>)) +import Control.Applicative ((<$>)) +import Control.Monad (when, unless) +import Control.Exception ( IOException + , SomeException + , AsyncException + , handle + , throw + , throwIO + , try + , bracketOnError + , mask + , onException + , fromException + ) +import Data.IORef (IORef, newIORef, writeIORef, readIORef) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS (concat) +import qualified Data.ByteString.Char8 as BSC (pack, unpack, split) +import Data.Int (Int32) +import Data.IntMap (IntMap) +import qualified Data.IntMap as IntMap (empty) +import Data.IntSet (IntSet) +import qualified Data.IntSet as IntSet ( empty + , insert + , elems + , singleton + , null + , delete + , member + ) +import Data.Map (Map) +import qualified Data.Map as Map (empty) +import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) +import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) +import Data.Foldable (forM_, mapM_) + +-- $design +-- +-- [Goals] +-- +-- The TCP transport maps multiple logical connections between /A/ and /B/ (in +-- either direction) to a single TCP connection: +-- +-- > +-------+ +-------+ +-- > | A |==========================| B | +-- > | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | +-- > | Q |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~Q | +-- > | \~~~|~~~~~~~~~~~~~~~~~~~~~~~~~<| | +-- > | |==========================| | +-- > +-------+ +-------+ +-- +-- Ignoring the complications detailed below, the TCP connection is set up is +-- when the first lightweight connection is created (in either direction), and +-- torn down when the last lightweight connection (in either direction) is +-- closed. +-- +-- [Connecting] +-- +-- Let /A/, /B/ be two endpoints without any connections. When /A/ wants to +-- connect to /B/, it locally records that it is trying to connect to /B/ and +-- sends a request to /B/. As part of the request /A/ sends its own endpoint +-- address to /B/ (so that /B/ can reuse the connection in the other direction). +-- +-- When /B/ receives the connection request it first checks if it did not +-- already initiate a connection request to /A/. If not it will acknowledge the +-- connection request by sending 'ConnectionRequestAccepted' to /A/ and record +-- that it has a TCP connection to /A/. +-- +-- The tricky case arises when /A/ sends a connection request to /B/ and /B/ +-- finds that it had already sent a connection request to /A/. In this case /B/ +-- will accept the connection request from /A/ if /A/s endpoint address is +-- smaller (lexicographically) than /B/s, and reject it otherwise. If it rejects +-- it, it sends a 'ConnectionRequestCrossed' message to /A/. (The +-- lexicographical ordering is an arbitrary but convenient way to break the +-- tie.) +-- +-- When it receives a 'ConnectionRequestCrossed' message the /A/ thread that +-- initiated the request just needs to wait until the /A/ thread that is dealing +-- with /B/'s connection request completes. +-- +-- [Disconnecting] +-- +-- The TCP connection is created as soon as the first logical connection from +-- /A/ to /B/ (or /B/ to /A/) is established. At this point a thread (@#@) is +-- spawned that listens for incoming connections from /B/: +-- +-- > +-------+ +-------+ +-- > | A |==========================| B | +-- > | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | +-- > | | | Q | +-- > | #| | | +-- > | |==========================| | +-- > +-------+ +-------+ +-- +-- The question is when the TCP connection can be closed again. Conceptually, +-- we want to do reference counting: when there are no logical connections left +-- between /A/ and /B/ we want to close the socket (possibly after some +-- timeout). +-- +-- However, /A/ and /B/ need to agree that the refcount has reached zero. It +-- might happen that /B/ sends a connection request over the existing socket at +-- the same time that /A/ closes its logical connection to /B/ and closes the +-- socket. This will cause a failure in /B/ (which will have to retry) which is +-- not caused by a network failure, which is unfortunate. (Note that the +-- connection request from /B/ might succeed even if /A/ closes the socket.) +-- +-- Instead, when /A/ is ready to close the socket it sends a 'CloseSocket' +-- request to /B/ and records that its connection to /B/ is closing. If /A/ +-- receives a new connection request from /B/ after having sent the +-- 'CloseSocket' request it simply forgets that it sent a 'CloseSocket' request +-- and increments the reference count of the connection again. +-- +-- When /B/ receives a 'CloseSocket' message and it too is ready to close the +-- connection, it will respond with a reciprocal 'CloseSocket' request to /A/ +-- and then actually close the socket. /A/ meanwhile will not send any more +-- requests to /B/ after having sent a 'CloseSocket' request, and will actually +-- close its end of the socket only when receiving the 'CloseSocket' message +-- from /B/. (Since /A/ recorded that its connection to /B/ is in closing state +-- after sending a 'CloseSocket' request to /B/, it knows not to reciprocate /B/ +-- reciprocal 'CloseSocket' message.) +-- +-- If there is a concurrent thread in /A/ waiting to connect to /B/ after /A/ +-- has sent a 'CloseSocket' request then this thread will block until /A/ knows +-- whether to reuse the old socket (if /B/ sends a new connection request +-- instead of acknowledging the 'CloseSocket') or to set up a new socket. + +-------------------------------------------------------------------------------- +-- Internal datatypes -- +-------------------------------------------------------------------------------- + +-- We use underscores for fields that we might update (using accessors) +-- +-- All data types follow the same structure: +-- +-- * A top-level data type describing static properties (TCPTransport, +-- LocalEndPoint, RemoteEndPoint) +-- * The 'static' properties include an MVar containing a data structure for +-- the dynamic properties (TransportState, LocalEndPointState, +-- RemoteEndPointState). The state could be invalid/valid/closed,/etc. +-- * For the case of "valid" we use third data structure to give more details +-- about the state (ValidTransportState, ValidLocalEndPointState, +-- ValidRemoteEndPointState). + +data TCPTransport = TCPTransport + { transportHost :: N.HostName + , transportPort :: N.ServiceName + , transportState :: MVar TransportState + , transportParams :: TCPParameters + } + +data TransportState = + TransportValid ValidTransportState + | TransportClosed + +data ValidTransportState = ValidTransportState + { _localEndPoints :: Map EndPointAddress LocalEndPoint + , _nextEndPointId :: EndPointId + } + +data LocalEndPoint = LocalEndPoint + { localAddress :: EndPointAddress + , localChannel :: Chan Event + , localState :: MVar LocalEndPointState + } + +data LocalEndPointState = + LocalEndPointValid ValidLocalEndPointState + | LocalEndPointClosed + +data ValidLocalEndPointState = ValidLocalEndPointState + { _nextConnectionId :: !ConnectionId + , _localConnections :: Map EndPointAddress RemoteEndPoint + , _nextRemoteId :: !Int + } + +-- REMOTE ENDPOINTS +-- +-- Remote endpoints (basically, TCP connections) have the following lifecycle: +-- +-- Init ---+---> Invalid +-- | +-- +-------------------------------\ +-- | | +-- | /----------\ | +-- | | | | +-- | v | v +-- +---> Valid ---> Closing ---> Closed +-- | | | | +-- | | | v +-- \-------+----------+--------> Failed +-- +-- Init: There are two places where we create new remote endpoints: in +-- requestConnectionTo (in response to an API 'connect' call) and in +-- handleConnectionRequest (when a remote node tries to connect to us). +-- 'Init' carries an MVar () 'resolved' which concurrent threads can use to +-- wait for the remote endpoint to finish initialization. We record who +-- requested the connection (the local endpoint or the remote endpoint). +-- +-- Invalid: We put the remote endpoint in invalid state only during +-- requestConnectionTo when we fail to connect. +-- +-- Valid: This is the "normal" state for a working remote endpoint. +-- +-- Closing: When we detect that a remote endpoint is no longer used, we send a +-- CloseSocket request across the connection and put the remote endpoint in +-- closing state. As with Init, 'Closing' carries an MVar () 'resolved' which +-- concurrent threads can use to wait for the remote endpoint to either be +-- closed fully (if the communication parnet responds with another +-- CloseSocket) or be put back in 'Valid' state if the remote endpoint denies +-- the request. +-- +-- We also put the endpoint in Closed state, directly from Init, if we our +-- outbound connection request crossed an inbound connection request and we +-- decide to keep the inbound (i.e., the remote endpoint sent us a +-- ConnectionRequestCrossed message). +-- +-- Closed: The endpoint is put in Closed state after a successful garbage +-- collection. +-- +-- Failed: If the connection to the remote endpoint is lost, or the local +-- endpoint (or the whole transport) is closed manually, the remote endpoint is +-- put in Failed state, and we record the reason. +-- +-- Invariants for dealing with remote endpoints: +-- +-- INV-SEND: Whenever we send data the remote endpoint must be locked (to avoid +-- interleaving bits of payload). +-- +-- INV-CLOSE: Local endpoints should never point to remote endpoint in closed +-- state. Whenever we put an endpoint in Closed state we remove that +-- endpoint from localConnections first, so that if a concurrent thread reads +-- the MVar, finds RemoteEndPointClosed, and then looks up the endpoint in +-- localConnections it is guaranteed to either find a different remote +-- endpoint, or else none at all (if we don't insist in this order some +-- threads might start spinning). +-- +-- INV-RESOLVE: We should only signal on 'resolved' while the remote endpoint is +-- locked, and the remote endpoint must be in Valid or Closed state once +-- unlocked. This guarantees that there will not be two threads attempting to +-- both signal on 'resolved'. +-- +-- INV-LOST: If a send or recv fails, or a socket is closed unexpectedly, we +-- first put the remote endpoint in Closed state, and then send a +-- EventConnectionLost event. This guarantees that we only send this event +-- once. +-- +-- INV-CLOSING: An endpoint in closing state is for all intents and purposes +-- closed; that is, we shouldn't do any 'send's on it (although 'recv' is +-- acceptable, of course -- as we are waiting for the remote endpoint to +-- confirm or deny the request). +-- +-- INV-LOCK-ORDER: Remote endpoint must be locked before their local endpoints. +-- In other words: it is okay to call modifyMVar on a local endpoint inside a +-- modifyMVar on a remote endpoint, but not the other way around. In +-- particular, it is okay to call removeRemoteEndPoint inside +-- modifyRemoteState. + +data RemoteEndPoint = RemoteEndPoint + { remoteAddress :: EndPointAddress + , remoteState :: MVar RemoteState + , remoteId :: Int + } + +data RequestedBy = RequestedByUs | RequestedByThem + deriving (Eq, Show) + +data RemoteState = + -- | Invalid remote endpoint (for example, invalid address) + RemoteEndPointInvalid (TransportError ConnectErrorCode) + -- | The remote endpoint is being initialized + | RemoteEndPointInit (MVar ()) RequestedBy + -- | "Normal" working endpoint + | RemoteEndPointValid ValidRemoteEndPointState + -- | The remote endpoint is being closed (garbage collected) + | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState + -- | The remote endpoint has been closed (garbage collected) + | RemoteEndPointClosed + -- | The remote endpoint has failed, or has been forcefully shutdown + -- using a closeTransport or closeEndPoint API call + | RemoteEndPointFailed IOException + +data ValidRemoteEndPointState = ValidRemoteEndPointState + { _remoteOutgoing :: !Int + , _remoteIncoming :: IntSet + , remoteSocket :: N.Socket + , sendOn :: [ByteString] -> IO () + , _pendingCtrlRequests :: IntMap (MVar (Either IOException [ByteString])) + , _nextCtrlRequestId :: !ControlRequestId + } + +-- | Local identifier for an endpoint within this transport +type EndPointId = Int32 + +-- | Control request ID +-- +-- Control requests are asynchronous; the request ID makes it possible to match +-- requests and replies +type ControlRequestId = Int32 + +-- | Pair of local and a remote endpoint (for conciseness in signatures) +type EndPointPair = (LocalEndPoint, RemoteEndPoint) + +-- | Control headers +data ControlHeader = + -- | Request a new connection ID from the remote endpoint + RequestConnectionId + -- | Tell the remote endpoint we will no longer be using a connection + | CloseConnection + -- | Respond to a control request /from/ the remote endpoint + | ControlResponse + -- | Request to close the connection (see module description) + | CloseSocket + deriving (Enum, Bounded, Show) + +-- | Response sent by /B/ to /A/ when /A/ tries to connect +data ConnectionRequestResponse = + -- | /B/ accepts the connection + ConnectionRequestAccepted + -- | /A/ requested an invalid endpoint + | ConnectionRequestInvalid + -- | /A/s request crossed with a request from /B/ (see protocols) + | ConnectionRequestCrossed + deriving (Enum, Bounded, Show) + +-- | Parameters for setting up the TCP transport +data TCPParameters = TCPParameters { + -- | Backlog for 'listen'. + -- Defaults to SOMAXCONN. + tcpBacklog :: Int + -- | Should we set SO_REUSEADDR on the server socket? + -- Defaults to True. + , tcpReuseServerAddr :: Bool + -- | Should we set SO_REUSEADDR on client sockets? + -- Defaults to True. + , tcpReuseClientAddr :: Bool + } + +-- | Internal functionality we expose for unit testing +data TransportInternals = TransportInternals + { -- | The ID of the thread that listens for new incoming connections + transportThread :: ThreadId + -- | Find the socket between a local and a remote endpoint + , socketBetween :: EndPointAddress + -> EndPointAddress + -> IO N.Socket + } + +-------------------------------------------------------------------------------- +-- Top-level functionality -- +-------------------------------------------------------------------------------- + +-- | Create a TCP transport +createTransport :: N.HostName + -> N.ServiceName + -> TCPParameters + -> IO (Either IOException Transport) +createTransport host port params = + either Left (Right . fst) <$> createTransportExposeInternals host port params + +-- | You should probably not use this function (used for unit testing only) +createTransportExposeInternals + :: N.HostName + -> N.ServiceName + -> TCPParameters + -> IO (Either IOException (Transport, TransportInternals)) +createTransportExposeInternals host port params = do + state <- newMVar . TransportValid $ ValidTransportState + { _localEndPoints = Map.empty + , _nextEndPointId = 0 + } + let transport = TCPTransport { transportState = state + , transportHost = host + , transportPort = port + , transportParams = params + } + tryIO $ bracketOnError (forkServer + host + port + (tcpBacklog params) + (tcpReuseServerAddr params) + (terminationHandler transport) + (handleConnectionRequest transport)) + killThread + (mkTransport transport) + where + mkTransport :: TCPTransport + -> ThreadId + -> IO (Transport, TransportInternals) + mkTransport transport tid = return + ( Transport + { newEndPoint = apiNewEndPoint transport + , closeTransport = let evs = [ EndPointClosed + , throw $ userError "Transport closed" + ] in + apiCloseTransport transport (Just tid) evs + } + , TransportInternals + { transportThread = tid + , socketBetween = internalSocketBetween transport + } + ) + + terminationHandler :: TCPTransport -> SomeException -> IO () + terminationHandler transport ex = do + let evs = [ ErrorEvent (TransportError EventTransportFailed (show ex)) + , throw $ userError "Transport closed" + ] + apiCloseTransport transport Nothing evs + +-- | Default TCP parameters +defaultTCPParameters :: TCPParameters +defaultTCPParameters = TCPParameters { + tcpBacklog = N.sOMAXCONN + , tcpReuseServerAddr = True + , tcpReuseClientAddr = True + } + +-------------------------------------------------------------------------------- +-- API functions -- +-------------------------------------------------------------------------------- + +-- | Close the transport +apiCloseTransport :: TCPTransport -> Maybe ThreadId -> [Event] -> IO () +apiCloseTransport transport mTransportThread evs = + asyncWhenCancelled return $ do + mTSt <- modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> return (TransportClosed, Just vst) + TransportClosed -> return (TransportClosed, Nothing) + forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) + -- This will invoke the termination handler, which in turn will call + -- apiCloseTransport again, but then the transport will already be closed + -- and we won't be passed a transport thread, so we terminate immmediate + forM_ mTransportThread killThread + +-- | Create a new endpoint +apiNewEndPoint :: TCPTransport + -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) +apiNewEndPoint transport = + try . asyncWhenCancelled closeEndPoint $ do + ourEndPoint <- createLocalEndPoint transport + return EndPoint + { receive = readChan (localChannel ourEndPoint) + , address = localAddress ourEndPoint + , connect = apiConnect (transportParams transport) ourEndPoint + , closeEndPoint = let evs = [ EndPointClosed + , throw $ userError "Endpoint closed" + ] in + apiCloseEndPoint transport evs ourEndPoint + , newMulticastGroup = return . Left $ newMulticastGroupError + , resolveMulticastGroup = return . Left . const resolveMulticastGroupError + } + where + newMulticastGroupError = + TransportError NewMulticastGroupUnsupported "Multicast not supported" + resolveMulticastGroupError = + TransportError ResolveMulticastGroupUnsupported "Multicast not supported" + +-- | Connnect to an endpoint +apiConnect :: TCPParameters -- ^ Parameters + -> LocalEndPoint -- ^ Local end point + -> EndPointAddress -- ^ Remote address + -> Reliability -- ^ Reliability (ignored) + -> ConnectHints -- ^ Hints + -> IO (Either (TransportError ConnectErrorCode) Connection) +apiConnect params ourEndPoint theirAddress _reliability hints = + try . asyncWhenCancelled close $ + if localAddress ourEndPoint == theirAddress + then connectToSelf ourEndPoint + else do + resetIfBroken ourEndPoint theirAddress + (theirEndPoint, connId) <- + requestConnectionTo params ourEndPoint theirAddress hints + -- connAlive can be an IORef rather than an MVar because it is protected + -- by the remoteState MVar. We don't need the overhead of locking twice. + connAlive <- newIORef True + return Connection + { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive + , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive + } + +-- | Close a connection +apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () +apiClose (ourEndPoint, theirEndPoint) connId connAlive = + void . tryIO . asyncWhenCancelled return $ do + modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity + { caseValid = \vst -> do + alive <- readIORef connAlive + if alive + then do + writeIORef connAlive False + sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] + return ( RemoteEndPointValid + . (remoteOutgoing ^: (\x -> x - 1)) + $ vst + ) + else + return (RemoteEndPointValid vst) + } + closeIfUnused (ourEndPoint, theirEndPoint) + +-- | Send data across a connection +apiSend :: EndPointPair -- ^ Local and remote endpoint + -> ConnectionId -- ^ Connection ID (supplied by remote endpoint) + -> IORef Bool -- ^ Is the connection still alive? + -> [ByteString] -- ^ Payload + -> IO (Either (TransportError SendErrorCode) ()) +apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = + -- We don't need the overhead of asyncWhenCancelled here + try . mapIOException sendFailed $ + withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch + { caseInvalid = \_ -> + relyViolation (ourEndPoint, theirEndPoint) "apiSend" + , caseInit = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "apiSend" + , caseValid = \vst -> do + alive <- readIORef connAlive + if alive + then sendOn vst (encodeInt32 connId : prependLength payload) + else throwIO $ TransportError SendClosed "Connection closed" + , caseClosing = \_ _ -> do + alive <- readIORef connAlive + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + else throwIO $ TransportError SendClosed "Connection closed" + , caseClosed = do + alive <- readIORef connAlive + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + else throwIO $ TransportError SendClosed "Connection closed" + , caseFailed = \err -> do + alive <- readIORef connAlive + if alive + then throwIO $ TransportError SendFailed (show err) + else throwIO $ TransportError SendClosed "Connection closed" + } + where + sendFailed = TransportError SendFailed . show + +-- | Force-close the endpoint +apiCloseEndPoint :: TCPTransport -- ^ Transport + -> [Event] -- ^ Events used to report closure + -> LocalEndPoint -- ^ Local endpoint + -> IO () +apiCloseEndPoint transport evs ourEndPoint = + asyncWhenCancelled return $ do + -- Remove the reference from the transport state + removeLocalEndPoint transport ourEndPoint + -- Close the local endpoint + mOurState <- modifyMVar (localState ourEndPoint) $ \st -> + case st of + LocalEndPointValid vst -> + return (LocalEndPointClosed, Just vst) + LocalEndPointClosed -> + return (LocalEndPointClosed, Nothing) + forM_ mOurState $ \vst -> do + forM_ (vst ^. localConnections) tryCloseRemoteSocket + forM_ evs $ writeChan (localChannel ourEndPoint) + where + -- Close the remote socket and return the set of all incoming connections + tryCloseRemoteSocket :: RemoteEndPoint -> IO () + tryCloseRemoteSocket theirEndPoint = do + -- We make an attempt to close the connection nicely + -- (by sending a CloseSocket first) + let closed = RemoteEndPointFailed . userError $ "apiCloseEndPoint" + modifyMVar_ (remoteState theirEndPoint) $ \st -> + case st of + RemoteEndPointInvalid _ -> + return st + RemoteEndPointInit resolved _ -> do + putMVar resolved () + return closed + RemoteEndPointValid conn -> do + tryIO $ sendOn conn [encodeInt32 CloseSocket] + tryCloseSocket (remoteSocket conn) + return closed + RemoteEndPointClosing resolved conn -> do + putMVar resolved () + tryCloseSocket (remoteSocket conn) + return closed + RemoteEndPointClosed -> + return st + RemoteEndPointFailed err -> + return $ RemoteEndPointFailed err + +-------------------------------------------------------------------------------- +-- As soon as a remote connection fails, we want to put notify our endpoint -- +-- and put it into a closed state. Since this may happen in many places, we -- +-- provide some abstractions. -- +-------------------------------------------------------------------------------- + +data RemoteStatePatternMatch a = RemoteStatePatternMatch + { caseInvalid :: TransportError ConnectErrorCode -> IO a + , caseInit :: MVar () -> RequestedBy -> IO a + , caseValid :: ValidRemoteEndPointState -> IO a + , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a + , caseClosed :: IO a + , caseFailed :: IOException -> IO a + } + +remoteStateIdentity :: RemoteStatePatternMatch RemoteState +remoteStateIdentity = + RemoteStatePatternMatch + { caseInvalid = return . RemoteEndPointInvalid + , caseInit = (return .) . RemoteEndPointInit + , caseValid = return . RemoteEndPointValid + , caseClosing = (return .) . RemoteEndPointClosing + , caseClosed = return RemoteEndPointClosed + , caseFailed = return . RemoteEndPointFailed + } + +-- | Like modifyMVar, but if an I/O exception occurs don't restore the remote +-- endpoint to its original value but close it instead +modifyRemoteState :: EndPointPair + -> RemoteStatePatternMatch (RemoteState, a) + -> IO a +modifyRemoteState (ourEndPoint, theirEndPoint) match = + mask $ \restore -> do + st <- takeMVar theirState + case st of + RemoteEndPointValid vst -> do + mResult <- try $ restore (caseValid match vst) + case mResult of + Right (st', a) -> do + putMVar theirState st' + return a + Left ex -> do + case fromException ex of + Just ioEx -> handleIOException ioEx vst + Nothing -> putMVar theirState st + throwIO ex + -- The other cases are less interesting, because unless the endpoint is + -- in Valid state we're not supposed to do any IO on it + RemoteEndPointInit resolved origin -> do + (st', a) <- onException (restore $ caseInit match resolved origin) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointClosing resolved vst -> do + (st', a) <- onException (restore $ caseClosing match resolved vst) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointInvalid err -> do + (st', a) <- onException (restore $ caseInvalid match err) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointClosed -> do + (st', a) <- onException (restore $ caseClosed match) + (putMVar theirState st) + putMVar theirState st' + return a + RemoteEndPointFailed err -> do + (st', a) <- onException (restore $ caseFailed match err) + (putMVar theirState st) + putMVar theirState st' + return a + where + theirState :: MVar RemoteState + theirState = remoteState theirEndPoint + + handleIOException :: IOException -> ValidRemoteEndPointState -> IO () + handleIOException ex vst = do + tryCloseSocket (remoteSocket vst) + putMVar theirState (RemoteEndPointFailed ex) + let incoming = IntSet.elems $ vst ^. remoteIncoming + code = EventConnectionLost (Just $ remoteAddress theirEndPoint) incoming + err = TransportError code (show ex) + writeChan (localChannel ourEndPoint) $ ErrorEvent err + +-- | Like 'modifyRemoteState' but without a return value +modifyRemoteState_ :: EndPointPair + -> RemoteStatePatternMatch RemoteState + -> IO () +modifyRemoteState_ (ourEndPoint, theirEndPoint) match = + modifyRemoteState (ourEndPoint, theirEndPoint) + RemoteStatePatternMatch + { caseInvalid = u . caseInvalid match + , caseInit = \resolved origin -> u $ caseInit match resolved origin + , caseValid = u . caseValid match + , caseClosing = \resolved vst -> u $ caseClosing match resolved vst + , caseClosed = u $ caseClosed match + , caseFailed = u . caseFailed match + } + where + u :: IO a -> IO (a, ()) + u p = p >>= \a -> return (a, ()) + +-- | Like 'modifyRemoteState' but without the ability to change the state +withRemoteState :: EndPointPair + -> RemoteStatePatternMatch a + -> IO a +withRemoteState (ourEndPoint, theirEndPoint) match = + modifyRemoteState (ourEndPoint, theirEndPoint) + RemoteStatePatternMatch + { caseInvalid = \err -> do + a <- caseInvalid match err + return (RemoteEndPointInvalid err, a) + , caseInit = \resolved origin -> do + a <- caseInit match resolved origin + return (RemoteEndPointInit resolved origin, a) + , caseValid = \vst -> do + a <- caseValid match vst + return (RemoteEndPointValid vst, a) + , caseClosing = \resolved vst -> do + a <- caseClosing match resolved vst + return (RemoteEndPointClosing resolved vst, a) + , caseClosed = do + a <- caseClosed match + return (RemoteEndPointClosed, a) + , caseFailed = \err -> do + a <- caseFailed match err + return (RemoteEndPointFailed err, a) + } + +-------------------------------------------------------------------------------- +-- Incoming requests -- +-------------------------------------------------------------------------------- + +-- | Handle a connection request (that is, a remote endpoint that is trying to +-- establish a TCP connection with us) +-- +-- 'handleConnectionRequest' runs in the context of the transport thread, which +-- can be killed asynchronously by 'closeTransport'. We fork a separate thread +-- as soon as we have located the lcoal endpoint that the remote endpoint is +-- interested in. We cannot fork any sooner because then we have no way of +-- storing the thread ID and hence no way of killing the thread when we take +-- the transport down. We must be careful to close the socket when a (possibly +-- asynchronous, ThreadKilled) exception occurs. (If an exception escapes from +-- handleConnectionRequest the transport will be shut down.) +handleConnectionRequest :: TCPTransport -> N.Socket -> IO () +handleConnectionRequest transport sock = handle handleException $ do + ourEndPointId <- recvInt32 sock + theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock + let ourAddress = encodeEndPointAddress (transportHost transport) + (transportPort transport) + ourEndPointId + ourEndPoint <- withMVar (transportState transport) $ \st -> case st of + TransportValid vst -> + case vst ^. localEndPointAt ourAddress of + Nothing -> do + sendMany sock [encodeInt32 ConnectionRequestInvalid] + throwIO $ userError "handleConnectionRequest: Invalid endpoint" + Just ourEndPoint -> + return ourEndPoint + TransportClosed -> + throwIO $ userError "Transport closed" + void . forkIO $ go ourEndPoint theirAddress + where + go :: LocalEndPoint -> EndPointAddress -> IO () + go ourEndPoint theirAddress = do + -- This runs in a thread that will never be killed + mEndPoint <- handle ((>> return Nothing) . handleException) $ do + resetIfBroken ourEndPoint theirAddress + (theirEndPoint, isNew) <- + findRemoteEndPoint ourEndPoint theirAddress RequestedByThem + + if not isNew + then do + tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] + tryCloseSocket sock + return Nothing + else do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 + } + sendMany sock [encodeInt32 ConnectionRequestAccepted] + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) + return (Just theirEndPoint) + -- If we left the scope of the exception handler with a return value of + -- Nothing then the socket is already closed; otherwise, the socket has + -- been recorded as part of the remote endpoint. Either way, we no longer + -- have to worry about closing the socket on receiving an asynchronous + -- exception from this point forward. + forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint + + handleException :: SomeException -> IO () + handleException ex = do + tryCloseSocket sock + rethrowIfAsync (fromException ex) + + rethrowIfAsync :: Maybe AsyncException -> IO () + rethrowIfAsync = mapM_ throwIO + +-- | Handle requests from a remote endpoint. +-- +-- Returns only if the remote party closes the socket or if an error occurs. +-- This runs in a thread that will never be killed. +handleIncomingMessages :: EndPointPair -> IO () +handleIncomingMessages (ourEndPoint, theirEndPoint) = do + mSock <- withMVar theirState $ \st -> + case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages (invalid)" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages (init)" + RemoteEndPointValid ep -> + return . Just $ remoteSocket ep + RemoteEndPointClosing _ ep -> + return . Just $ remoteSocket ep + RemoteEndPointClosed -> + return Nothing + RemoteEndPointFailed _ -> + return Nothing + + forM_ mSock $ \sock -> + tryIO (go sock) >>= either (prematureExit sock) return + where + -- Dispatch + -- + -- If a recv throws an exception this will be caught top-level and + -- 'prematureExit' will be invoked. The same will happen if the remote + -- endpoint is put into a Closed (or Closing) state by a concurrent thread + -- (because a 'send' failed) -- the individual handlers below will throw a + -- user exception which is then caught and handled the same way as an + -- exception thrown by 'recv'. + go :: N.Socket -> IO () + go sock = do + connId <- recvInt32 sock + if connId >= firstNonReservedConnectionId + then do + readMessage sock connId + go sock + else + case tryToEnum (fromIntegral connId) of + Just RequestConnectionId -> do + recvInt32 sock >>= createNewConnection + go sock + Just ControlResponse -> do + recvInt32 sock >>= readControlResponse sock + go sock + Just CloseConnection -> do + recvInt32 sock >>= closeConnection + go sock + Just CloseSocket -> do + didClose <- closeSocket sock + unless didClose $ go sock + Nothing -> + throwIO $ userError "Invalid control request" + + -- Create a new connection + createNewConnection :: ControlRequestId -> IO () + createNewConnection reqId = do + newId <- getNextConnectionId ourEndPoint + modifyMVar_ theirState $ \st -> do + vst <- case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:createNewConnection (invalid)" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:createNewConnection (init)" + RemoteEndPointValid vst -> + return (remoteIncoming ^: IntSet.insert newId $ vst) + RemoteEndPointClosing resolved vst -> do + -- If the endpoint is in closing state that means we send a + -- CloseSocket request to the remote endpoint. If the remote + -- endpoint replies with the request to create a new connection, it + -- either ignored our request or it sent the request before it got + -- ours. Either way, at this point we simply restore the endpoint + -- to RemoteEndPointValid + putMVar resolved () + return (remoteIncoming ^= IntSet.singleton newId $ vst) + RemoteEndPointFailed err -> + throwIO err + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) + "createNewConnection (closed)" + sendOn vst ( encodeInt32 ControlResponse + : encodeInt32 reqId + : prependLength [encodeInt32 newId] + ) + return (RemoteEndPointValid vst) + writeChan ourChannel (ConnectionOpened newId ReliableOrdered theirAddr) + + -- Read a control response + readControlResponse :: N.Socket -> ControlRequestId -> IO () + readControlResponse sock reqId = do + response <- recvWithLength sock + mmvar <- modifyMVar theirState $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) + "readControlResponse (invalid)" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) + "readControlResponse (init)" + RemoteEndPointValid vst -> + return ( RemoteEndPointValid + . (pendingCtrlRequestsAt reqId ^= Nothing) + $ vst + , vst ^. pendingCtrlRequestsAt reqId + ) + RemoteEndPointClosing _ _ -> + throwIO $ userError "Invalid control response" + RemoteEndPointFailed err -> + throwIO err + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) + "readControlResponse (closed)" + case mmvar of + Nothing -> + throwIO $ userError "Invalid request ID" + Just mvar -> + putMVar mvar (Right response) + + -- Close a connection + -- It is important that we verify that the connection is in fact open, + -- because otherwise we should not decrement the reference count + closeConnection :: ConnectionId -> IO () + closeConnection cid = do + modifyMVar_ theirState $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "closeConnection (invalid)" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) "closeConnection (init)" + RemoteEndPointValid vst -> do + unless (IntSet.member cid (vst ^. remoteIncoming)) $ + throwIO $ userError "Invalid CloseConnection" + return ( RemoteEndPointValid + . (remoteIncoming ^: IntSet.delete cid) + $ vst + ) + RemoteEndPointClosing _ _ -> + -- If the remote endpoint is in Closing state, that means that are as + -- far as we are concerned there are no incoming connections. This + -- means that a CloseConnection request at this point is invalid. + throwIO $ userError "Invalid CloseConnection request" + RemoteEndPointFailed err -> + throwIO err + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) "closeConnection (closed)" + writeChan ourChannel (ConnectionClosed cid) + closeIfUnused (ourEndPoint, theirEndPoint) + + -- Close the socket (if we don't have any outgoing connections) + closeSocket :: N.Socket -> IO Bool + closeSocket sock = + modifyMVar theirState $ \st -> + case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:closeSocket (invalid)" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:closeSocket (init)" + RemoteEndPointValid vst -> do + -- We regard a CloseSocket message as an (optimized) way for the + -- remote endpoint to indicate that all its connections to us are + -- now properly closed + forM_ (IntSet.elems $ vst ^. remoteIncoming) $ + writeChan ourChannel . ConnectionClosed + let vst' = remoteIncoming ^= IntSet.empty $ vst + -- Check if we agree that the connection should be closed + if vst' ^. remoteOutgoing == 0 + then do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + -- Attempt to reply (but don't insist) + tryIO $ sendOn vst' [encodeInt32 CloseSocket] + tryCloseSocket sock + return (RemoteEndPointClosed, True) + else + return (RemoteEndPointValid vst', False) + RemoteEndPointClosing resolved _ -> do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + tryCloseSocket sock + putMVar resolved () + return (RemoteEndPointClosed, True) + RemoteEndPointFailed err -> + throwIO err + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:closeSocket (closed)" + + -- Read a message and output it on the endPoint's channel. By rights we + -- should verify that the connection ID is valid, but this is unnecessary + -- overhead + readMessage :: N.Socket -> ConnectionId -> IO () + readMessage sock connId = + recvWithLength sock >>= writeChan ourChannel . Received connId + + -- Arguments + ourChannel = localChannel ourEndPoint + theirState = remoteState theirEndPoint + theirAddr = remoteAddress theirEndPoint + + -- Deal with a premature exit + prematureExit :: N.Socket -> IOException -> IO () + prematureExit sock err = do + tryCloseSocket sock + modifyMVar_ theirState $ \st -> + case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:prematureExit" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:prematureExit" + RemoteEndPointValid vst -> do + let code = EventConnectionLost + (Just $ remoteAddress theirEndPoint) + (IntSet.elems $ vst ^. remoteIncoming) + writeChan ourChannel . ErrorEvent $ TransportError code (show err) + forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) + return (RemoteEndPointFailed err) + RemoteEndPointClosing resolved _ -> do + putMVar resolved () + return (RemoteEndPointFailed err) + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages:prematureExit" + RemoteEndPointFailed err' -> + return (RemoteEndPointFailed err') + +-------------------------------------------------------------------------------- +-- Uninterruptable auxiliary functions -- +-- -- +-- All these functions assume they are running in a thread which will never -- +-- be killed. +-------------------------------------------------------------------------------- + +-- | Request a connection to a remote endpoint +-- +-- This will block until we get a connection ID from the remote endpoint; if +-- the remote endpoint was in 'RemoteEndPointClosing' state then we will +-- additionally block until that is resolved. +-- +-- May throw a TransportError ConnectErrorCode exception. +requestConnectionTo :: TCPParameters + -> LocalEndPoint + -> EndPointAddress + -> ConnectHints + -> IO (RemoteEndPoint, ConnectionId) +requestConnectionTo params ourEndPoint theirAddress hints = go + where + go = do + (theirEndPoint, isNew) <- mapIOException connectFailed $ + findRemoteEndPoint ourEndPoint theirAddress RequestedByUs + + if isNew + then do + forkIO . handle absorbAllExceptions $ + setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints + go + else do + reply <- mapIOException connectFailed $ + doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId + return (theirEndPoint, decodeInt32 . BS.concat $ reply) + + connectFailed :: IOException -> TransportError ConnectErrorCode + connectFailed = TransportError ConnectFailed . show + + absorbAllExceptions :: SomeException -> IO () + absorbAllExceptions _ex = + return () + +-- | Set up a remote endpoint +setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () +setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do + result <- socketToEndPoint ourAddress + theirAddress + (tcpReuseClientAddr params) + (connectTimeout hints) + didAccept <- case result of + Right (sock, ConnectionRequestAccepted) -> do + let vst = ValidRemoteEndPointState + { remoteSocket = sock + , _remoteOutgoing = 0 + , _remoteIncoming = IntSet.empty + , sendOn = sendMany sock + , _pendingCtrlRequests = IntMap.empty + , _nextCtrlRequestId = 0 + } + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) + return True + Right (sock, ConnectionRequestInvalid) -> do + let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + tryCloseSocket sock + return False + Right (sock, ConnectionRequestCrossed) -> do + resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed + tryCloseSocket sock + return False + Left err -> do + resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) + return False + + when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) + where + ourAddress = localAddress ourEndPoint + theirAddress = remoteAddress theirEndPoint + invalidAddress = TransportError ConnectNotFound + +-- | Do a (blocking) remote request +-- +-- May throw IO (user) exception if the local or the remote endpoint is closed, +-- if the send fails, or if the remote endpoint fails before it replies. +doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] +doRemoteRequest (ourEndPoint, theirEndPoint) header = do + replyMVar <- newEmptyMVar + modifyRemoteState_ (ourEndPoint, theirEndPoint) RemoteStatePatternMatch + { caseValid = \vst -> do + let reqId = vst ^. nextCtrlRequestId + sendOn vst [encodeInt32 header, encodeInt32 reqId] + return ( RemoteEndPointValid + . (nextCtrlRequestId ^: (+ 1)) + . (pendingCtrlRequestsAt reqId ^= Just replyMVar) + $ vst + ) + -- Error cases + , caseInvalid = + throwIO + , caseInit = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" + , caseClosing = \_ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" + , caseClosed = + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" + , caseFailed = + throwIO + } + mReply <- takeMVar replyMVar + case mReply of + Left err -> throwIO err + Right reply -> return reply + +-- | Send a CloseSocket request if the remote endpoint is unused +closeIfUnused :: EndPointPair -> IO () +closeIfUnused (ourEndPoint, theirEndPoint) = + modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity + { caseValid = \vst -> + if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + then do + sendOn vst [encodeInt32 CloseSocket] + resolved <- newEmptyMVar + return $ RemoteEndPointClosing resolved vst + else + return $ RemoteEndPointValid vst + } + +-- | Reset a remote endpoint if it is in Invalid mode +-- +-- If the remote endpoint is currently in broken state, and +-- +-- - a user calls the API function 'connect', or and the remote endpoint is +-- - an inbound connection request comes in from this remote address +-- +-- we remove the remote endpoint first. +-- +-- Throws a TransportError ConnectFailed exception if the local endpoint is +-- closed. +resetIfBroken :: LocalEndPoint -> EndPointAddress -> IO () +resetIfBroken ourEndPoint theirAddress = do + mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> + return (vst ^. localConnectionTo theirAddress) + LocalEndPointClosed -> + throwIO $ TransportError ConnectFailed "Endpoint closed" + forM_ mTheirEndPoint $ \theirEndPoint -> + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInvalid _ -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + RemoteEndPointFailed _ -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + _ -> + return () + +-- | Special case of 'apiConnect': connect an endpoint to itself +-- +-- May throw a TransportError ConnectErrorCode (if the local endpoint is closed) +connectToSelf :: LocalEndPoint + -> IO Connection +connectToSelf ourEndPoint = do + connAlive <- newIORef True -- Protected by the local endpoint lock + connId <- mapIOException connectFailed $ getNextConnectionId ourEndPoint + writeChan ourChan $ + ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) + return Connection + { send = selfSend connAlive connId + , close = selfClose connAlive connId + } + where + selfSend :: IORef Bool + -> ConnectionId + -> [ByteString] + -> IO (Either (TransportError SendErrorCode) ()) + selfSend connAlive connId msg = + try . withMVar ourState $ \st -> case st of + LocalEndPointValid _ -> do + alive <- readIORef connAlive + if alive + then writeChan ourChan (Received connId msg) + else throwIO $ TransportError SendClosed "Connection closed" + LocalEndPointClosed -> + throwIO $ TransportError SendFailed "Endpoint closed" + + selfClose :: IORef Bool -> ConnectionId -> IO () + selfClose connAlive connId = + withMVar ourState $ \st -> case st of + LocalEndPointValid _ -> do + alive <- readIORef connAlive + when alive $ do + writeChan ourChan (ConnectionClosed connId) + writeIORef connAlive False + LocalEndPointClosed -> + return () + + ourChan = localChannel ourEndPoint + ourState = localState ourEndPoint + connectFailed = TransportError ConnectFailed . show + +-- | Resolve an endpoint currently in 'Init' state +resolveInit :: EndPointPair -> RemoteState -> IO () +resolveInit (ourEndPoint, theirEndPoint) newState = + modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit resolved _ -> do + putMVar resolved () + case newState of + RemoteEndPointClosed -> + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + _ -> + return () + return newState + RemoteEndPointFailed ex -> + throwIO ex + _ -> + relyViolation (ourEndPoint, theirEndPoint) "resolveInit" + +-- | Get the next connection ID +-- +-- Throws an IO exception when the endpoint is closed. +getNextConnectionId :: LocalEndPoint -> IO ConnectionId +getNextConnectionId ourEndpoint = + modifyMVar (localState ourEndpoint) $ \st -> case st of + LocalEndPointValid vst -> do + let connId = vst ^. nextConnectionId + return ( LocalEndPointValid + . (nextConnectionId ^= connId + 1) + $ vst + , connId) + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + +-- | Create a new local endpoint +-- +-- May throw a TransportError NewEndPointErrorCode exception if the transport +-- is closed. +createLocalEndPoint :: TCPTransport -> IO LocalEndPoint +createLocalEndPoint transport = do + chan <- newChan + state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState + { _nextConnectionId = firstNonReservedConnectionId + , _localConnections = Map.empty + , _nextRemoteId = 0 + } + modifyMVar (transportState transport) $ \st -> case st of + TransportValid vst -> do + let ix = vst ^. nextEndPointId + let addr = encodeEndPointAddress (transportHost transport) + (transportPort transport) + ix + let localEndPoint = LocalEndPoint { localAddress = addr + , localChannel = chan + , localState = state + } + return ( TransportValid + . (localEndPointAt addr ^= Just localEndPoint) + . (nextEndPointId ^= ix + 1) + $ vst + , localEndPoint + ) + TransportClosed -> + throwIO (TransportError NewEndPointFailed "Transport closed") + + +-- | Remove reference to a remote endpoint from a local endpoint +-- +-- If the local endpoint is closed, do nothing +removeRemoteEndPoint :: EndPointPair -> IO () +removeRemoteEndPoint (ourEndPoint, theirEndPoint) = + modifyMVar_ ourState $ \st -> case st of + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Nothing -> + return st + Just remoteEndPoint' -> + if remoteId remoteEndPoint' == remoteId theirEndPoint + then return + ( LocalEndPointValid + . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) + $ vst + ) + else return st + LocalEndPointClosed -> + return LocalEndPointClosed + where + ourState = localState ourEndPoint + theirAddress = remoteAddress theirEndPoint + +-- | Remove reference to a local endpoint from the transport state +-- +-- Does nothing if the transport is closed +removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () +removeLocalEndPoint transport ourEndPoint = + modifyMVar_ (transportState transport) $ \st -> case st of + TransportValid vst -> + return ( TransportValid + . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) + $ vst + ) + TransportClosed -> + return TransportClosed + +-- | Find a remote endpoint. If the remote endpoint does not yet exist we +-- create it in Init state. Returns if the endpoint was new. +findRemoteEndPoint + :: LocalEndPoint + -> EndPointAddress + -> RequestedBy + -> IO (RemoteEndPoint, Bool) +findRemoteEndPoint ourEndPoint theirAddress findOrigin = go + where + go = do + (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of + LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of + Just theirEndPoint -> + return (st, (theirEndPoint, False)) + Nothing -> do + resolved <- newEmptyMVar + theirState <- newMVar (RemoteEndPointInit resolved findOrigin) + let theirEndPoint = RemoteEndPoint + { remoteAddress = theirAddress + , remoteState = theirState + , remoteId = vst ^. nextRemoteId + } + return ( LocalEndPointValid + . (localConnectionTo theirAddress ^= Just theirEndPoint) + . (nextRemoteId ^: (+ 1)) + $ vst + , (theirEndPoint, True) + ) + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + + if isNew + then + return (theirEndPoint, True) + else do + let theirState = remoteState theirEndPoint + snapshot <- modifyMVar theirState $ \st -> case st of + RemoteEndPointValid vst -> + case findOrigin of + RequestedByUs -> do + let st' = RemoteEndPointValid + . (remoteOutgoing ^: (+ 1)) + $ vst + return (st', st') + RequestedByThem -> + return (st, st) + _ -> + return (st, st) + -- The snapshot may no longer be up to date at this point, but if we + -- increased the refcount then it can only either be Valid or Failed + -- (after an explicit call to 'closeEndPoint' or 'closeTransport') + case snapshot of + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointInit resolved initOrigin -> + case (findOrigin, initOrigin) of + (RequestedByUs, RequestedByUs) -> + readMVar resolved >> go + (RequestedByUs, RequestedByThem) -> + readMVar resolved >> go + (RequestedByThem, RequestedByUs) -> + if ourAddress > theirAddress + then + -- Wait for the Crossed message + readMVar resolved >> go + else + return (theirEndPoint, False) + (RequestedByThem, RequestedByThem) -> + throwIO $ userError "Already connected" + RemoteEndPointValid _ -> + -- We assume that the request crossed if we find the endpoint in + -- Valid state. It is possible that this is really an invalid + -- request, but only in the case of a broken client (we don't + -- maintain enough history to be able to tell the difference). + return (theirEndPoint, False) + RemoteEndPointClosing resolved _ -> + readMVar resolved >> go + RemoteEndPointClosed -> + go + RemoteEndPointFailed err -> + throwIO err + + ourState = localState ourEndPoint + ourAddress = localAddress ourEndPoint + +-------------------------------------------------------------------------------- +-- "Stateless" (MVar free) functions -- +-------------------------------------------------------------------------------- + +-- | Establish a connection to a remote endpoint +-- +-- Maybe throw a TransportError +socketToEndPoint :: EndPointAddress -- ^ Our address + -> EndPointAddress -- ^ Their address + -> Bool -- ^ Use SO_REUSEADDR? + -> Maybe Int -- ^ Timeout for connect + -> IO (Either (TransportError ConnectErrorCode) + (N.Socket, ConnectionRequestResponse)) +socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = + try $ do + (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of + Nothing -> throwIO (failed . userError $ "Could not parse") + Just dec -> return dec + addr:_ <- mapIOException invalidAddress $ + N.getAddrInfo Nothing (Just host) (Just port) + bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do + when reuseAddr $ + mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 + mapIOException invalidAddress $ + timeoutMaybe timeout timeoutError $ + N.connect sock (N.addrAddress addr) + response <- mapIOException failed $ do + sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) + recvInt32 sock + case tryToEnum response of + Nothing -> throwIO (failed . userError $ "Unexpected response") + Just r -> return (sock, r) + where + createSocket :: N.AddrInfo -> IO N.Socket + createSocket addr = mapIOException insufficientResources $ + N.socket (N.addrFamily addr) N.Stream N.defaultProtocol + + invalidAddress = TransportError ConnectNotFound . show + insufficientResources = TransportError ConnectInsufficientResources . show + failed = TransportError ConnectFailed . show + timeoutError = TransportError ConnectTimeout "Timed out" + +-- | Encode end point address +encodeEndPointAddress :: N.HostName + -> N.ServiceName + -> EndPointId + -> EndPointAddress +encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ + host ++ ":" ++ port ++ ":" ++ show ix + +-- | Decode end point address +decodeEndPointAddress :: EndPointAddress + -> Maybe (N.HostName, N.ServiceName, EndPointId) +decodeEndPointAddress (EndPointAddress bs) = + case map BSC.unpack $ BSC.split ':' bs of + [host, port, endPointIdStr] -> + case reads endPointIdStr of + [(endPointId, "")] -> Just (host, port, endPointId) + _ -> Nothing + _ -> + Nothing + +-------------------------------------------------------------------------------- +-- Functions from TransportInternals -- +-------------------------------------------------------------------------------- + +-- Find a socket between two endpoints +-- +-- Throws an IO exception if the socket could not be found. +internalSocketBetween :: TCPTransport -- ^ Transport + -> EndPointAddress -- ^ Local endpoint + -> EndPointAddress -- ^ Remote endpoint + -> IO N.Socket +internalSocketBetween transport ourAddress theirAddress = do + ourEndPoint <- withMVar (transportState transport) $ \st -> case st of + TransportClosed -> + throwIO $ userError "Transport closed" + TransportValid vst -> + case vst ^. localEndPointAt ourAddress of + Nothing -> throwIO $ userError "Local endpoint not found" + Just ep -> return ep + theirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + LocalEndPointValid vst -> + case vst ^. localConnectionTo theirAddress of + Nothing -> throwIO $ userError "Remote endpoint not found" + Just ep -> return ep + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit _ _ -> + throwIO $ userError "Remote endpoint not yet initialized" + RemoteEndPointValid vst -> + return $ remoteSocket vst + RemoteEndPointClosing _ vst -> + return $ remoteSocket vst + RemoteEndPointClosed -> + throwIO $ userError "Remote endpoint closed" + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointFailed err -> + throwIO err + +-------------------------------------------------------------------------------- +-- Constants -- +-------------------------------------------------------------------------------- + +-- | We reserve a bunch of connection IDs for control messages +firstNonReservedConnectionId :: ConnectionId +firstNonReservedConnectionId = 1024 + +-------------------------------------------------------------------------------- +-- Accessor definitions -- +-------------------------------------------------------------------------------- + +localEndPoints :: Accessor ValidTransportState (Map EndPointAddress LocalEndPoint) +localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es }) + +nextEndPointId :: Accessor ValidTransportState EndPointId +nextEndPointId = accessor _nextEndPointId (\eid st -> st { _nextEndPointId = eid }) + +nextConnectionId :: Accessor ValidLocalEndPointState ConnectionId +nextConnectionId = accessor _nextConnectionId (\cix st -> st { _nextConnectionId = cix }) + +localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) +localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) + +nextRemoteId :: Accessor ValidLocalEndPointState Int +nextRemoteId = accessor _nextRemoteId (\rid st -> st { _nextRemoteId = rid }) + +remoteOutgoing :: Accessor ValidRemoteEndPointState Int +remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs }) + +remoteIncoming :: Accessor ValidRemoteEndPointState IntSet +remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) + +pendingCtrlRequests :: Accessor ValidRemoteEndPointState (IntMap (MVar (Either IOException [ByteString]))) +pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) + +nextCtrlRequestId :: Accessor ValidRemoteEndPointState ControlRequestId +nextCtrlRequestId = accessor _nextCtrlRequestId (\cid st -> st { _nextCtrlRequestId = cid }) + +localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) +localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr + +pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidRemoteEndPointState (Maybe (MVar (Either IOException [ByteString]))) +pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral ix) + +localConnectionTo :: EndPointAddress + -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) +localConnectionTo addr = localConnections >>> DAC.mapMaybe addr + +------------------------------------------------------------------------------- +-- Debugging -- +------------------------------------------------------------------------------- + +relyViolation :: EndPointPair -> String -> IO a +relyViolation (ourEndPoint, theirEndPoint) str = do + elog (ourEndPoint, theirEndPoint) (str ++ " RELY violation") + fail (str ++ " RELY violation") + +elog :: EndPointPair -> String -> IO () +elog (ourEndPoint, theirEndPoint) msg = do + tid <- myThreadId + putStrLn $ show (localAddress ourEndPoint) + ++ "/" ++ show (remoteAddress theirEndPoint) + ++ "(" ++ show (remoteId theirEndPoint) ++ ")" + ++ "/" ++ show tid + ++ ": " ++ msg diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs new file mode 100644 index 00000000..eabcf0a6 --- /dev/null +++ b/src/Network/Transport/TCP/Internal.hs @@ -0,0 +1,116 @@ +-- | Utility functions for TCP sockets +module Network.Transport.TCP.Internal ( forkServer + , recvWithLength + , recvExact + , recvInt32 + , tryCloseSocket + ) where + +import Prelude hiding (catch) +import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) +import qualified Network.Socket as N ( HostName + , ServiceName + , Socket + , SocketType(Stream) + , SocketOption(ReuseAddr) + , getAddrInfo + , defaultHints + , socket + , bindSocket + , listen + , addrFamily + , addrAddress + , defaultProtocol + , setSocketOption + , accept + , sClose + ) +import qualified Network.Socket.ByteString as NBS (recv) +import Control.Concurrent (ThreadId) +import Control.Monad (forever, when) +import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) +import Control.Applicative ((<$>)) +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS (length, concat, null) +import Data.Int (Int32) + +-- | Start a server at the specified address. +-- +-- This sets up a server socket for the specified host and port. Exceptions +-- thrown during setup are not caught. +-- +-- Once the socket is created we spawn a new thread which repeatedly accepts +-- incoming connections and executes the given request handler. If any +-- exception occurs the thread terminates and calls the terminationHandler. +-- This exception may occur because of a call to 'N.accept', because the thread +-- was explicitly killed, or because of a synchronous exception thrown by the +-- request handler. Typically, you should avoid the last case by catching any +-- relevant exceptions in the request handler. +-- +-- The request handler should spawn threads to handle each individual request +-- or the server will block. Once a thread has been spawned it will be the +-- responsibility of the new thread to close the socket when an exception +-- occurs. +forkServer :: N.HostName -- ^ Host + -> N.ServiceName -- ^ Port + -> Int -- ^ Backlog (maximum number of queued connections) + -> Bool -- ^ Set ReuseAddr option? + -> (SomeException -> IO ()) -- ^ Termination handler + -> (N.Socket -> IO ()) -- ^ Request handler + -> IO ThreadId +forkServer host port backlog reuseAddr terminationHandler requestHandler = do + -- Resolve the specified address. By specification, getAddrInfo will never + -- return an empty list (but will throw an exception instead) and will return + -- the "best" address first, whatever that means + addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just host) (Just port) + bracketOnError (N.socket (N.addrFamily addr) N.Stream N.defaultProtocol) + tryCloseSocket $ \sock -> do + when reuseAddr $ N.setSocketOption sock N.ReuseAddr 1 + N.bindSocket sock (N.addrAddress addr) + N.listen sock backlog + -- We start listening for incoming requests in a separate thread. When + -- that thread is killed, we close the server socket and the termination + -- handler. We have to make sure that the exception handler is installed + -- /before/ any asynchronous exception occurs. So we mask_, then fork + -- (the child thread inherits the masked state from the parent), then + -- unmask only inside the catch. + mask_ $ forkIOWithUnmask $ \unmask -> + catch (unmask (forever $ acceptRequest sock)) $ \ex -> do + tryCloseSocket sock + terminationHandler ex + where + acceptRequest :: N.Socket -> IO () + acceptRequest sock = bracketOnError (N.accept sock) + (tryCloseSocket . fst) + (requestHandler . fst) + +-- | Read a length and then a payload of that length +recvWithLength :: N.Socket -> IO [ByteString] +recvWithLength sock = recvInt32 sock >>= recvExact sock + +-- | Receive a 32-bit integer +recvInt32 :: Num a => N.Socket -> IO a +recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4 + +-- | Close a socket, ignoring I/O exceptions +tryCloseSocket :: N.Socket -> IO () +tryCloseSocket sock = void . tryIO $ + N.sClose sock + +-- | Read an exact number of bytes from a socket +-- +-- Throws an I/O exception if the socket closes before the specified +-- number of bytes could be read +recvExact :: N.Socket -- ^ Socket to read from + -> Int32 -- ^ Number of bytes to read + -> IO [ByteString] +recvExact _ len | len <= 0 = throwIO (userError "recvExact: Negative length") +recvExact sock len = go [] len + where + go :: [ByteString] -> Int32 -> IO [ByteString] + go acc 0 = return (reverse acc) + go acc l = do + bs <- NBS.recv sock (fromIntegral l `min` 4096) + if BS.null bs + then throwIO (userError "recvExact: Socket closed") + else go (bs : acc) (l - fromIntegral (BS.length bs)) diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs new file mode 100644 index 00000000..d912ee6e --- /dev/null +++ b/tests/TestAuxiliary.hs @@ -0,0 +1,108 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module TestAuxiliary ( -- Running tests + runTest + , runTests + -- Writing tests + , forkTry + , trySome + , randomThreadDelay + ) where + +import Prelude hiding (catch) +import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) +import Control.Concurrent.Chan (Chan) +import Control.Monad (liftM2, unless) +import Control.Exception (SomeException, try, catch) +import System.Timeout (timeout) +import System.IO (stdout, hFlush) +import System.Console.ANSI ( SGR(SetColor, Reset) + , Color(Red, Green) + , ConsoleLayer(Foreground) + , ColorIntensity(Vivid) + , setSGR + ) +import System.Random (randomIO) +import Network.Transport +import Traced (Traceable(..), traceShow) + +-- | Like fork, but throw exceptions in the child thread to the parent +forkTry :: IO () -> IO ThreadId +forkTry p = do + tid <- myThreadId + forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) + +-- | Like try, but specialized to SomeException +trySome :: IO a -> IO (Either SomeException a) +trySome = try + +-- | Run the given test, catching timeouts and exceptions +runTest :: String -> IO () -> IO Bool +runTest description test = do + putStr $ "Running " ++ show description ++ ": " + hFlush stdout + done <- try . timeout 60000000 $ test -- 60 seconds + case done of + Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" + Right Nothing -> failed $ "(timeout)" + Right (Just ()) -> ok + where + failed :: String -> IO Bool + failed err = do + setSGR [SetColor Foreground Vivid Red] + putStr "failed " + setSGR [Reset] + putStrLn err + return False + + ok :: IO Bool + ok = do + setSGR [SetColor Foreground Vivid Green] + putStrLn "ok" + setSGR [Reset] + return True + +-- | Run a bunch of tests and throw an exception if any fails +runTests :: [(String, IO ())] -> IO () +runTests tests = do + success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests + unless success $ fail "Some tests failed" + +-- | Random thread delay between 0 and the specified max +randomThreadDelay :: Int -> IO () +randomThreadDelay maxDelay = do + delay <- randomIO :: IO Int + threadDelay (delay `mod` maxDelay) + +-------------------------------------------------------------------------------- +-- traceShow instances -- +-------------------------------------------------------------------------------- + +instance Traceable EndPoint where + trace = const Nothing + +instance Traceable Transport where + trace = const Nothing + +instance Traceable Connection where + trace = const Nothing + +instance Traceable Event where + trace = traceShow + +instance Show err => Traceable (TransportError err) where + trace = traceShow + +instance Traceable EndPointAddress where + trace = traceShow . endPointAddressToByteString + +instance Traceable SomeException where + trace = traceShow + +instance Traceable ThreadId where + trace = const Nothing + +instance Traceable (Chan a) where + trace = const Nothing + +instance Traceable Float where + trace = traceShow diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs new file mode 100644 index 00000000..fcc387e5 --- /dev/null +++ b/tests/TestTCP.hs @@ -0,0 +1,786 @@ +{-# LANGUAGE RebindableSyntax #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Main where + +import Prelude hiding (catch, (>>=), (>>), return, fail) +import TestTransport (testTransport) +import TestAuxiliary (forkTry, runTests) +import Network.Transport +import Network.Transport.TCP ( createTransport + , createTransportExposeInternals + , TransportInternals(..) + , encodeEndPointAddress + , defaultTCPParameters + ) +import Data.Int (Int32) +import Control.Concurrent (threadDelay, killThread) +import Control.Concurrent.MVar ( MVar + , newEmptyMVar + , putMVar + , takeMVar + , readMVar + , isEmptyMVar + , newMVar + , modifyMVar + ) +import Control.Monad (replicateM, guard, forM_, replicateM_, when) +import Control.Applicative ((<$>)) +import Control.Exception (throwIO, try, SomeException) +import Network.Transport.TCP ( ControlHeader(..) + , ConnectionRequestResponse(..) + , socketToEndPoint + ) +import Network.Transport.Internal ( encodeInt32 + , prependLength + , tlog + , tryIO + , void + ) +import Network.Transport.TCP.Internal (recvInt32, forkServer, recvWithLength) +import qualified Network.Socket as N ( sClose + , ServiceName + , Socket + , AddrInfo + , shutdown + , ShutdownCmd(ShutdownSend) + ) +import Network.Socket.ByteString (sendMany) +import Data.String (fromString) +import Traced +import GHC.IO.Exception (ioe_errno) +import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) +import System.Timeout (timeout) + +instance Traceable ControlHeader where + trace = traceShow + +instance Traceable ConnectionRequestResponse where + trace = traceShow + +instance Traceable N.Socket where + trace = traceShow + +instance Traceable N.AddrInfo where + trace = traceShow + +instance Traceable TransportInternals where + trace = const Nothing + +-- Test that the server gets a ConnectionClosed message when the client closes +-- the socket without sending an explicit control message to the server first +testEarlyDisconnect :: IO N.ServiceName -> IO () +testEarlyDisconnect nextPort = do + clientAddr <- newEmptyMVar + serverAddr <- newEmptyMVar + serverDone <- newEmptyMVar + + tlog "testEarlyDisconnect" + forkTry $ server serverAddr clientAddr serverDone + forkTry $ client serverAddr clientAddr + + takeMVar serverDone + where + server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () + server serverAddr clientAddr serverDone = do + tlog "Server" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + theirAddr <- readMVar clientAddr + + -- TEST 1: they connect to us, then drop the connection + do + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid']) _) <- receive endpoint + True <- return $ addr' == theirAddr && cid' == cid + + return () + + -- TEST 2: after they dropped their connection to us, we now try to + -- establish a connection to them. This should re-establish the broken + -- TCP connection. + tlog "Trying to connect to client" + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- TEST 3: To test the connection, we do a simple ping test; as before, + -- however, the remote client won't close the connection nicely but just + -- closes the socket + do + Right () <- send conn ["ping"] + + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + Received cid' ["pong"] <- receive endpoint + True <- return $ cid == cid' + + ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid'']) _) <- receive endpoint + True <- return $ addr' == theirAddr && cid'' == cid + + return () + + -- TEST 4: A subsequent send on an already-open connection will now break + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- *Pfew* + putMVar serverDone () + + client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () + client serverAddr clientAddr = do + tlog "Client" + clientPort <- nextPort + let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 + putMVar clientAddr ourAddress + + -- Listen for incoming messages + forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do + -- Initial setup + 0 <- recvInt32 sock :: IO Int + _ <- recvWithLength sock + sendMany sock [encodeInt32 ConnectionRequestAccepted] + + -- Server requests a logical connection + RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) + reqId <- recvInt32 sock :: IO Int + sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + + -- Server sends a message + 10001 <- recvInt32 sock :: IO Int + ["ping"] <- recvWithLength sock + + -- Reply + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] + ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) + 10002 <- recvInt32 sock :: IO Int + [cid] <- recvWithLength sock + sendMany sock (cid : prependLength ["pong"]) + + -- Close the socket + N.sClose sock + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + + -- Request a new connection, but don't wait for the response + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + + -- Close the socket without closing the connection explicitly + -- The server should receive an error event + N.sClose sock + +-- | Test the behaviour of a premature CloseSocket request +testEarlyCloseSocket :: IO N.ServiceName -> IO () +testEarlyCloseSocket nextPort = do + clientAddr <- newEmptyMVar + serverAddr <- newEmptyMVar + serverDone <- newEmptyMVar + + tlog "testEarlyDisconnect" + forkTry $ server serverAddr clientAddr serverDone + forkTry $ client serverAddr clientAddr + + takeMVar serverDone + where + server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () + server serverAddr clientAddr serverDone = do + tlog "Server" + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + theirAddr <- readMVar clientAddr + + -- TEST 1: they connect to us, then send a CloseSocket. Since we don't + -- have any outgoing connections, this means we will agree to close the + -- socket + do + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + ConnectionClosed cid' <- receive endpoint + True <- return $ cid' == cid + + return () + + -- TEST 2: after they dropped their connection to us, we now try to + -- establish a connection to them. This should re-establish the broken + -- TCP connection. + tlog "Trying to connect to client" + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- TEST 3: To test the connection, we do a simple ping test; as before, + -- however, the remote client won't close the connection nicely but just + -- sends a CloseSocket -- except that now we *do* have outgoing + -- connections, so we won't agree and hence will receive an error when + -- the socket gets closed + do + Right () <- send conn ["ping"] + + ConnectionOpened cid _ addr <- receive endpoint + True <- return $ addr == theirAddr + + Received cid' ["pong"] <- receive endpoint + True <- return $ cid' == cid + + ConnectionClosed cid'' <- receive endpoint + True <- return $ cid'' == cid + + ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint + True <- return $ addr' == theirAddr + + return () + + -- TEST 4: A subsequent send on an already-open connection will now break + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- *Pfew* + putMVar serverDone () + + client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () + client serverAddr clientAddr = do + tlog "Client" + clientPort <- nextPort + let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 + putMVar clientAddr ourAddress + + -- Listen for incoming messages + forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do + -- Initial setup + 0 <- recvInt32 sock :: IO Int + _ <- recvWithLength sock + sendMany sock [encodeInt32 ConnectionRequestAccepted] + + -- Server requests a logical connection + RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) + reqId <- recvInt32 sock :: IO Int + sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + + -- Server sends a message + 10001 <- recvInt32 sock :: IO Int + ["ping"] <- recvWithLength sock + + -- Reply + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] + ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) + 10002 <- recvInt32 sock :: IO Int + [cid] <- recvWithLength sock + sendMany sock (cid : prependLength ["pong"]) + + -- Send a CloseSocket even though there are still connections *in both + -- directions* + sendMany sock [encodeInt32 CloseSocket] + N.sClose sock + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + + -- Request a new connection, but don't wait for the response + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + + -- Send a CloseSocket without sending a closeconnecton + -- The server should still receive a ConnectionClosed message + sendMany sock [encodeInt32 CloseSocket] + N.sClose sock + +-- | Test the creation of a transport with an invalid address +testInvalidAddress :: IO N.ServiceName -> IO () +testInvalidAddress nextPort = do + Left _ <- nextPort >>= \port -> createTransport "invalidHostName" port defaultTCPParameters + return () + +-- | Test connecting to invalid or non-existing endpoints +testInvalidConnect :: IO N.ServiceName -> IO () +testInvalidConnect nextPort = do + port <- nextPort + Right transport <- createTransport "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + + -- Syntax error in the endpoint address + Left (TransportError ConnectFailed _) <- + connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered defaultConnectHints + + -- Syntax connect, but invalid hostname (TCP address lookup failure) + Left (TransportError ConnectNotFound _) <- + connect endpoint (encodeEndPointAddress "invalidHost" "port" 0) ReliableOrdered defaultConnectHints + + -- TCP address correct, but nobody home at that address + Left (TransportError ConnectNotFound _) <- + connect endpoint (encodeEndPointAddress "127.0.0.1" "9000" 0) ReliableOrdered defaultConnectHints + + -- Valid TCP address but invalid endpoint number + Left (TransportError ConnectNotFound _) <- + connect endpoint (encodeEndPointAddress "127.0.0.1" port 1) ReliableOrdered defaultConnectHints + + return () + +-- | Test that an endpoint can ignore CloseSocket requests (in "reality" this +-- would happen when the endpoint sends a new connection request before +-- receiving an (already underway) CloseSocket request) +testIgnoreCloseSocket :: IO N.ServiceName -> IO () +testIgnoreCloseSocket nextPort = do + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + + forkTry $ server transport serverAddr + forkTry $ client transport serverAddr clientDone + + takeMVar clientDone + + where + server :: Transport -> MVar EndPointAddress -> IO () + server transport serverAddr = do + tlog "Server" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Wait for the client to connect and disconnect + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint + + -- At this point the server will have sent a CloseSocket request to the + -- client, which however ignores it, instead it requests and closes + -- another connection + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint + + + tlog "Server waiting.." + + client :: Transport -> MVar EndPointAddress -> MVar () -> IO () + client transport serverAddr clientDone = do + tlog "Client" + Right endpoint <- newEndPoint transport + let ourAddress = address endpoint + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + + -- Request a new connection + tlog "Requesting connection" + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + response <- replicateM 4 $ recvInt32 sock :: IO [Int32] + + -- Close the connection again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] + + -- Server will now send a CloseSocket request as its refcount reached 0 + tlog "Waiting for CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + + -- But we ignore it and request another connection + tlog "Ignoring it, requesting another connection" + let reqId' = 1 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] + response' <- replicateM 4 $ recvInt32 sock :: IO [Int32] + + -- Close it again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response' !! 3)] + + -- We now get a CloseSocket again, and this time we heed it + tlog "Waiting for second CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + + tlog "Closing socket" + sendMany sock [encodeInt32 CloseSocket] + N.sClose sock + + putMVar clientDone () + +-- | Like 'testIgnoreSocket', but now the server requests a connection after the +-- client closed their connection. In the meantime, the server will have sent a +-- CloseSocket request to the client, and must block until the client responds. +testBlockAfterCloseSocket :: IO N.ServiceName -> IO () +testBlockAfterCloseSocket nextPort = do + serverAddr <- newEmptyMVar + clientAddr <- newEmptyMVar + clientDone <- newEmptyMVar + port <- nextPort + Right transport <- createTransport "127.0.0.1" port defaultTCPParameters + + forkTry $ server transport serverAddr clientAddr + forkTry $ client transport serverAddr clientAddr clientDone + + takeMVar clientDone + + where + server :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> IO () + server transport serverAddr clientAddr = do + tlog "Server" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Wait for the client to connect and disconnect + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint + + -- At this point the server will have sent a CloseSocket request to the + -- client, and must block until the client responds + tlog "Server waiting to connect to the client.." + Right _ <- readMVar clientAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + tlog "Server waiting.." + + client :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () + client transport serverAddr clientAddr clientDone = do + tlog "Client" + Right endpoint <- newEndPoint transport + putMVar clientAddr (address endpoint) + let ourAddress = address endpoint + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + + -- Request a new connection + tlog "Requesting connection" + let reqId = 0 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + response <- replicateM 4 $ recvInt32 sock :: IO [Int32] + + -- Close the connection again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] + + -- Server will now send a CloseSocket request as its refcount reached 0 + tlog "Waiting for CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + + unblocked <- newEmptyMVar + + -- We should not hear from the server until we unblock him by + -- responding to the CloseSocket request (in this case, we + -- respond by sending a ConnectionRequest) + forkTry $ do + recvInt32 sock :: IO Int32 + isEmptyMVar unblocked >>= (guard . not) + putMVar clientDone () + + threadDelay 1000000 + + tlog "Client ignores close socket and sends connection request" + tlog "This should unblock the server" + putMVar unblocked () + let reqId' = 1 :: Int32 + sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] + +-- | Test what happens when a remote endpoint sends a connection request to our +-- transport for an endpoint it already has a connection to +testUnnecessaryConnect :: IO N.ServiceName -> Int -> IO () +testUnnecessaryConnect nextPort numThreads = do + clientDone <- newEmptyMVar + serverAddr <- newEmptyMVar + + forkTry $ do + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + forkTry $ do + -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check + let ourAddress = EndPointAddress "126.0.0.1" + + -- We should only get a single 'Accepted' reply + gotAccepted <- newEmptyMVar + dones <- replicateM numThreads $ do + done <- newEmptyMVar + forkTry $ do + -- It is possible that the remote endpoint just rejects the request by closing the socket + -- immediately (depending on far the remote endpoint got with the initialization) + response <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + case response of + Right (_, ConnectionRequestAccepted) -> + -- We don't close this socket because we want to keep this connection open + putMVar gotAccepted () + -- We might get either Invalid or Crossed (the transport does not + -- maintain enough history to be able to tell) + Right (sock, ConnectionRequestInvalid) -> + N.sClose sock + Right (sock, ConnectionRequestCrossed) -> + N.sClose sock + Left _ -> + return () + putMVar done () + return done + + mapM_ readMVar (gotAccepted : dones) + putMVar clientDone () + + takeMVar clientDone + +-- | Test that we can create "many" transport instances +testMany :: IO N.ServiceName -> IO () +testMany nextPort = do + Right masterTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + Right masterEndPoint <- newEndPoint masterTransport + + replicateM_ 10 $ do + mTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + case mTransport of + Left ex -> do + putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex) + case (ioe_errno ex) of + Just no | Errno no == eADDRNOTAVAIL -> putStrLn "(ADDRNOTAVAIL)" + _ -> return () + throwIO ex + Right transport -> + replicateM_ 2 $ do + Right endpoint <- newEndPoint transport + Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered defaultConnectHints + return () + +-- | Test what happens when the transport breaks completely +testBreakTransport :: IO N.ServiceName -> IO () +testBreakTransport nextPort = do + Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + + killThread (transportThread internals) -- Uh oh + + ErrorEvent (TransportError EventTransportFailed _) <- receive endpoint + + return () + +-- | Test that a second call to 'connect' might succeed even if the first +-- failed. This is a TCP specific test rather than an endpoint specific test +-- because we must manually create the endpoint address to match an endpoint we +-- have yet to set up. +-- Then test that we get a connection lost message after the remote endpoint +-- suddenly closes the socket, and that a subsequent 'connect' allows us to +-- re-establish a connection to the same endpoint +testReconnect :: IO N.ServiceName -> IO () +testReconnect nextPort = do + serverPort <- nextPort + serverDone <- newEmptyMVar + firstAttempt <- newEmptyMVar + endpointCreated <- newEmptyMVar + + -- Server + forkTry $ do + -- Wait for the client to do its first attempt + readMVar firstAttempt + + counter <- newMVar (0 :: Int) + + forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do + -- Accept the connection + Right 0 <- tryIO $ (recvInt32 sock :: IO Int) + Right _ <- tryIO $ recvWithLength sock + Right () <- tryIO $ sendMany sock [encodeInt32 ConnectionRequestAccepted] + + -- The first time we close the socket before accepting the logical connection + count <- modifyMVar counter $ \i -> return (i + 1, i) + + when (count > 0) $ do + -- Client requests a logical connection + Right RequestConnectionId <- tryIO $ toEnum <$> (recvInt32 sock :: IO Int) + Right reqId <- tryIO $ (recvInt32 sock :: IO Int) + Right () <- tryIO $ sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + return () + + when (count > 1) $ do + -- Client sends a message + Right 10001 <- tryIO $ (recvInt32 sock :: IO Int) + Right ["ping"] <- tryIO $ recvWithLength sock + putMVar serverDone () + + Right () <- tryIO $ N.sClose sock + return () + + putMVar endpointCreated () + + -- Client + forkTry $ do + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 + + -- The first attempt will fail because no endpoint is yet set up + -- Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + putMVar firstAttempt () + + -- The second attempt will fail because the server closes the socket before we can request a connection + takeMVar endpointCreated + -- This might time out or not, depending on whether the server closes the + -- socket before or after we can send the RequestConnectionId request + resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints + case resultConnect of + Nothing -> return () + Just (Left (TransportError ConnectFailed _)) -> return () + Just (Left err) -> throwIO err + Just (Right _) -> throwIO $ userError "testConnect: unexpected connect success" + + -- The third attempt succeeds + Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- But a send will fail because the server has closed the connection again + Left (TransportError SendFailed _) <- send conn1 ["ping"] + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + + -- But a subsequent call to connect should reestablish the connection + Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- Send should now succeed + Right () <- send conn2 ["ping"] + return () + + takeMVar serverDone + +-- Test what happens if we close the socket one way only. This means that the +-- 'recv' in 'handleIncomingMessages' will not fail, but a 'send' or 'connect' +-- *will* fail. We are testing that error handling everywhere does the right +-- thing. +testUnidirectionalError :: IO N.ServiceName -> IO () +testUnidirectionalError nextPort = do + clientDone <- newEmptyMVar + serverPort <- nextPort + serverGotPing <- newEmptyMVar + + -- Server + forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do + -- We accept connections, but when an exception occurs we don't do + -- anything (in particular, we don't close the socket). This is important + -- because when we shutdown one direction of the socket a recv here will + -- fail, but we don't want to close that socket at that point (which + -- would shutdown the socket in the other direction) + void . (try :: IO () -> IO (Either SomeException ())) $ do + 0 <- recvInt32 sock :: IO Int + _ <- recvWithLength sock + () <- sendMany sock [encodeInt32 ConnectionRequestAccepted] + + RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) + reqId <- recvInt32 sock :: IO Int + sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + + 10001 <- recvInt32 sock :: IO Int + ["ping"] <- recvWithLength sock + putMVar serverGotPing () + + -- Client + forkTry $ do + Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters + Right endpoint <- newEndPoint transport + let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 + + -- Establish a connection to the server + Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn1 ["ping"] + takeMVar serverGotPing + + -- Close the *outgoing* part of the socket only + sock <- socketBetween internals (address endpoint) theirAddr + N.shutdown sock N.ShutdownSend + + -- At this point we cannot notice the problem yet so we shouldn't receive an event yet + Nothing <- timeout 500000 $ receive endpoint + + -- But when we send we find the error + Left (TransportError SendFailed _) <- send conn1 ["ping"] + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + + -- A call to connect should now re-establish the connection + Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn2 ["ping"] + takeMVar serverGotPing + + -- Again, close the outgoing part of the socket + sock' <- socketBetween internals (address endpoint) theirAddr + N.shutdown sock' N.ShutdownSend + + -- We now find the error when we attempt to close the connection + Nothing <- timeout 500000 $ receive endpoint + close conn2 + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + Right conn3 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn3 ["ping"] + takeMVar serverGotPing + + -- We repeat once more. + sock'' <- socketBetween internals (address endpoint) theirAddr + N.shutdown sock'' N.ShutdownSend + + -- Now we notice the problem when we try to connect + Nothing <- timeout 500000 $ receive endpoint + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + Right conn4 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn4 ["ping"] + takeMVar serverGotPing + + putMVar clientDone () + + takeMVar clientDone + +testInvalidCloseConnection :: IO N.ServiceName -> IO () +testInvalidCloseConnection nextPort = do + Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + serverDone <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + ConnectionOpened _ _ _ <- receive endpoint + + -- At this point the client sends an invalid request, so we terminate the + -- connection + ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint + + putMVar serverDone () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + let ourAddr = address endpoint + + -- Connect so that we have a TCP connection + theirAddr <- readMVar serverAddr + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- Get a handle on the TCP connection and manually send an invalid CloseConnection request + sock <- socketBetween internals ourAddr theirAddr + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (12345 :: Int)] + + putMVar clientDone () + + mapM_ takeMVar [clientDone, serverDone] + +main :: IO () +main = do + portMVar <- newEmptyMVar + forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show + let nextPort = takeMVar portMVar + tcpResult <- tryIO $ runTests + [ ("EarlyDisconnect", testEarlyDisconnect nextPort) + , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) + , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) + , ("BlockAfterCloseSocket", testBlockAfterCloseSocket nextPort) + , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort 10) + , ("InvalidAddress", testInvalidAddress nextPort) + , ("InvalidConnect", testInvalidConnect nextPort) + , ("Many", testMany nextPort) + , ("BreakTransport", testBreakTransport nextPort) + , ("Reconnect", testReconnect nextPort) + , ("UnidirectionalError", testUnidirectionalError nextPort) + , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) + ] + -- Run the generic tests even if the TCP specific tests failed.. + testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) + -- ..but if the generic tests pass, still fail if the specific tests did not + case tcpResult of + Left err -> throwIO err + Right () -> return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs new file mode 100644 index 00000000..e528e327 --- /dev/null +++ b/tests/TestTransport.hs @@ -0,0 +1,956 @@ +{-# LANGUAGE RebindableSyntax #-} +module TestTransport where + +import Prelude hiding (catch, (>>=), (>>), return, fail) +import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) +import Control.Concurrent (forkIO, killThread, yield) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) +import Control.Exception (evaluate, throw, throwIO, bracket) +import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) +import Control.Monad.Error () +import Control.Applicative ((<$>)) +import Network.Transport +import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) +import Network.Transport.Util (spawn) +import System.Random (randomIO) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack) +import Data.Map (Map) +import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) +import Data.String (fromString) +import Data.List (permutations) +import Traced + +-- | Server that echoes messages straight back to the origin endpoint. +echoServer :: EndPoint -> IO () +echoServer endpoint = do + go Map.empty + where + go :: Map ConnectionId Connection -> IO () + go cs = do + event <- receive endpoint + case event of + ConnectionOpened cid rel addr -> do + tlog $ "Opened new connection " ++ show cid + Right conn <- connect endpoint addr rel defaultConnectHints + go (Map.insert cid conn cs) + Received cid payload -> do + send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload + go cs + ConnectionClosed cid -> do + tlog $ "Close connection " ++ show cid + close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) + go (Map.delete cid cs) + ReceivedMulticast _ _ -> + -- Ignore + go cs + ErrorEvent _ -> + putStrLn $ "Echo server received error event: " ++ show event + EndPointClosed -> + return () + +-- | Ping client used in a few tests +ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () +ping endpoint server numPings msg = do + -- Open connection to the server + tlog "Connect to echo server" + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Wait for the server to open reply connection + tlog "Wait for ConnectionOpened message" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings and wait for reply + tlog "Send ping and wait for reply" + replicateM_ numPings $ do + send conn [msg] + Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg + return () + + -- Close the connection + tlog "Close the connection" + close conn + + -- Wait for the server to close its connection to us + tlog "Wait for ConnectionClosed message" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + -- Done + tlog "Ping client done" + +-- | Basic ping test +testPingPong :: Transport -> Int -> IO () +testPingPong transport numPings = do + tlog "Starting ping pong test" + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + tlog "Ping client" + Right endpoint <- newEndPoint transport + ping endpoint server numPings "ping" + putMVar result () + + takeMVar result + +-- | Test that endpoints don't get confused +testEndPoints :: Transport -> Int -> IO () +testEndPoints transport numPings = do + server <- spawn transport echoServer + dones <- replicateM 2 newEmptyMVar + + forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do + let name' :: ByteString + name' = pack [name] + Right endpoint <- newEndPoint transport + tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) + ping endpoint server numPings name' + putMVar done () + + forM_ dones takeMVar + +-- Test that connections don't get confused +testConnections :: Transport -> Int -> IO () +testConnections transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ replicateM_ numPings $ send conn1 ["pingA"] + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ numPings $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (2 * numPings) + + takeMVar result + +-- | Test that closing one connection does not close the other +testCloseOneConnection :: Transport -> Int -> IO () +testCloseOneConnection transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ do + replicateM_ numPings $ send conn1 ["pingA"] + close conn1 + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (3 * numPings) + + takeMVar result + +-- | Test that if A connects to B and B connects to A, B can still send to A after +-- A closes its connection to B (for instance, in the TCP transport, the socket pair +-- connecting A and B should not yet be closed). +testCloseOneDirection :: Transport -> Int -> IO () +testCloseOneDirection transport numPings = do + addrA <- newEmptyMVar + addrB <- newEmptyMVar + doneA <- newEmptyMVar + doneB <- newEmptyMVar + + -- A + forkTry $ do + tlog "A" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrA (address endpoint) + + -- Connect to B + tlog "Connect to B" + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + -- Wait for B to connect to us + tlog "Wait for B" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings to B + tlog "Send pings to B" + replicateM_ numPings $ send conn ["ping"] + + -- Close our connection to B + tlog "Close connection" + close conn + + -- Wait for B's pongs + tlog "Wait for pongs from B" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for B to close it's connection to us + tlog "Wait for B to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Done + tlog "Done" + putMVar doneA () + + -- B + forkTry $ do + tlog "B" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrB (address endpoint) + + -- Wait for A to connect + tlog "Wait for A to connect" + ConnectionOpened cid _ _ <- receive endpoint + + -- Connect to A + tlog "Connect to A" + Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + -- Wait for A's pings + tlog "Wait for pings from A" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for A to close it's connection to us + tlog "Wait for A to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Send pongs to A + tlog "Send pongs to A" + replicateM_ numPings $ send conn ["pong"] + + -- Close our connection to A + tlog "Close connection to A" + close conn + + -- Done + tlog "Done" + putMVar doneB () + + mapM_ takeMVar [doneA, doneB] + +-- | Collect events and order them by connection ID +collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] +collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty + where + -- TODO: for more serious use of this function we'd need to make these arguments strict + go (Just 0) open closed = finish open closed + go n open closed = do + mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint + case mEvent of + Left _ -> finish open closed + Right event -> do + let n' = (\x -> x - 1) <$> n + case event of + ConnectionOpened cid _ _ -> + go n' (Map.insert cid [] open) closed + ConnectionClosed cid -> + let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in + go n' (Map.delete cid open) (Map.insert cid list closed) + Received cid msg -> + go n' (Map.adjust (msg :) cid open) closed + ReceivedMulticast _ _ -> + fail "Unexpected multicast" + ErrorEvent _ -> + fail "Unexpected error" + EndPointClosed -> + fail "Unexpected endpoint closure" + + finish open closed = + if Map.null open + then return . Map.toList . Map.map reverse $ closed + else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) + +-- | Open connection, close it, then reopen it +-- (In the TCP transport this means the socket will be closed, then reopened) +-- +-- Note that B cannot expect to receive all of A's messages on the first connection +-- before receiving the messages on the second connection. What might (and sometimes +-- does) happen is that finishes sending all of its messages on the first connection +-- (in the TCP transport, the first socket pair) while B is behind on reading _from_ +-- this connection (socket pair) -- the messages are "in transit" on the network +-- (these tests are done on localhost, so there are in some OS buffer). Then when +-- A opens the second connection (socket pair) B will spawn a new thread for this +-- connection, and hence might start interleaving messages from the first and second +-- connection. +-- +-- This is correct behaviour, however: the transport API guarantees reliability and +-- ordering _per connection_, but not _across_ connections. +testCloseReopen :: Transport -> Int -> IO () +testCloseReopen transport numPings = do + addrB <- newEmptyMVar + doneB <- newEmptyMVar + + let numRepeats = 2 :: Int + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + + forM_ [1 .. numRepeats] $ \i -> do + tlog "A connecting" + -- Connect to B + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + tlog "A pinging" + -- Say hi + forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] + + tlog "A closing" + -- Disconnect again + close conn + + tlog "A finishing" + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar addrB (address endpoint) + + eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing + + forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do + forM_ (zip [1 .. numPings] events) $ \(j, event) -> do + guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) + + putMVar doneB () + + takeMVar doneB + +-- | Test lots of parallel connection attempts +testParallelConnects :: Transport -> Int -> IO () +testParallelConnects transport numPings = do + server <- spawn transport echoServer + done <- newEmptyMVar + + Right endpoint <- newEndPoint transport + + -- Spawn lots of clients + forM_ [1 .. numPings] $ \i -> forkTry $ do + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints + send conn [pack $ "ping" ++ show i] + send conn [pack $ "ping" ++ show i] + close conn + + forkTry $ do + eventss <- collect endpoint (Just (numPings * 4)) Nothing + -- Check that no pings got sent to the wrong connection + forM_ eventss $ \(_, [[ping1], [ping2]]) -> + guard (ping1 == ping2) + putMVar done () + + takeMVar done + +-- | Test that sending on a closed connection gives an error +testSendAfterClose :: Transport -> Int -> IO () +testSendAfterClose transport numRepeats = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + -- We request two lightweight connections + replicateM numRepeats $ do + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Close the second, but leave the first open; then output on the second + -- connection (i.e., on a closed connection while there is still another + -- connection open) + close conn2 + Left (TransportError SendClosed _) <- send conn2 ["ping2"] + + -- Now close the first connection, and output on it (i.e., output while + -- there are no lightweight connection at all anymore) + close conn1 + Left (TransportError SendClosed _) <- send conn2 ["ping2"] + + return () + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that closing the same connection twice has no effect +testCloseTwice :: Transport -> Int -> IO () +testCloseTwice transport numRepeats = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + replicateM numRepeats $ do + -- We request two lightweight connections + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Close the second one twice + close conn2 + close conn2 + + -- Then send a message on the first and close that twice too + send conn1 ["ping"] + close conn1 + + -- Verify expected response from the echo server + ConnectionOpened cid1 _ _ <- receive endpoint + ConnectionOpened cid2 _ _ <- receive endpoint + ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 + Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 + ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 + + return () + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that we can connect an endpoint to itself +testConnectToSelf :: Transport -> Int -> IO () +testConnectToSelf transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + + tlog "Talk to myself" + + -- One thread to write to the endpoint + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn ["ping"] + + tlog $ "Closing connection" + close conn + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + tlog "Waiting for ConnectionOpened" + ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint + + tlog "Waiting for Received" + replicateM_ numPings $ do + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + return () + + tlog "Waiting for ConnectionClosed" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we can connect an endpoint to itself multiple times +testConnectToSelfTwice :: Transport -> Int -> IO () +testConnectToSelfTwice transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + + tlog "Talk to myself" + + -- One thread to write to the endpoint using the first connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn1 ["pingA"] + + tlog $ "Closing connection" + close conn1 + + -- One thread to write to the endpoint using the second connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn2 ["pingB"] + + tlog $ "Closing connection" + close conn2 + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing + True <- return $ events1 == replicate numPings ["pingA"] + True <- return $ events2 == replicate numPings ["pingB"] + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we self-connections no longer work once we close our endpoint +-- or our transport +testCloseSelf :: IO (Either String Transport) -> IO () +testCloseSelf newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints + + -- Close the conneciton and try to send + close conn1 + Left (TransportError SendClosed _) <- send conn1 ["ping"] + + -- Close the first endpoint. We should not be able to use the first + -- connection anymore, or open more self connections, but the self connection + -- to the second endpoint should still be fine + closeEndPoint endpoint1 + Left (TransportError SendFailed _) <- send conn2 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right () <- send conn3 ["ping"] + + -- Close the transport; now the second should no longer work + closeTransport transport + Left (TransportError SendFailed _) <- send conn3 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints + + return () + +-- | Test various aspects of 'closeEndPoint' +testCloseEndPoint :: Transport -> Int -> IO () +testCloseEndPoint transport _ = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- First test (see client) + do + theirAddr <- readMVar clientAddr1 + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + return () + + -- Second test + do + theirAddr <- readMVar clientAddr2 + + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn ["pong"] + + ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' + ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint ; True <- return $ addr' == theirAddr + + Left (TransportError SendFailed _) <- send conn ["pong2"] + + return () + + putMVar serverDone () + + -- Client + forkTry $ do + theirAddr <- readMVar serverAddr + + -- First test: close endpoint with one outgoing but no incoming connections + do + Right endpoint <- newEndPoint transport + putMVar clientAddr1 (address endpoint) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + closeEndPoint endpoint + EndPointClosed <- receive endpoint + return () + + -- Second test: close endpoint with one outgoing and one incoming connection + do + Right endpoint <- newEndPoint transport + putMVar clientAddr2 (address endpoint) + + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' + + -- Close the endpoint + closeEndPoint endpoint + EndPointClosed <- receive endpoint + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + return () + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- Test closeTransport +-- +-- This tests many of the same things that testEndPoint does, and some more +testCloseTransport :: IO (Either String Transport) -> IO () +testCloseTransport newTransport = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right transport <- newTransport + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Client sets up first endpoint + theirAddr1 <- readMVar clientAddr1 + ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 + + -- Client sets up second endpoint + theirAddr2 <- readMVar clientAddr2 + + ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 + Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 + + Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints + send conn ["pong"] + + -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) + evs <- replicateM 3 $ receive endpoint + let expected = [ ConnectionClosed cid1 + , ConnectionClosed cid2 + , ErrorEvent (TransportError (EventConnectionLost (Just theirAddr2) []) "") + ] + True <- return $ any (== expected) (permutations evs) + + -- An attempt to send to the endpoint should now fail + Left (TransportError SendFailed _) <- send conn ["pong2"] + + putMVar serverDone () + + -- Client + forkTry $ do + Right transport <- newTransport + theirAddr <- readMVar serverAddr + + -- Set up endpoint with one outgoing but no incoming connections + Right endpoint1 <- newEndPoint transport + putMVar clientAddr1 (address endpoint1) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + + -- Set up an endpoint with one outgoing and out incoming connection + Right endpoint2 <- newEndPoint transport + putMVar clientAddr2 (address endpoint2) + + Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' + + -- Now shut down the entire transport + closeTransport transport + + -- Both endpoints should report that they have been closed + EndPointClosed <- receive endpoint1 + EndPointClosed <- receive endpoint2 + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect on either endpoint + Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints + + -- And finally, so should an attempt to create a new endpoint + Left (TransportError NewEndPointFailed _) <- newEndPoint transport + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- | Remote node attempts to connect to a closed local endpoint +testConnectClosedEndPoint :: Transport -> IO () +testConnectClosedEndPoint transport = do + serverAddr <- newEmptyMVar + serverClosed <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + closeEndPoint endpoint + putMVar serverClosed () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + readMVar serverClosed + + Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + putMVar clientDone () + + takeMVar clientDone + +-- | We should receive an exception when doing a 'receive' after we have been +-- notified that an endpoint has been closed +testExceptionOnReceive :: IO (Either String Transport) -> IO () +testExceptionOnReceive newTransport = do + Right transport <- newTransport + + -- Test one: when we close an endpoint specifically + Right endpoint1 <- newEndPoint transport + closeEndPoint endpoint1 + EndPointClosed <- receive endpoint1 + Left _ <- trySome (receive endpoint1 >>= evaluate) + + -- Test two: when we close the entire transport + Right endpoint2 <- newEndPoint transport + closeTransport transport + EndPointClosed <- receive endpoint2 + Left _ <- trySome (receive endpoint2 >>= evaluate) + + return () + +-- | Test what happens when the argument to 'send' is an exceptional value +testSendException :: IO (Either String Transport) -> IO () +testSendException newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + + -- Connect endpoint1 to endpoint2 + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + ConnectionOpened _ _ _ <- receive endpoint2 + + -- Send an exceptional value + Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") + + -- This will have been as a failure to send by endpoint1, which will + -- therefore have closed the socket. In turn this will have caused endpoint2 + -- to report that the connection was lost + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint1 + ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 + + -- A new connection will re-establish the connection + Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + send conn2 ["ping"] + close conn2 + + ConnectionOpened _ _ _ <- receive endpoint2 + Received _ ["ping"] <- receive endpoint2 + ConnectionClosed _ <- receive endpoint2 + + return () + +-- | If threads get killed while executing a 'connect', 'send', or 'close', this +-- should not affect other threads. +-- +-- The intention of this test is to see what happens when a asynchronous +-- exception happes _while executing a send_. This is exceedingly difficult to +-- guarantee, however. Hence we run a large number of tests and insert random +-- thread delays -- and even then it might not happen. Moreover, it will only +-- happen when we run on multiple cores. +testKill :: IO (Either String Transport) -> Int -> IO () +testKill newTransport numThreads = do + Right transport1 <- newTransport + Right transport2 <- newTransport + Right endpoint1 <- newEndPoint transport1 + Right endpoint2 <- newEndPoint transport2 + + threads <- replicateM numThreads . forkIO $ do + randomThreadDelay 100 + bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) + -- Note that we should not insert a randomThreadDelay into the + -- exception handler itself as this means that the exception handler + -- could be interrupted and we might not close + (\(Right conn) -> close conn) + (\(Right conn) -> do randomThreadDelay 100 + Right () <- send conn ["ping"] + randomThreadDelay 100) + + numAlive <- newMVar (0 :: Int) + + -- Kill half of those threads + forkIO . forM_ threads $ \tid -> do + shouldKill <- randomIO + if shouldKill + then randomThreadDelay 600 >> killThread tid + else modifyMVar_ numAlive (return . (+ 1)) + + -- Since it is impossible to predict when the kill exactly happens, we don't + -- know how many connects were opened and how many pings were sent. But we + -- should not have any open connections (if we do, collect will throw an + -- error) and we should have at least the number of pings equal to the number + -- of threads we did *not* kill + eventss <- collect endpoint2 Nothing (Just 1000000) + let actualPings = sum . map (length . snd) $ eventss + expectedPings <- takeMVar numAlive + unless (actualPings >= expectedPings) $ + throwIO (userError "Missing pings") + +-- print (actualPings, expectedPings) + + +-- | Set up conditions with a high likelyhood of "crossing" (for transports +-- that multiplex lightweight connections across heavyweight connections) +testCrossing :: Transport -> Int -> IO () +testCrossing transport numRepeats = do + [aAddr, bAddr] <- replicateM 2 newEmptyMVar + [aDone, bDone] <- replicateM 2 newEmptyMVar + [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar + go <- newEmptyMVar + + let hints = defaultConnectHints { + connectTimeout = Just 5000000 + } + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar aAddr (address endpoint) + theirAddress <- readMVar bAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + -- Because we are creating lots of connections, it's possible that + -- connect times out (for instance, in the TCP transport, + -- Network.Socket.connect may time out). We shouldn't regard this as an + -- error in the Transport, though. + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar aTimeout () + Left (TransportError ConnectFailed _) -> readMVar bTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err + putMVar aDone () + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar bAddr (address endpoint) + theirAddress <- readMVar aAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar bTimeout () + Left (TransportError ConnectFailed _) -> readMVar aTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err + putMVar bDone () + + -- Driver + forM_ [1 .. numRepeats] $ \_i -> do + -- putStrLn $ "Round " ++ show _i + tryTakeMVar aTimeout + tryTakeMVar bTimeout + putMVar go () + putMVar go () + takeMVar aDone + takeMVar bDone + +-- Transport tests +testTransport :: IO (Either String Transport) -> IO () +testTransport newTransport = do + Right transport <- newTransport + runTests + [ ("PingPong", testPingPong transport numPings) + , ("EndPoints", testEndPoints transport numPings) + , ("Connections", testConnections transport numPings) + , ("CloseOneConnection", testCloseOneConnection transport numPings) + , ("CloseOneDirection", testCloseOneDirection transport numPings) + , ("CloseReopen", testCloseReopen transport numPings) + , ("ParallelConnects", testParallelConnects transport numPings) + , ("SendAfterClose", testSendAfterClose transport 1000) + , ("Crossing", testCrossing transport 100) + , ("CloseTwice", testCloseTwice transport 100) + , ("ConnectToSelf", testConnectToSelf transport numPings) + , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) + , ("CloseSelf", testCloseSelf newTransport) + , ("CloseEndPoint", testCloseEndPoint transport numPings) + , ("CloseTransport", testCloseTransport newTransport) + , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) + , ("ExceptionOnReceive", testExceptionOnReceive newTransport) + , ("SendException", testSendException newTransport) + , ("Kill", testKill newTransport 10000) + ] + where + numPings = 10000 :: Int diff --git a/tests/Traced.hs b/tests/Traced.hs new file mode 100644 index 00000000..a7735efa --- /dev/null +++ b/tests/Traced.hs @@ -0,0 +1,191 @@ +-- | Add tracing to the IO monad (see examples). +-- +-- [Usage] +-- +-- > {-# LANGUAGE RebindableSyntax #-} +-- > import Prelude hiding (catch, (>>=), (>>), return, fail) +-- > import Traced +-- +-- [Example] +-- +-- > test1 :: IO Int +-- > test1 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > putStrLn "Hello world" +-- > Right y <- return (Left 2 :: Either Int Int) +-- > return (x + y) +-- +-- outputs +-- +-- > Hello world +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) +-- > Trace: +-- > 0 Left 2 +-- > 1 Left 1 +-- +-- [Guards] +-- +-- Use the following idiom instead of using 'Control.Monad.guard': +-- +-- > test2 :: IO Int +-- > test2 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > True <- return (x == 3) +-- > return x +-- +-- The advantage of this idiom is that it gives you line number information when the guard fails: +-- +-- > *Traced> test2 +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) +-- > Trace: +-- > 0 Left 1 +module Traced ( MonadS(..) + , return + , (>>=) + , (>>) + , fail + , ifThenElse + , Showable(..) + , Traceable(..) + , traceShow + ) where + +import Prelude hiding ((>>=), return, fail, catch, (>>)) +import qualified Prelude +import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) +import Control.Applicative ((<$>)) +import Data.Typeable (Typeable) +import Data.Maybe (catMaybes) +import Data.ByteString (ByteString) +import Data.Int (Int32) +import Control.Concurrent.MVar (MVar) + +-------------------------------------------------------------------------------- +-- MonadS class -- +-------------------------------------------------------------------------------- + +-- | Like 'Monad' but bind is only defined for 'Trace'able instances +class MonadS m where + returnS :: a -> m a + bindS :: Traceable a => m a -> (a -> m b) -> m b + failS :: String -> m a + seqS :: m a -> m b -> m b + +-- | Redefinition of 'Prelude.>>=' +(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b +(>>=) = bindS + +-- | Redefinition of 'Prelude.>>' +(>>) :: MonadS m => m a -> m b -> m b +(>>) = seqS + +-- | Redefinition of 'Prelude.return' +return :: MonadS m => a -> m a +return = returnS + +-- | Redefinition of 'Prelude.fail' +fail :: MonadS m => String -> m a +fail = failS + +-------------------------------------------------------------------------------- +-- Trace typeclass (for adding elements to a trace -- +-------------------------------------------------------------------------------- + +data Showable = forall a. Show a => Showable a + +instance Show Showable where + show (Showable x) = show x + +mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable +mapShowable f (Showable x) = f x + +traceShow :: Show a => a -> Maybe Showable +traceShow = Just . Showable + +class Traceable a where + trace :: a -> Maybe Showable + +instance (Traceable a, Traceable b) => Traceable (Either a b) where + trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x + trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y + +instance (Traceable a, Traceable b) => Traceable (a, b) where + trace (x, y) = case (trace x, trace y) of + (Nothing, Nothing) -> Nothing + (Just t1, Nothing) -> traceShow t1 + (Nothing, Just t2) -> traceShow t2 + (Just t1, Just t2) -> traceShow (t1, t2) + +instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where + trace (x, y, z) = case (trace x, trace y, trace z) of + (Nothing, Nothing, Nothing) -> Nothing + (Just t1, Nothing, Nothing) -> traceShow t1 + (Nothing, Just t2, Nothing) -> traceShow t2 + (Just t1, Just t2, Nothing) -> traceShow (t1, t2) + (Nothing, Nothing, Just t3) -> traceShow t3 + (Just t1, Nothing, Just t3) -> traceShow (t1, t3) + (Nothing, Just t2, Just t3) -> traceShow (t2, t3) + (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) + +instance Traceable a => Traceable (Maybe a) where + trace Nothing = traceShow (Nothing :: Maybe ()) + trace (Just x) = mapShowable (Showable . Just) <$> trace x + +instance Traceable a => Traceable [a] where + trace = traceShow . catMaybes . map trace + +instance Traceable () where + trace = const Nothing + +instance Traceable Int where + trace = traceShow + +instance Traceable Int32 where + trace = traceShow + +instance Traceable Bool where + trace = const Nothing + +instance Traceable ByteString where + trace = traceShow + +instance Traceable (MVar a) where + trace = const Nothing + +instance Traceable [Char] where + trace = traceShow + +instance Traceable IOException where + trace = traceShow + +-------------------------------------------------------------------------------- +-- IO instance for MonadS -- +-------------------------------------------------------------------------------- + +data TracedException = TracedException [String] SomeException + deriving Typeable + +instance Exception TracedException + +-- | Add tracing to 'IO' (see examples) +instance MonadS IO where + returnS = Prelude.return + bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) + failS = Prelude.fail + seqS = (Prelude.>>) + +instance Show TracedException where + show (TracedException ts ex) = + show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) + +traceHandlers :: Traceable a => a -> [Handler b] +traceHandlers a = case trace a of + Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] + Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex + , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) + ] + +-- | Definition of 'ifThenElse' for use with RebindableSyntax +ifThenElse :: Bool -> a -> a -> a +ifThenElse True x _ = x +ifThenElse False _ y = y From 044bbb0e94fa368297815e350d2bfc2440548bab Mon Sep 17 00:00:00 2001 From: ghc704 Date: Fri, 6 Jul 2012 19:25:31 +0100 Subject: [PATCH 0109/2357] Split Network.Transport. THIS BREAKS THE CH BUILD. Starting to prepare for release. Have not yet updated the CH build to reflect the changes. --- LICENSE | 2 +- network-transport.cabal | 137 +-- src/Network/Transport.hs | 4 +- src/Network/Transport/Chan.hs | 157 --- src/Network/Transport/Internal/TCP.hs | 116 -- src/Network/Transport/TCP.hs | 1641 ------------------------- tests/TestAuxiliary.hs | 108 -- tests/TestInMemory.hs | 8 - tests/TestMulticast.hs | 72 -- tests/TestMulticastInMemory.hs | 7 - tests/TestTCP.hs | 786 ------------ tests/TestTransport.hs | 956 -------------- tests/Traced.hs | 191 --- 13 files changed, 69 insertions(+), 4116 deletions(-) delete mode 100644 src/Network/Transport/Chan.hs delete mode 100644 src/Network/Transport/Internal/TCP.hs delete mode 100644 src/Network/Transport/TCP.hs delete mode 100644 tests/TestAuxiliary.hs delete mode 100644 tests/TestInMemory.hs delete mode 100644 tests/TestMulticast.hs delete mode 100644 tests/TestMulticastInMemory.hs delete mode 100644 tests/TestTCP.hs delete mode 100644 tests/TestTransport.hs delete mode 100644 tests/Traced.hs diff --git a/LICENSE b/LICENSE index bbc98067..f3459e44 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright Well-Typed LLP, 2011 +Copyright Well-Typed LLP, 2011-2012 All rights reserved. diff --git a/network-transport.cabal b/network-transport.cabal index db933017..0979c44c 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,80 +1,75 @@ Name: network-transport -Version: 0.1.0 -Description: Network Transport -Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: dcoutts@well-typed.com +Version: 0.2.0 +Cabal-Version: >=1.2.3 +Build-Type: Simple License: BSD3 -License-file: LICENSE +License-File: LICENSE +Copyright: Well-Typed LLP +Author: Duncan Coutts, Nicolas Wu, Edsko de Vries +Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process +Bug-Reports: mailto:edsko@well-typed.com Synopsis: Network abstraction layer +Description: "Network.Transport" is a Network Abstraction Layer which provides + the following high-level concepts: + . + * Nodes in the network are represented by 'EndPoint's. These are + heavyweight stateful objects. + . + * Each 'EndPoint' has an 'EndPointAddress'. + . + * Connections can be established from one 'EndPoint' to another + using the 'EndPointAddress' of the remote end. + . + * The 'EndPointAddress' can be serialised and sent over the + network, where as 'EndPoint's and connections cannot. + . + * Connections between 'EndPoint's are unidirectional and lightweight. + . + * Outgoing messages are sent via a 'Connection' object that + represents the sending end of the connection. + . + * Incoming messages for /all/ of the incoming connections on + an 'EndPoint' are collected via a shared receive queue. + . + * In addition to incoming messages, 'EndPoint's are notified of + other 'Event's such as new connections or broken connections. + . + This design was heavily influenced by the design of the Common + Communication Interface + (). + Important design goals are: + . + * Connections should be lightweight: it should be no problem to + create thousands of connections between endpoints. + . + * Error handling is explicit: every function declares as part of + its type which errors it can return (no exceptions are thrown) + . + * Error handling is "abstract": errors that originate from + implementation specific problems (such as "no more sockets" in + the TCP implementation) get mapped to generic errors + ("insufficient resources") at the Transport level. + . + This package provides the generic interface only; you will + probably also want to install at least one transport + implementation (network-transport-*). +Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Network -Homepage: http://github.com/haskell-distributed -Build-Type: Simple -Cabal-Version: >=1.9.2 Library - Build-Depends: base >= 4 && < 5, - bytestring, - containers, - data-accessor, - network, - mtl, - binary, - transformers - Exposed-modules: Network.Transport, - Network.Transport.Chan, - Network.Transport.TCP, + Build-Depends: base >= 4.3 && < 5, + binary >= 0.5 && < 0.6, + bytestring >= 0.9 && < 0.10, + transformers >= 0.2 && < 0.4 + Exposed-Modules: Network.Transport, Network.Transport.Util Network.Transport.Internal - Network.Transport.Internal.TCP - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving - ghc-options: -Wall -fno-warn-unused-do-bind + Extensions: ForeignFunctionInterface, + RankNTypes, + ScopedTypeVariables, + DeriveDataTypeable, + GeneralizedNewtypeDeriving + GHC-Options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src - -Test-Suite TestTCP - Type: exitcode-stdio-1.0 - Main-Is: TestTCP.hs - Build-Depends: base >= 4, - bytestring, - containers, - data-accessor, - network, - mtl, - transformers, - ansi-terminal, - binary, - random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving, ExistentialQuantification, FlexibleInstances, OverlappingInstances - ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N - HS-Source-Dirs: tests src - -Test-Suite TestMulticastInMemory - Type: exitcode-stdio-1.0 - Main-Is: TestMulticastInMemory.hs - Build-Depends: base >= 4, - bytestring, - containers, - data-accessor, - mtl, - transformers, - ansi-terminal, - binary, - random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving, ExistentialQuantification, FlexibleInstances, OverlappingInstances - ghc-options: -Wall -fno-warn-unused-do-bind - HS-Source-Dirs: tests src - -Test-Suite TestInMemory - Type: exitcode-stdio-1.0 - Main-Is: TestInMemory.hs - Build-Depends: base >= 4, - bytestring, - containers, - data-accessor, - mtl, - transformers, - ansi-terminal, - binary, - random - extensions: OverloadedStrings, ForeignFunctionInterface, DeriveDataTypeable, RankNTypes, ScopedTypeVariables, GeneralizedNewtypeDeriving, ExistentialQuantification, FlexibleInstances, OverlappingInstances - ghc-options: -Wall -fno-warn-unused-do-bind - HS-Source-Dirs: tests src diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 684e45ab..6c327706 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -130,13 +130,13 @@ instance Show MulticastAddress where -- placeholders only. -- -------------------------------------------------------------------------------- --- Hints used by 'connect' +-- | Hints used by 'connect' data ConnectHints = ConnectHints { -- Timeout connectTimeout :: Maybe Int } --- Default hints for connecting +-- | Default hints for connecting defaultConnectHints :: ConnectHints defaultConnectHints = ConnectHints { connectTimeout = Nothing diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs deleted file mode 100644 index 1c194e24..00000000 --- a/src/Network/Transport/Chan.hs +++ /dev/null @@ -1,157 +0,0 @@ --- | In-memory implementation of the Transport API. -module Network.Transport.Chan (createTransport) where - -import Network.Transport -import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) -import Control.Applicative ((<$>)) -import Control.Category ((>>>)) -import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_, readMVar) -import Control.Exception (throwIO) -import Control.Monad (forM_, when) -import Data.Map (Map) -import qualified Data.Map as Map (empty, insert, size, delete, findWithDefault) -import Data.Set (Set) -import qualified Data.Set as Set (empty, elems, insert, delete) -import Data.ByteString (ByteString) -import qualified Data.ByteString.Char8 as BSC (pack) -import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) -import qualified Data.Accessor.Container as DAC (mapMaybe) - --- Global state: next available "address", mapping from addresses to channels and next available connection -data TransportState = State { _channels :: Map EndPointAddress (Chan Event) - , _nextConnectionId :: Map EndPointAddress ConnectionId - , _multigroups :: Map MulticastAddress (MVar (Set EndPointAddress)) - } - --- | Create a new Transport. --- --- Only a single transport should be created per Haskell process --- (threads can, and should, create their own endpoints though). -createTransport :: IO Transport -createTransport = do - state <- newMVar State { _channels = Map.empty - , _nextConnectionId = Map.empty - , _multigroups = Map.empty - } - return Transport { newEndPoint = apiNewEndPoint state - , closeTransport = throwIO (userError "closeEndPoint not implemented") - } - --- | Create a new end point -apiNewEndPoint :: MVar TransportState -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) -apiNewEndPoint state = do - chan <- newChan - addr <- modifyMVar state $ \st -> do - let addr = EndPointAddress . BSC.pack . show . Map.size $ st ^. channels - return ((channelAt addr ^= chan) . (nextConnectionIdAt addr ^= 1) $ st, addr) - return . Right $ EndPoint { receive = readChan chan - , address = addr - , connect = apiConnect addr state - , closeEndPoint = throwIO (userError "closeEndPoint not implemented") - , newMulticastGroup = apiNewMulticastGroup state addr - , resolveMulticastGroup = apiResolveMulticastGroup state addr - } - --- | Create a new connection -apiConnect :: EndPointAddress - -> MVar TransportState - -> EndPointAddress - -> Reliability - -> ConnectHints - -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect myAddress state theirAddress _reliability _hints = do - (chan, conn) <- modifyMVar state $ \st -> do - let chan = st ^. channelAt theirAddress - let conn = st ^. nextConnectionIdAt theirAddress - return (nextConnectionIdAt theirAddress ^: (+ 1) $ st, (chan, conn)) - writeChan chan $ ConnectionOpened conn ReliableOrdered myAddress - connAlive <- newMVar True - return . Right $ Connection { send = apiSend chan conn connAlive - , close = apiClose chan conn connAlive - } - --- | Send a message over a connection -apiSend :: Chan Event -> ConnectionId -> MVar Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) -apiSend chan conn connAlive msg = - modifyMVar connAlive $ \alive -> - if alive - then do - writeChan chan (Received conn msg) - return (alive, Right ()) - else - return (alive, Left (TransportError SendFailed "Connection closed")) - --- | Close a connection -apiClose :: Chan Event -> ConnectionId -> MVar Bool -> IO () -apiClose chan conn connAlive = - modifyMVar_ connAlive $ \alive -> do - when alive . writeChan chan $ ConnectionClosed conn - return False - --- | Create a new multicast group -apiNewMulticastGroup :: MVar TransportState -> EndPointAddress -> IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) -apiNewMulticastGroup state ourAddress = do - group <- newMVar Set.empty - groupAddr <- modifyMVar state $ \st -> do - let addr = MulticastAddress . BSC.pack . show . Map.size $ st ^. multigroups - return (multigroupAt addr ^= group $ st, addr) - return . Right $ createMulticastGroup state ourAddress groupAddr group - --- | Construct a multicast group --- --- When the group is deleted some endpoints may still receive messages, but --- subsequent calls to resolveMulticastGroup will fail. This mimicks the fact --- that some multicast messages may still be in transit when the group is --- deleted. -createMulticastGroup :: MVar TransportState -> EndPointAddress -> MulticastAddress -> MVar (Set EndPointAddress) -> MulticastGroup -createMulticastGroup state ourAddress groupAddress group = - MulticastGroup { multicastAddress = groupAddress - , deleteMulticastGroup = modifyMVar_ state $ return . (multigroups ^: Map.delete groupAddress) - , maxMsgSize = Nothing - , multicastSend = \payload -> do - cs <- (^. channels) <$> readMVar state - es <- readMVar group - forM_ (Set.elems es) $ \ep -> do - let ch = cs ^. at ep "Invalid endpoint" - writeChan ch (ReceivedMulticast groupAddress payload) - , multicastSubscribe = modifyMVar_ group $ return . Set.insert ourAddress - , multicastUnsubscribe = modifyMVar_ group $ return . Set.delete ourAddress - , multicastClose = return () - } - --- | Resolve a multicast group -apiResolveMulticastGroup :: MVar TransportState - -> EndPointAddress - -> MulticastAddress - -> IO (Either (TransportError ResolveMulticastGroupErrorCode) MulticastGroup) -apiResolveMulticastGroup state ourAddress groupAddress = do - group <- (^. (multigroups >>> DAC.mapMaybe groupAddress)) <$> readMVar state - case group of - Nothing -> return . Left $ TransportError ResolveMulticastGroupNotFound ("Group " ++ show groupAddress ++ " not found") - Just mvar -> return . Right $ createMulticastGroup state ourAddress groupAddress mvar - --------------------------------------------------------------------------------- --- Lens definitions -- --------------------------------------------------------------------------------- - -channels :: Accessor TransportState (Map EndPointAddress (Chan Event)) -channels = accessor _channels (\ch st -> st { _channels = ch }) - -nextConnectionId :: Accessor TransportState (Map EndPointAddress ConnectionId) -nextConnectionId = accessor _nextConnectionId (\cid st -> st { _nextConnectionId = cid }) - -multigroups :: Accessor TransportState (Map MulticastAddress (MVar (Set EndPointAddress))) -multigroups = accessor _multigroups (\gs st -> st { _multigroups = gs }) - -at :: Ord k => k -> String -> Accessor (Map k v) v -at k err = accessor (Map.findWithDefault (error err) k) (Map.insert k) - -channelAt :: EndPointAddress -> Accessor TransportState (Chan Event) -channelAt addr = channels >>> at addr "Invalid channel" - -nextConnectionIdAt :: EndPointAddress -> Accessor TransportState ConnectionId -nextConnectionIdAt addr = nextConnectionId >>> at addr "Invalid connection ID" - -multigroupAt :: MulticastAddress -> Accessor TransportState (MVar (Set EndPointAddress)) -multigroupAt addr = multigroups >>> at addr "Invalid multigroup" - diff --git a/src/Network/Transport/Internal/TCP.hs b/src/Network/Transport/Internal/TCP.hs deleted file mode 100644 index 851564e7..00000000 --- a/src/Network/Transport/Internal/TCP.hs +++ /dev/null @@ -1,116 +0,0 @@ --- | Utility functions for TCP sockets -module Network.Transport.Internal.TCP ( forkServer - , recvWithLength - , recvExact - , recvInt32 - , tryCloseSocket - ) where - -import Prelude hiding (catch) -import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) -import qualified Network.Socket as N ( HostName - , ServiceName - , Socket - , SocketType(Stream) - , SocketOption(ReuseAddr) - , getAddrInfo - , defaultHints - , socket - , bindSocket - , listen - , addrFamily - , addrAddress - , defaultProtocol - , setSocketOption - , accept - , sClose - ) -import qualified Network.Socket.ByteString as NBS (recv) -import Control.Concurrent (ThreadId) -import Control.Monad (forever, when) -import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) -import Control.Applicative ((<$>)) -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS (length, concat, null) -import Data.Int (Int32) - --- | Start a server at the specified address. --- --- This sets up a server socket for the specified host and port. Exceptions --- thrown during setup are not caught. --- --- Once the socket is created we spawn a new thread which repeatedly accepts --- incoming connections and executes the given request handler. If any --- exception occurs the thread terminates and calls the terminationHandler. --- This exception may occur because of a call to 'N.accept', because the thread --- was explicitly killed, or because of a synchronous exception thrown by the --- request handler. Typically, you should avoid the last case by catching any --- relevant exceptions in the request handler. --- --- The request handler should spawn threads to handle each individual request --- or the server will block. Once a thread has been spawned it will be the --- responsibility of the new thread to close the socket when an exception --- occurs. -forkServer :: N.HostName -- ^ Host - -> N.ServiceName -- ^ Port - -> Int -- ^ Backlog (maximum number of queued connections) - -> Bool -- ^ Set ReuseAddr option? - -> (SomeException -> IO ()) -- ^ Termination handler - -> (N.Socket -> IO ()) -- ^ Request handler - -> IO ThreadId -forkServer host port backlog reuseAddr terminationHandler requestHandler = do - -- Resolve the specified address. By specification, getAddrInfo will never - -- return an empty list (but will throw an exception instead) and will return - -- the "best" address first, whatever that means - addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just host) (Just port) - bracketOnError (N.socket (N.addrFamily addr) N.Stream N.defaultProtocol) - tryCloseSocket $ \sock -> do - when reuseAddr $ N.setSocketOption sock N.ReuseAddr 1 - N.bindSocket sock (N.addrAddress addr) - N.listen sock backlog - -- We start listening for incoming requests in a separate thread. When - -- that thread is killed, we close the server socket and the termination - -- handler. We have to make sure that the exception handler is installed - -- /before/ any asynchronous exception occurs. So we mask_, then fork - -- (the child thread inherits the masked state from the parent), then - -- unmask only inside the catch. - mask_ $ forkIOWithUnmask $ \unmask -> - catch (unmask (forever $ acceptRequest sock)) $ \ex -> do - tryCloseSocket sock - terminationHandler ex - where - acceptRequest :: N.Socket -> IO () - acceptRequest sock = bracketOnError (N.accept sock) - (tryCloseSocket . fst) - (requestHandler . fst) - --- | Read a length and then a payload of that length -recvWithLength :: N.Socket -> IO [ByteString] -recvWithLength sock = recvInt32 sock >>= recvExact sock - --- | Receive a 32-bit integer -recvInt32 :: Num a => N.Socket -> IO a -recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4 - --- | Close a socket, ignoring I/O exceptions -tryCloseSocket :: N.Socket -> IO () -tryCloseSocket sock = void . tryIO $ - N.sClose sock - --- | Read an exact number of bytes from a socket --- --- Throws an I/O exception if the socket closes before the specified --- number of bytes could be read -recvExact :: N.Socket -- ^ Socket to read from - -> Int32 -- ^ Number of bytes to read - -> IO [ByteString] -recvExact _ len | len <= 0 = throwIO (userError "recvExact: Negative length") -recvExact sock len = go [] len - where - go :: [ByteString] -> Int32 -> IO [ByteString] - go acc 0 = return (reverse acc) - go acc l = do - bs <- NBS.recv sock (fromIntegral l `min` 4096) - if BS.null bs - then throwIO (userError "recvExact: Socket closed") - else go (bs : acc) (l - fromIntegral (BS.length bs)) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs deleted file mode 100644 index 5bf8a9e4..00000000 --- a/src/Network/Transport/TCP.hs +++ /dev/null @@ -1,1641 +0,0 @@ --- | TCP implementation of the transport layer. --- --- The TCP implementation guarantees that only a single TCP connection (socket) --- will be used between endpoints, provided that the addresses specified are --- canonical. If /A/ connects to /B/ and reports its address as --- @192.168.0.1:8080@ and /B/ subsequently connects tries to connect to /A/ as --- @client1.local:http-alt@ then the transport layer will not realize that the --- TCP connection can be reused. --- --- Applications that use the TCP transport should use --- 'Network.Socket.withSocketsDo' in their main function for Windows --- compatibility (see "Network.Socket"). -module Network.Transport.TCP ( -- * Main API - createTransport - , TCPParameters(..) - , defaultTCPParameters - -- * Internals (exposed for unit tests) - , createTransportExposeInternals - , TransportInternals(..) - , EndPointId - , encodeEndPointAddress - , decodeEndPointAddress - , ControlHeader(..) - , ConnectionRequestResponse(..) - , firstNonReservedConnectionId - , socketToEndPoint - -- * Design notes - -- $design - ) where - -import Prelude hiding (catch, mapM_) -import Network.Transport -import Network.Transport.Internal.TCP ( forkServer - , recvWithLength - , recvInt32 - , tryCloseSocket - ) -import Network.Transport.Internal ( encodeInt32 - , decodeInt32 - , prependLength - , mapIOException - , tryIO - , tryToEnum - , void - , timeoutMaybe - , asyncWhenCancelled - ) -import qualified Network.Socket as N ( HostName - , ServiceName - , Socket - , getAddrInfo - , socket - , addrFamily - , addrAddress - , SocketType(Stream) - , defaultProtocol - , setSocketOption - , SocketOption(ReuseAddr) - , connect - , sOMAXCONN - , AddrInfo - ) -import Network.Socket.ByteString (sendMany) -import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) -import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) -import Control.Concurrent.MVar ( MVar - , newMVar - , modifyMVar - , modifyMVar_ - , readMVar - , takeMVar - , putMVar - , newEmptyMVar - , withMVar - ) -import Control.Category ((>>>)) -import Control.Applicative ((<$>)) -import Control.Monad (when, unless) -import Control.Exception ( IOException - , SomeException - , AsyncException - , handle - , throw - , throwIO - , try - , bracketOnError - , mask - , onException - , fromException - ) -import Data.IORef (IORef, newIORef, writeIORef, readIORef) -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS (concat) -import qualified Data.ByteString.Char8 as BSC (pack, unpack, split) -import Data.Int (Int32) -import Data.IntMap (IntMap) -import qualified Data.IntMap as IntMap (empty) -import Data.IntSet (IntSet) -import qualified Data.IntSet as IntSet ( empty - , insert - , elems - , singleton - , null - , delete - , member - ) -import Data.Map (Map) -import qualified Data.Map as Map (empty) -import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) -import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) -import Data.Foldable (forM_, mapM_) - --- $design --- --- [Goals] --- --- The TCP transport maps multiple logical connections between /A/ and /B/ (in --- either direction) to a single TCP connection: --- --- > +-------+ +-------+ --- > | A |==========================| B | --- > | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | --- > | Q |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~Q | --- > | \~~~|~~~~~~~~~~~~~~~~~~~~~~~~~<| | --- > | |==========================| | --- > +-------+ +-------+ --- --- Ignoring the complications detailed below, the TCP connection is set up is --- when the first lightweight connection is created (in either direction), and --- torn down when the last lightweight connection (in either direction) is --- closed. --- --- [Connecting] --- --- Let /A/, /B/ be two endpoints without any connections. When /A/ wants to --- connect to /B/, it locally records that it is trying to connect to /B/ and --- sends a request to /B/. As part of the request /A/ sends its own endpoint --- address to /B/ (so that /B/ can reuse the connection in the other direction). --- --- When /B/ receives the connection request it first checks if it did not --- already initiate a connection request to /A/. If not it will acknowledge the --- connection request by sending 'ConnectionRequestAccepted' to /A/ and record --- that it has a TCP connection to /A/. --- --- The tricky case arises when /A/ sends a connection request to /B/ and /B/ --- finds that it had already sent a connection request to /A/. In this case /B/ --- will accept the connection request from /A/ if /A/s endpoint address is --- smaller (lexicographically) than /B/s, and reject it otherwise. If it rejects --- it, it sends a 'ConnectionRequestCrossed' message to /A/. (The --- lexicographical ordering is an arbitrary but convenient way to break the --- tie.) --- --- When it receives a 'ConnectionRequestCrossed' message the /A/ thread that --- initiated the request just needs to wait until the /A/ thread that is dealing --- with /B/'s connection request completes. --- --- [Disconnecting] --- --- The TCP connection is created as soon as the first logical connection from --- /A/ to /B/ (or /B/ to /A/) is established. At this point a thread (@#@) is --- spawned that listens for incoming connections from /B/: --- --- > +-------+ +-------+ --- > | A |==========================| B | --- > | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | --- > | | | Q | --- > | #| | | --- > | |==========================| | --- > +-------+ +-------+ --- --- The question is when the TCP connection can be closed again. Conceptually, --- we want to do reference counting: when there are no logical connections left --- between /A/ and /B/ we want to close the socket (possibly after some --- timeout). --- --- However, /A/ and /B/ need to agree that the refcount has reached zero. It --- might happen that /B/ sends a connection request over the existing socket at --- the same time that /A/ closes its logical connection to /B/ and closes the --- socket. This will cause a failure in /B/ (which will have to retry) which is --- not caused by a network failure, which is unfortunate. (Note that the --- connection request from /B/ might succeed even if /A/ closes the socket.) --- --- Instead, when /A/ is ready to close the socket it sends a 'CloseSocket' --- request to /B/ and records that its connection to /B/ is closing. If /A/ --- receives a new connection request from /B/ after having sent the --- 'CloseSocket' request it simply forgets that it sent a 'CloseSocket' request --- and increments the reference count of the connection again. --- --- When /B/ receives a 'CloseSocket' message and it too is ready to close the --- connection, it will respond with a reciprocal 'CloseSocket' request to /A/ --- and then actually close the socket. /A/ meanwhile will not send any more --- requests to /B/ after having sent a 'CloseSocket' request, and will actually --- close its end of the socket only when receiving the 'CloseSocket' message --- from /B/. (Since /A/ recorded that its connection to /B/ is in closing state --- after sending a 'CloseSocket' request to /B/, it knows not to reciprocate /B/ --- reciprocal 'CloseSocket' message.) --- --- If there is a concurrent thread in /A/ waiting to connect to /B/ after /A/ --- has sent a 'CloseSocket' request then this thread will block until /A/ knows --- whether to reuse the old socket (if /B/ sends a new connection request --- instead of acknowledging the 'CloseSocket') or to set up a new socket. - --------------------------------------------------------------------------------- --- Internal datatypes -- --------------------------------------------------------------------------------- - --- We use underscores for fields that we might update (using accessors) --- --- All data types follow the same structure: --- --- * A top-level data type describing static properties (TCPTransport, --- LocalEndPoint, RemoteEndPoint) --- * The 'static' properties include an MVar containing a data structure for --- the dynamic properties (TransportState, LocalEndPointState, --- RemoteEndPointState). The state could be invalid/valid/closed,/etc. --- * For the case of "valid" we use third data structure to give more details --- about the state (ValidTransportState, ValidLocalEndPointState, --- ValidRemoteEndPointState). - -data TCPTransport = TCPTransport - { transportHost :: N.HostName - , transportPort :: N.ServiceName - , transportState :: MVar TransportState - , transportParams :: TCPParameters - } - -data TransportState = - TransportValid ValidTransportState - | TransportClosed - -data ValidTransportState = ValidTransportState - { _localEndPoints :: Map EndPointAddress LocalEndPoint - , _nextEndPointId :: EndPointId - } - -data LocalEndPoint = LocalEndPoint - { localAddress :: EndPointAddress - , localChannel :: Chan Event - , localState :: MVar LocalEndPointState - } - -data LocalEndPointState = - LocalEndPointValid ValidLocalEndPointState - | LocalEndPointClosed - -data ValidLocalEndPointState = ValidLocalEndPointState - { _nextConnectionId :: !ConnectionId - , _localConnections :: Map EndPointAddress RemoteEndPoint - , _nextRemoteId :: !Int - } - --- REMOTE ENDPOINTS --- --- Remote endpoints (basically, TCP connections) have the following lifecycle: --- --- Init ---+---> Invalid --- | --- +-------------------------------\ --- | | --- | /----------\ | --- | | | | --- | v | v --- +---> Valid ---> Closing ---> Closed --- | | | | --- | | | v --- \-------+----------+--------> Failed --- --- Init: There are two places where we create new remote endpoints: in --- requestConnectionTo (in response to an API 'connect' call) and in --- handleConnectionRequest (when a remote node tries to connect to us). --- 'Init' carries an MVar () 'resolved' which concurrent threads can use to --- wait for the remote endpoint to finish initialization. We record who --- requested the connection (the local endpoint or the remote endpoint). --- --- Invalid: We put the remote endpoint in invalid state only during --- requestConnectionTo when we fail to connect. --- --- Valid: This is the "normal" state for a working remote endpoint. --- --- Closing: When we detect that a remote endpoint is no longer used, we send a --- CloseSocket request across the connection and put the remote endpoint in --- closing state. As with Init, 'Closing' carries an MVar () 'resolved' which --- concurrent threads can use to wait for the remote endpoint to either be --- closed fully (if the communication parnet responds with another --- CloseSocket) or be put back in 'Valid' state if the remote endpoint denies --- the request. --- --- We also put the endpoint in Closed state, directly from Init, if we our --- outbound connection request crossed an inbound connection request and we --- decide to keep the inbound (i.e., the remote endpoint sent us a --- ConnectionRequestCrossed message). --- --- Closed: The endpoint is put in Closed state after a successful garbage --- collection. --- --- Failed: If the connection to the remote endpoint is lost, or the local --- endpoint (or the whole transport) is closed manually, the remote endpoint is --- put in Failed state, and we record the reason. --- --- Invariants for dealing with remote endpoints: --- --- INV-SEND: Whenever we send data the remote endpoint must be locked (to avoid --- interleaving bits of payload). --- --- INV-CLOSE: Local endpoints should never point to remote endpoint in closed --- state. Whenever we put an endpoint in Closed state we remove that --- endpoint from localConnections first, so that if a concurrent thread reads --- the MVar, finds RemoteEndPointClosed, and then looks up the endpoint in --- localConnections it is guaranteed to either find a different remote --- endpoint, or else none at all (if we don't insist in this order some --- threads might start spinning). --- --- INV-RESOLVE: We should only signal on 'resolved' while the remote endpoint is --- locked, and the remote endpoint must be in Valid or Closed state once --- unlocked. This guarantees that there will not be two threads attempting to --- both signal on 'resolved'. --- --- INV-LOST: If a send or recv fails, or a socket is closed unexpectedly, we --- first put the remote endpoint in Closed state, and then send a --- EventConnectionLost event. This guarantees that we only send this event --- once. --- --- INV-CLOSING: An endpoint in closing state is for all intents and purposes --- closed; that is, we shouldn't do any 'send's on it (although 'recv' is --- acceptable, of course -- as we are waiting for the remote endpoint to --- confirm or deny the request). --- --- INV-LOCK-ORDER: Remote endpoint must be locked before their local endpoints. --- In other words: it is okay to call modifyMVar on a local endpoint inside a --- modifyMVar on a remote endpoint, but not the other way around. In --- particular, it is okay to call removeRemoteEndPoint inside --- modifyRemoteState. - -data RemoteEndPoint = RemoteEndPoint - { remoteAddress :: EndPointAddress - , remoteState :: MVar RemoteState - , remoteId :: Int - } - -data RequestedBy = RequestedByUs | RequestedByThem - deriving (Eq, Show) - -data RemoteState = - -- | Invalid remote endpoint (for example, invalid address) - RemoteEndPointInvalid (TransportError ConnectErrorCode) - -- | The remote endpoint is being initialized - | RemoteEndPointInit (MVar ()) RequestedBy - -- | "Normal" working endpoint - | RemoteEndPointValid ValidRemoteEndPointState - -- | The remote endpoint is being closed (garbage collected) - | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState - -- | The remote endpoint has been closed (garbage collected) - | RemoteEndPointClosed - -- | The remote endpoint has failed, or has been forcefully shutdown - -- using a closeTransport or closeEndPoint API call - | RemoteEndPointFailed IOException - -data ValidRemoteEndPointState = ValidRemoteEndPointState - { _remoteOutgoing :: !Int - , _remoteIncoming :: IntSet - , remoteSocket :: N.Socket - , sendOn :: [ByteString] -> IO () - , _pendingCtrlRequests :: IntMap (MVar (Either IOException [ByteString])) - , _nextCtrlRequestId :: !ControlRequestId - } - -type EndPointId = Int32 -type ControlRequestId = Int32 -type EndPointPair = (LocalEndPoint, RemoteEndPoint) - --- | Control headers -data ControlHeader = - -- | Request a new connection ID from the remote endpoint - RequestConnectionId - -- | Tell the remote endpoint we will no longer be using a connection - | CloseConnection - -- | Respond to a control request /from/ the remote endpoint - | ControlResponse - -- | Request to close the connection (see module description) - | CloseSocket - deriving (Enum, Bounded, Show) - --- Response sent by /B/ to /A/ when /A/ tries to connect -data ConnectionRequestResponse = - -- | /B/ accepts the connection - ConnectionRequestAccepted - -- | /A/ requested an invalid endpoint - | ConnectionRequestInvalid - -- | /A/s request crossed with a request from /B/ (see protocols) - | ConnectionRequestCrossed - deriving (Enum, Bounded, Show) - --- Parameters for setting up the TCP transport -data TCPParameters = TCPParameters { - -- | Backlog for 'listen'. - -- Defaults to SOMAXCONN. - tcpBacklog :: Int - -- | Should we set SO_REUSEADDR on the server socket? - -- Defaults to True. - , tcpReuseServerAddr :: Bool - -- | Should we set SO_REUSEADDR on client sockets? - -- Defaults to True. - , tcpReuseClientAddr :: Bool - } - --- Internal functionality we expose for unit testing -data TransportInternals = TransportInternals - { -- | The ID of the thread that listens for new incoming connections - transportThread :: ThreadId - -- | Find the socket between a local and a remote endpoint - , socketBetween :: EndPointAddress - -> EndPointAddress - -> IO N.Socket - } - --------------------------------------------------------------------------------- --- Top-level functionality -- --------------------------------------------------------------------------------- - --- | Create a TCP transport -createTransport :: N.HostName - -> N.ServiceName - -> TCPParameters - -> IO (Either IOException Transport) -createTransport host port params = - either Left (Right . fst) <$> createTransportExposeInternals host port params - --- | You should probably not use this function (used for unit testing only) -createTransportExposeInternals - :: N.HostName - -> N.ServiceName - -> TCPParameters - -> IO (Either IOException (Transport, TransportInternals)) -createTransportExposeInternals host port params = do - state <- newMVar . TransportValid $ ValidTransportState - { _localEndPoints = Map.empty - , _nextEndPointId = 0 - } - let transport = TCPTransport { transportState = state - , transportHost = host - , transportPort = port - , transportParams = params - } - tryIO $ bracketOnError (forkServer - host - port - (tcpBacklog params) - (tcpReuseServerAddr params) - (terminationHandler transport) - (handleConnectionRequest transport)) - killThread - (mkTransport transport) - where - mkTransport :: TCPTransport - -> ThreadId - -> IO (Transport, TransportInternals) - mkTransport transport tid = return - ( Transport - { newEndPoint = apiNewEndPoint transport - , closeTransport = let evs = [ EndPointClosed - , throw $ userError "Transport closed" - ] in - apiCloseTransport transport (Just tid) evs - } - , TransportInternals - { transportThread = tid - , socketBetween = internalSocketBetween transport - } - ) - - terminationHandler :: TCPTransport -> SomeException -> IO () - terminationHandler transport ex = do - let evs = [ ErrorEvent (TransportError EventTransportFailed (show ex)) - , throw $ userError "Transport closed" - ] - apiCloseTransport transport Nothing evs - --- | Default TCP parameters -defaultTCPParameters :: TCPParameters -defaultTCPParameters = TCPParameters { - tcpBacklog = N.sOMAXCONN - , tcpReuseServerAddr = True - , tcpReuseClientAddr = True - } - --------------------------------------------------------------------------------- --- API functions -- --------------------------------------------------------------------------------- - --- | Close the transport -apiCloseTransport :: TCPTransport -> Maybe ThreadId -> [Event] -> IO () -apiCloseTransport transport mTransportThread evs = - asyncWhenCancelled return $ do - mTSt <- modifyMVar (transportState transport) $ \st -> case st of - TransportValid vst -> return (TransportClosed, Just vst) - TransportClosed -> return (TransportClosed, Nothing) - forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) - -- This will invoke the termination handler, which in turn will call - -- apiCloseTransport again, but then the transport will already be closed - -- and we won't be passed a transport thread, so we terminate immmediate - forM_ mTransportThread killThread - --- | Create a new endpoint -apiNewEndPoint :: TCPTransport - -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) -apiNewEndPoint transport = - try . asyncWhenCancelled closeEndPoint $ do - ourEndPoint <- createLocalEndPoint transport - return EndPoint - { receive = readChan (localChannel ourEndPoint) - , address = localAddress ourEndPoint - , connect = apiConnect (transportParams transport) ourEndPoint - , closeEndPoint = let evs = [ EndPointClosed - , throw $ userError "Endpoint closed" - ] in - apiCloseEndPoint transport evs ourEndPoint - , newMulticastGroup = return . Left $ newMulticastGroupError - , resolveMulticastGroup = return . Left . const resolveMulticastGroupError - } - where - newMulticastGroupError = - TransportError NewMulticastGroupUnsupported "Multicast not supported" - resolveMulticastGroupError = - TransportError ResolveMulticastGroupUnsupported "Multicast not supported" - --- | Connnect to an endpoint -apiConnect :: TCPParameters -- ^ Parameters - -> LocalEndPoint -- ^ Local end point - -> EndPointAddress -- ^ Remote address - -> Reliability -- ^ Reliability (ignored) - -> ConnectHints -- ^ Hints - -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect params ourEndPoint theirAddress _reliability hints = - try . asyncWhenCancelled close $ - if localAddress ourEndPoint == theirAddress - then connectToSelf ourEndPoint - else do - resetIfBroken ourEndPoint theirAddress - (theirEndPoint, connId) <- - requestConnectionTo params ourEndPoint theirAddress hints - -- connAlive can be an IORef rather than an MVar because it is protected - -- by the remoteState MVar. We don't need the overhead of locking twice. - connAlive <- newIORef True - return Connection - { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive - , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive - } - --- | Close a connection -apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () -apiClose (ourEndPoint, theirEndPoint) connId connAlive = - void . tryIO . asyncWhenCancelled return $ do - modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity - { caseValid = \vst -> do - alive <- readIORef connAlive - if alive - then do - writeIORef connAlive False - sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] - return ( RemoteEndPointValid - . (remoteOutgoing ^: (\x -> x - 1)) - $ vst - ) - else - return (RemoteEndPointValid vst) - } - closeIfUnused (ourEndPoint, theirEndPoint) - --- | Send data across a connection -apiSend :: EndPointPair -- ^ Local and remote endpoint - -> ConnectionId -- ^ Connection ID (supplied by remote endpoint) - -> IORef Bool -- ^ Is the connection still alive? - -> [ByteString] -- ^ Payload - -> IO (Either (TransportError SendErrorCode) ()) -apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = - -- We don't need the overhead of asyncWhenCancelled here - try . mapIOException sendFailed $ - withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseInvalid = \_ -> - relyViolation (ourEndPoint, theirEndPoint) "apiSend" - , caseInit = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "apiSend" - , caseValid = \vst -> do - alive <- readIORef connAlive - if alive - then sendOn vst (encodeInt32 connId : prependLength payload) - else throwIO $ TransportError SendClosed "Connection closed" - , caseClosing = \_ _ -> do - alive <- readIORef connAlive - if alive - then relyViolation (ourEndPoint, theirEndPoint) "apiSend" - else throwIO $ TransportError SendClosed "Connection closed" - , caseClosed = do - alive <- readIORef connAlive - if alive - then relyViolation (ourEndPoint, theirEndPoint) "apiSend" - else throwIO $ TransportError SendClosed "Connection closed" - , caseFailed = \err -> do - alive <- readIORef connAlive - if alive - then throwIO $ TransportError SendFailed (show err) - else throwIO $ TransportError SendClosed "Connection closed" - } - where - sendFailed = TransportError SendFailed . show - --- | Force-close the endpoint -apiCloseEndPoint :: TCPTransport -- ^ Transport - -> [Event] -- ^ Events used to report closure - -> LocalEndPoint -- ^ Local endpoint - -> IO () -apiCloseEndPoint transport evs ourEndPoint = - asyncWhenCancelled return $ do - -- Remove the reference from the transport state - removeLocalEndPoint transport ourEndPoint - -- Close the local endpoint - mOurState <- modifyMVar (localState ourEndPoint) $ \st -> - case st of - LocalEndPointValid vst -> - return (LocalEndPointClosed, Just vst) - LocalEndPointClosed -> - return (LocalEndPointClosed, Nothing) - forM_ mOurState $ \vst -> do - forM_ (vst ^. localConnections) tryCloseRemoteSocket - forM_ evs $ writeChan (localChannel ourEndPoint) - where - -- Close the remote socket and return the set of all incoming connections - tryCloseRemoteSocket :: RemoteEndPoint -> IO () - tryCloseRemoteSocket theirEndPoint = do - -- We make an attempt to close the connection nicely - -- (by sending a CloseSocket first) - let closed = RemoteEndPointFailed . userError $ "apiCloseEndPoint" - modifyMVar_ (remoteState theirEndPoint) $ \st -> - case st of - RemoteEndPointInvalid _ -> - return st - RemoteEndPointInit resolved _ -> do - putMVar resolved () - return closed - RemoteEndPointValid conn -> do - tryIO $ sendOn conn [encodeInt32 CloseSocket] - tryCloseSocket (remoteSocket conn) - return closed - RemoteEndPointClosing resolved conn -> do - putMVar resolved () - tryCloseSocket (remoteSocket conn) - return closed - RemoteEndPointClosed -> - return st - RemoteEndPointFailed err -> - return $ RemoteEndPointFailed err - --------------------------------------------------------------------------------- --- As soon as a remote connection fails, we want to put notify our endpoint -- --- and put it into a closed state. Since this may happen in many places, we -- --- provide some abstractions. -- --------------------------------------------------------------------------------- - -data RemoteStatePatternMatch a = RemoteStatePatternMatch - { caseInvalid :: TransportError ConnectErrorCode -> IO a - , caseInit :: MVar () -> RequestedBy -> IO a - , caseValid :: ValidRemoteEndPointState -> IO a - , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a - , caseClosed :: IO a - , caseFailed :: IOException -> IO a - } - -remoteStateIdentity :: RemoteStatePatternMatch RemoteState -remoteStateIdentity = - RemoteStatePatternMatch - { caseInvalid = return . RemoteEndPointInvalid - , caseInit = (return .) . RemoteEndPointInit - , caseValid = return . RemoteEndPointValid - , caseClosing = (return .) . RemoteEndPointClosing - , caseClosed = return RemoteEndPointClosed - , caseFailed = return . RemoteEndPointFailed - } - --- | Like modifyMVar, but if an I/O exception occurs don't restore the remote --- endpoint to its original value but close it instead -modifyRemoteState :: EndPointPair - -> RemoteStatePatternMatch (RemoteState, a) - -> IO a -modifyRemoteState (ourEndPoint, theirEndPoint) match = - mask $ \restore -> do - st <- takeMVar theirState - case st of - RemoteEndPointValid vst -> do - mResult <- try $ restore (caseValid match vst) - case mResult of - Right (st', a) -> do - putMVar theirState st' - return a - Left ex -> do - case fromException ex of - Just ioEx -> handleIOException ioEx vst - Nothing -> putMVar theirState st - throwIO ex - -- The other cases are less interesting, because unless the endpoint is - -- in Valid state we're not supposed to do any IO on it - RemoteEndPointInit resolved origin -> do - (st', a) <- onException (restore $ caseInit match resolved origin) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointClosing resolved vst -> do - (st', a) <- onException (restore $ caseClosing match resolved vst) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointInvalid err -> do - (st', a) <- onException (restore $ caseInvalid match err) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointClosed -> do - (st', a) <- onException (restore $ caseClosed match) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointFailed err -> do - (st', a) <- onException (restore $ caseFailed match err) - (putMVar theirState st) - putMVar theirState st' - return a - where - theirState :: MVar RemoteState - theirState = remoteState theirEndPoint - - handleIOException :: IOException -> ValidRemoteEndPointState -> IO () - handleIOException ex vst = do - tryCloseSocket (remoteSocket vst) - putMVar theirState (RemoteEndPointFailed ex) - let incoming = IntSet.elems $ vst ^. remoteIncoming - code = EventConnectionLost (Just $ remoteAddress theirEndPoint) incoming - err = TransportError code (show ex) - writeChan (localChannel ourEndPoint) $ ErrorEvent err - --- | Like 'modifyRemoteState' but without a return value -modifyRemoteState_ :: EndPointPair - -> RemoteStatePatternMatch RemoteState - -> IO () -modifyRemoteState_ (ourEndPoint, theirEndPoint) match = - modifyRemoteState (ourEndPoint, theirEndPoint) - RemoteStatePatternMatch - { caseInvalid = u . caseInvalid match - , caseInit = \resolved origin -> u $ caseInit match resolved origin - , caseValid = u . caseValid match - , caseClosing = \resolved vst -> u $ caseClosing match resolved vst - , caseClosed = u $ caseClosed match - , caseFailed = u . caseFailed match - } - where - u :: IO a -> IO (a, ()) - u p = p >>= \a -> return (a, ()) - --- | Like 'modifyRemoteState' but without the ability to change the state -withRemoteState :: EndPointPair - -> RemoteStatePatternMatch a - -> IO a -withRemoteState (ourEndPoint, theirEndPoint) match = - modifyRemoteState (ourEndPoint, theirEndPoint) - RemoteStatePatternMatch - { caseInvalid = \err -> do - a <- caseInvalid match err - return (RemoteEndPointInvalid err, a) - , caseInit = \resolved origin -> do - a <- caseInit match resolved origin - return (RemoteEndPointInit resolved origin, a) - , caseValid = \vst -> do - a <- caseValid match vst - return (RemoteEndPointValid vst, a) - , caseClosing = \resolved vst -> do - a <- caseClosing match resolved vst - return (RemoteEndPointClosing resolved vst, a) - , caseClosed = do - a <- caseClosed match - return (RemoteEndPointClosed, a) - , caseFailed = \err -> do - a <- caseFailed match err - return (RemoteEndPointFailed err, a) - } - --------------------------------------------------------------------------------- --- Incoming requests -- --------------------------------------------------------------------------------- - --- | Handle a connection request (that is, a remote endpoint that is trying to --- establish a TCP connection with us) --- --- 'handleConnectionRequest' runs in the context of the transport thread, which --- can be killed asynchronously by 'closeTransport'. We fork a separate thread --- as soon as we have located the lcoal endpoint that the remote endpoint is --- interested in. We cannot fork any sooner because then we have no way of --- storing the thread ID and hence no way of killing the thread when we take --- the transport down. We must be careful to close the socket when a (possibly --- asynchronous, ThreadKilled) exception occurs. (If an exception escapes from --- handleConnectionRequest the transport will be shut down.) -handleConnectionRequest :: TCPTransport -> N.Socket -> IO () -handleConnectionRequest transport sock = handle handleException $ do - ourEndPointId <- recvInt32 sock - theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock - let ourAddress = encodeEndPointAddress (transportHost transport) - (transportPort transport) - ourEndPointId - ourEndPoint <- withMVar (transportState transport) $ \st -> case st of - TransportValid vst -> - case vst ^. localEndPointAt ourAddress of - Nothing -> do - sendMany sock [encodeInt32 ConnectionRequestInvalid] - throwIO $ userError "handleConnectionRequest: Invalid endpoint" - Just ourEndPoint -> - return ourEndPoint - TransportClosed -> - throwIO $ userError "Transport closed" - void . forkIO $ go ourEndPoint theirAddress - where - go :: LocalEndPoint -> EndPointAddress -> IO () - go ourEndPoint theirAddress = do - -- This runs in a thread that will never be killed - mEndPoint <- handle ((>> return Nothing) . handleException) $ do - resetIfBroken ourEndPoint theirAddress - (theirEndPoint, isNew) <- - findRemoteEndPoint ourEndPoint theirAddress RequestedByThem - - if not isNew - then do - tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] - tryCloseSocket sock - return Nothing - else do - let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 - } - sendMany sock [encodeInt32 ConnectionRequestAccepted] - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) - return (Just theirEndPoint) - -- If we left the scope of the exception handler with a return value of - -- Nothing then the socket is already closed; otherwise, the socket has - -- been recorded as part of the remote endpoint. Either way, we no longer - -- have to worry about closing the socket on receiving an asynchronous - -- exception from this point forward. - forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint - - handleException :: SomeException -> IO () - handleException ex = do - tryCloseSocket sock - rethrowIfAsync (fromException ex) - - rethrowIfAsync :: Maybe AsyncException -> IO () - rethrowIfAsync = mapM_ throwIO - --- | Handle requests from a remote endpoint. --- --- Returns only if the remote party closes the socket or if an error occurs. --- This runs in a thread that will never be killed. -handleIncomingMessages :: EndPointPair -> IO () -handleIncomingMessages (ourEndPoint, theirEndPoint) = do - mSock <- withMVar theirState $ \st -> - case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages (invalid)" - RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages (init)" - RemoteEndPointValid ep -> - return . Just $ remoteSocket ep - RemoteEndPointClosing _ ep -> - return . Just $ remoteSocket ep - RemoteEndPointClosed -> - return Nothing - RemoteEndPointFailed _ -> - return Nothing - - forM_ mSock $ \sock -> - tryIO (go sock) >>= either (prematureExit sock) return - where - -- Dispatch - -- - -- If a recv throws an exception this will be caught top-level and - -- 'prematureExit' will be invoked. The same will happen if the remote - -- endpoint is put into a Closed (or Closing) state by a concurrent thread - -- (because a 'send' failed) -- the individual handlers below will throw a - -- user exception which is then caught and handled the same way as an - -- exception thrown by 'recv'. - go :: N.Socket -> IO () - go sock = do - connId <- recvInt32 sock - if connId >= firstNonReservedConnectionId - then do - readMessage sock connId - go sock - else - case tryToEnum (fromIntegral connId) of - Just RequestConnectionId -> do - recvInt32 sock >>= createNewConnection - go sock - Just ControlResponse -> do - recvInt32 sock >>= readControlResponse sock - go sock - Just CloseConnection -> do - recvInt32 sock >>= closeConnection - go sock - Just CloseSocket -> do - didClose <- closeSocket sock - unless didClose $ go sock - Nothing -> - throwIO $ userError "Invalid control request" - - -- Create a new connection - createNewConnection :: ControlRequestId -> IO () - createNewConnection reqId = do - newId <- getNextConnectionId ourEndPoint - modifyMVar_ theirState $ \st -> do - vst <- case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:createNewConnection (invalid)" - RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:createNewConnection (init)" - RemoteEndPointValid vst -> - return (remoteIncoming ^: IntSet.insert newId $ vst) - RemoteEndPointClosing resolved vst -> do - -- If the endpoint is in closing state that means we send a - -- CloseSocket request to the remote endpoint. If the remote - -- endpoint replies with the request to create a new connection, it - -- either ignored our request or it sent the request before it got - -- ours. Either way, at this point we simply restore the endpoint - -- to RemoteEndPointValid - putMVar resolved () - return (remoteIncoming ^= IntSet.singleton newId $ vst) - RemoteEndPointFailed err -> - throwIO err - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) - "createNewConnection (closed)" - sendOn vst ( encodeInt32 ControlResponse - : encodeInt32 reqId - : prependLength [encodeInt32 newId] - ) - return (RemoteEndPointValid vst) - writeChan ourChannel (ConnectionOpened newId ReliableOrdered theirAddr) - - -- Read a control response - readControlResponse :: N.Socket -> ControlRequestId -> IO () - readControlResponse sock reqId = do - response <- recvWithLength sock - mmvar <- modifyMVar theirState $ \st -> case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "readControlResponse (invalid)" - RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) - "readControlResponse (init)" - RemoteEndPointValid vst -> - return ( RemoteEndPointValid - . (pendingCtrlRequestsAt reqId ^= Nothing) - $ vst - , vst ^. pendingCtrlRequestsAt reqId - ) - RemoteEndPointClosing _ _ -> - throwIO $ userError "Invalid control response" - RemoteEndPointFailed err -> - throwIO err - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) - "readControlResponse (closed)" - case mmvar of - Nothing -> - throwIO $ userError "Invalid request ID" - Just mvar -> - putMVar mvar (Right response) - - -- Close a connection - -- It is important that we verify that the connection is in fact open, - -- because otherwise we should not decrement the reference count - closeConnection :: ConnectionId -> IO () - closeConnection cid = do - modifyMVar_ theirState $ \st -> case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) "closeConnection (invalid)" - RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "closeConnection (init)" - RemoteEndPointValid vst -> do - unless (IntSet.member cid (vst ^. remoteIncoming)) $ - throwIO $ userError "Invalid CloseConnection" - return ( RemoteEndPointValid - . (remoteIncoming ^: IntSet.delete cid) - $ vst - ) - RemoteEndPointClosing _ _ -> - -- If the remote endpoint is in Closing state, that means that are as - -- far as we are concerned there are no incoming connections. This - -- means that a CloseConnection request at this point is invalid. - throwIO $ userError "Invalid CloseConnection request" - RemoteEndPointFailed err -> - throwIO err - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) "closeConnection (closed)" - writeChan ourChannel (ConnectionClosed cid) - closeIfUnused (ourEndPoint, theirEndPoint) - - -- Close the socket (if we don't have any outgoing connections) - closeSocket :: N.Socket -> IO Bool - closeSocket sock = - modifyMVar theirState $ \st -> - case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:closeSocket (invalid)" - RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:closeSocket (init)" - RemoteEndPointValid vst -> do - -- We regard a CloseSocket message as an (optimized) way for the - -- remote endpoint to indicate that all its connections to us are - -- now properly closed - forM_ (IntSet.elems $ vst ^. remoteIncoming) $ - writeChan ourChannel . ConnectionClosed - let vst' = remoteIncoming ^= IntSet.empty $ vst - -- Check if we agree that the connection should be closed - if vst' ^. remoteOutgoing == 0 - then do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - -- Attempt to reply (but don't insist) - tryIO $ sendOn vst' [encodeInt32 CloseSocket] - tryCloseSocket sock - return (RemoteEndPointClosed, True) - else - return (RemoteEndPointValid vst', False) - RemoteEndPointClosing resolved _ -> do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - tryCloseSocket sock - putMVar resolved () - return (RemoteEndPointClosed, True) - RemoteEndPointFailed err -> - throwIO err - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:closeSocket (closed)" - - -- Read a message and output it on the endPoint's channel. By rights we - -- should verify that the connection ID is valid, but this is unnecessary - -- overhead - readMessage :: N.Socket -> ConnectionId -> IO () - readMessage sock connId = - recvWithLength sock >>= writeChan ourChannel . Received connId - - -- Arguments - ourChannel = localChannel ourEndPoint - theirState = remoteState theirEndPoint - theirAddr = remoteAddress theirEndPoint - - -- Deal with a premature exit - prematureExit :: N.Socket -> IOException -> IO () - prematureExit sock err = do - tryCloseSocket sock - modifyMVar_ theirState $ \st -> - case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:prematureExit" - RemoteEndPointInit _ _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:prematureExit" - RemoteEndPointValid vst -> do - let code = EventConnectionLost - (Just $ remoteAddress theirEndPoint) - (IntSet.elems $ vst ^. remoteIncoming) - writeChan ourChannel . ErrorEvent $ TransportError code (show err) - forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) - return (RemoteEndPointFailed err) - RemoteEndPointClosing resolved _ -> do - putMVar resolved () - return (RemoteEndPointFailed err) - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages:prematureExit" - RemoteEndPointFailed err' -> - return (RemoteEndPointFailed err') - --------------------------------------------------------------------------------- --- Uninterruptable auxiliary functions -- --- -- --- All these functions assume they are running in a thread which will never -- --- be killed. --------------------------------------------------------------------------------- - --- | Request a connection to a remote endpoint --- --- This will block until we get a connection ID from the remote endpoint; if --- the remote endpoint was in 'RemoteEndPointClosing' state then we will --- additionally block until that is resolved. --- --- May throw a TransportError ConnectErrorCode exception. -requestConnectionTo :: TCPParameters - -> LocalEndPoint - -> EndPointAddress - -> ConnectHints - -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo params ourEndPoint theirAddress hints = go - where - go = do - (theirEndPoint, isNew) <- mapIOException connectFailed $ - findRemoteEndPoint ourEndPoint theirAddress RequestedByUs - - if isNew - then do - forkIO . handle absorbAllExceptions $ - setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints - go - else do - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - return (theirEndPoint, decodeInt32 . BS.concat $ reply) - - connectFailed :: IOException -> TransportError ConnectErrorCode - connectFailed = TransportError ConnectFailed . show - - absorbAllExceptions :: SomeException -> IO () - absorbAllExceptions _ex = - return () - --- | Set up a remote endpoint -setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () -setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do - result <- socketToEndPoint ourAddress - theirAddress - (tcpReuseClientAddr params) - (connectTimeout hints) - didAccept <- case result of - Right (sock, ConnectionRequestAccepted) -> do - let vst = ValidRemoteEndPointState - { remoteSocket = sock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 - } - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) - return True - Right (sock, ConnectionRequestInvalid) -> do - let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - tryCloseSocket sock - return False - Right (sock, ConnectionRequestCrossed) -> do - resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed - tryCloseSocket sock - return False - Left err -> do - resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) - return False - - when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) - where - ourAddress = localAddress ourEndPoint - theirAddress = remoteAddress theirEndPoint - invalidAddress = TransportError ConnectNotFound - --- | Do a (blocking) remote request --- --- May throw IO (user) exception if the local or the remote endpoint is closed, --- if the send fails, or if the remote endpoint fails before it replies. -doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] -doRemoteRequest (ourEndPoint, theirEndPoint) header = do - replyMVar <- newEmptyMVar - modifyRemoteState_ (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseValid = \vst -> do - let reqId = vst ^. nextCtrlRequestId - sendOn vst [encodeInt32 header, encodeInt32 reqId] - return ( RemoteEndPointValid - . (nextCtrlRequestId ^: (+ 1)) - . (pendingCtrlRequestsAt reqId ^= Just replyMVar) - $ vst - ) - -- Error cases - , caseInvalid = - throwIO - , caseInit = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" - , caseClosing = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - , caseClosed = - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" - , caseFailed = - throwIO - } - mReply <- takeMVar replyMVar - case mReply of - Left err -> throwIO err - Right reply -> return reply - --- | Send a CloseSocket request if the remote endpoint is unused -closeIfUnused :: EndPointPair -> IO () -closeIfUnused (ourEndPoint, theirEndPoint) = - modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity - { caseValid = \vst -> - if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) - then do - sendOn vst [encodeInt32 CloseSocket] - resolved <- newEmptyMVar - return $ RemoteEndPointClosing resolved vst - else - return $ RemoteEndPointValid vst - } - --- | Reset a remote endpoint if it is in Invalid mode --- --- If the remote endpoint is currently in broken state, and --- --- - a user calls the API function 'connect', or and the remote endpoint is --- - an inbound connection request comes in from this remote address --- --- we remove the remote endpoint first. --- --- Throws a TransportError ConnectFailed exception if the local endpoint is --- closed. -resetIfBroken :: LocalEndPoint -> EndPointAddress -> IO () -resetIfBroken ourEndPoint theirAddress = do - mTheirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> - return (vst ^. localConnectionTo theirAddress) - LocalEndPointClosed -> - throwIO $ TransportError ConnectFailed "Endpoint closed" - forM_ mTheirEndPoint $ \theirEndPoint -> - withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInvalid _ -> - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - RemoteEndPointFailed _ -> - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - _ -> - return () - --- | Special case of 'apiConnect': connect an endpoint to itself --- --- May throw a TransportError ConnectErrorCode (if the local endpoint is closed) -connectToSelf :: LocalEndPoint - -> IO Connection -connectToSelf ourEndPoint = do - connAlive <- newIORef True -- Protected by the local endpoint lock - connId <- mapIOException connectFailed $ getNextConnectionId ourEndPoint - writeChan ourChan $ - ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) - return Connection - { send = selfSend connAlive connId - , close = selfClose connAlive connId - } - where - selfSend :: IORef Bool - -> ConnectionId - -> [ByteString] - -> IO (Either (TransportError SendErrorCode) ()) - selfSend connAlive connId msg = - try . withMVar ourState $ \st -> case st of - LocalEndPointValid _ -> do - alive <- readIORef connAlive - if alive - then writeChan ourChan (Received connId msg) - else throwIO $ TransportError SendClosed "Connection closed" - LocalEndPointClosed -> - throwIO $ TransportError SendFailed "Endpoint closed" - - selfClose :: IORef Bool -> ConnectionId -> IO () - selfClose connAlive connId = - withMVar ourState $ \st -> case st of - LocalEndPointValid _ -> do - alive <- readIORef connAlive - when alive $ do - writeChan ourChan (ConnectionClosed connId) - writeIORef connAlive False - LocalEndPointClosed -> - return () - - ourChan = localChannel ourEndPoint - ourState = localState ourEndPoint - connectFailed = TransportError ConnectFailed . show - --- | Resolve an endpoint currently in 'Init' state -resolveInit :: EndPointPair -> RemoteState -> IO () -resolveInit (ourEndPoint, theirEndPoint) newState = - modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit resolved _ -> do - putMVar resolved () - case newState of - RemoteEndPointClosed -> - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - _ -> - return () - return newState - RemoteEndPointFailed ex -> - throwIO ex - _ -> - relyViolation (ourEndPoint, theirEndPoint) "resolveInit" - --- | Get the next connection ID --- --- Throws an IO exception when the endpoint is closed. -getNextConnectionId :: LocalEndPoint -> IO ConnectionId -getNextConnectionId ourEndpoint = - modifyMVar (localState ourEndpoint) $ \st -> case st of - LocalEndPointValid vst -> do - let connId = vst ^. nextConnectionId - return ( LocalEndPointValid - . (nextConnectionId ^= connId + 1) - $ vst - , connId) - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - --- | Create a new local endpoint --- --- May throw a TransportError NewEndPointErrorCode exception if the transport --- is closed. -createLocalEndPoint :: TCPTransport -> IO LocalEndPoint -createLocalEndPoint transport = do - chan <- newChan - state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState - { _nextConnectionId = firstNonReservedConnectionId - , _localConnections = Map.empty - , _nextRemoteId = 0 - } - modifyMVar (transportState transport) $ \st -> case st of - TransportValid vst -> do - let ix = vst ^. nextEndPointId - let addr = encodeEndPointAddress (transportHost transport) - (transportPort transport) - ix - let localEndPoint = LocalEndPoint { localAddress = addr - , localChannel = chan - , localState = state - } - return ( TransportValid - . (localEndPointAt addr ^= Just localEndPoint) - . (nextEndPointId ^= ix + 1) - $ vst - , localEndPoint - ) - TransportClosed -> - throwIO (TransportError NewEndPointFailed "Transport closed") - - --- | Remove reference to a remote endpoint from a local endpoint --- --- If the local endpoint is closed, do nothing -removeRemoteEndPoint :: EndPointPair -> IO () -removeRemoteEndPoint (ourEndPoint, theirEndPoint) = - modifyMVar_ ourState $ \st -> case st of - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Nothing -> - return st - Just remoteEndPoint' -> - if remoteId remoteEndPoint' == remoteId theirEndPoint - then return - ( LocalEndPointValid - . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) - $ vst - ) - else return st - LocalEndPointClosed -> - return LocalEndPointClosed - where - ourState = localState ourEndPoint - theirAddress = remoteAddress theirEndPoint - --- | Remove reference to a local endpoint from the transport state --- --- Does nothing if the transport is closed -removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () -removeLocalEndPoint transport ourEndPoint = - modifyMVar_ (transportState transport) $ \st -> case st of - TransportValid vst -> - return ( TransportValid - . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) - $ vst - ) - TransportClosed -> - return TransportClosed - --- | Find a remote endpoint. If the remote endpoint does not yet exist we --- create it in Init state. Returns if the endpoint was new. -findRemoteEndPoint - :: LocalEndPoint - -> EndPointAddress - -> RequestedBy - -> IO (RemoteEndPoint, Bool) -findRemoteEndPoint ourEndPoint theirAddress findOrigin = go - where - go = do - (theirEndPoint, isNew) <- modifyMVar ourState $ \st -> case st of - LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of - Just theirEndPoint -> - return (st, (theirEndPoint, False)) - Nothing -> do - resolved <- newEmptyMVar - theirState <- newMVar (RemoteEndPointInit resolved findOrigin) - let theirEndPoint = RemoteEndPoint - { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId - } - return ( LocalEndPointValid - . (localConnectionTo theirAddress ^= Just theirEndPoint) - . (nextRemoteId ^: (+ 1)) - $ vst - , (theirEndPoint, True) - ) - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - - if isNew - then - return (theirEndPoint, True) - else do - let theirState = remoteState theirEndPoint - snapshot <- modifyMVar theirState $ \st -> case st of - RemoteEndPointValid vst -> - case findOrigin of - RequestedByUs -> do - let st' = RemoteEndPointValid - . (remoteOutgoing ^: (+ 1)) - $ vst - return (st', st') - RequestedByThem -> - return (st, st) - _ -> - return (st, st) - -- The snapshot may no longer be up to date at this point, but if we - -- increased the refcount then it can only either be Valid or Failed - -- (after an explicit call to 'closeEndPoint' or 'closeTransport') - case snapshot of - RemoteEndPointInvalid err -> - throwIO err - RemoteEndPointInit resolved initOrigin -> - case (findOrigin, initOrigin) of - (RequestedByUs, RequestedByUs) -> - readMVar resolved >> go - (RequestedByUs, RequestedByThem) -> - readMVar resolved >> go - (RequestedByThem, RequestedByUs) -> - if ourAddress > theirAddress - then - -- Wait for the Crossed message - readMVar resolved >> go - else - return (theirEndPoint, False) - (RequestedByThem, RequestedByThem) -> - throwIO $ userError "Already connected" - RemoteEndPointValid _ -> - -- We assume that the request crossed if we find the endpoint in - -- Valid state. It is possible that this is really an invalid - -- request, but only in the case of a broken client (we don't - -- maintain enough history to be able to tell the difference). - return (theirEndPoint, False) - RemoteEndPointClosing resolved _ -> - readMVar resolved >> go - RemoteEndPointClosed -> - go - RemoteEndPointFailed err -> - throwIO err - - ourState = localState ourEndPoint - ourAddress = localAddress ourEndPoint - --------------------------------------------------------------------------------- --- "Stateless" (MVar free) functions -- --------------------------------------------------------------------------------- - --- | Establish a connection to a remote endpoint --- --- Maybe throw a TransportError -socketToEndPoint :: EndPointAddress -- ^ Our address - -> EndPointAddress -- ^ Their address - -> Bool -- ^ Use SO_REUSEADDR? - -> Maybe Int -- ^ Timeout for connect - -> IO (Either (TransportError ConnectErrorCode) - (N.Socket, ConnectionRequestResponse)) -socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = - try $ do - (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of - Nothing -> throwIO (failed . userError $ "Could not parse") - Just dec -> return dec - addr:_ <- mapIOException invalidAddress $ - N.getAddrInfo Nothing (Just host) (Just port) - bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do - when reuseAddr $ - mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 - mapIOException invalidAddress $ - timeoutMaybe timeout timeoutError $ - N.connect sock (N.addrAddress addr) - response <- mapIOException failed $ do - sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) - recvInt32 sock - case tryToEnum response of - Nothing -> throwIO (failed . userError $ "Unexpected response") - Just r -> return (sock, r) - where - createSocket :: N.AddrInfo -> IO N.Socket - createSocket addr = mapIOException insufficientResources $ - N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - - invalidAddress = TransportError ConnectNotFound . show - insufficientResources = TransportError ConnectInsufficientResources . show - failed = TransportError ConnectFailed . show - timeoutError = TransportError ConnectTimeout "Timed out" - --- | Encode end point address -encodeEndPointAddress :: N.HostName - -> N.ServiceName - -> EndPointId - -> EndPointAddress -encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ - host ++ ":" ++ port ++ ":" ++ show ix - --- | Decode end point address -decodeEndPointAddress :: EndPointAddress - -> Maybe (N.HostName, N.ServiceName, EndPointId) -decodeEndPointAddress (EndPointAddress bs) = - case map BSC.unpack $ BSC.split ':' bs of - [host, port, endPointIdStr] -> - case reads endPointIdStr of - [(endPointId, "")] -> Just (host, port, endPointId) - _ -> Nothing - _ -> - Nothing - --------------------------------------------------------------------------------- --- Functions from TransportInternals -- --------------------------------------------------------------------------------- - --- Find a socket between two endpoints --- --- Throws an IO exception if the socket could not be found. -internalSocketBetween :: TCPTransport -- ^ Transport - -> EndPointAddress -- ^ Local endpoint - -> EndPointAddress -- ^ Remote endpoint - -> IO N.Socket -internalSocketBetween transport ourAddress theirAddress = do - ourEndPoint <- withMVar (transportState transport) $ \st -> case st of - TransportClosed -> - throwIO $ userError "Transport closed" - TransportValid vst -> - case vst ^. localEndPointAt ourAddress of - Nothing -> throwIO $ userError "Local endpoint not found" - Just ep -> return ep - theirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - LocalEndPointValid vst -> - case vst ^. localConnectionTo theirAddress of - Nothing -> throwIO $ userError "Remote endpoint not found" - Just ep -> return ep - withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit _ _ -> - throwIO $ userError "Remote endpoint not yet initialized" - RemoteEndPointValid vst -> - return $ remoteSocket vst - RemoteEndPointClosing _ vst -> - return $ remoteSocket vst - RemoteEndPointClosed -> - throwIO $ userError "Remote endpoint closed" - RemoteEndPointInvalid err -> - throwIO err - RemoteEndPointFailed err -> - throwIO err - --------------------------------------------------------------------------------- --- Constants -- --------------------------------------------------------------------------------- - --- | We reserve a bunch of connection IDs for control messages -firstNonReservedConnectionId :: ConnectionId -firstNonReservedConnectionId = 1024 - --------------------------------------------------------------------------------- --- Accessor definitions -- --------------------------------------------------------------------------------- - -localEndPoints :: Accessor ValidTransportState (Map EndPointAddress LocalEndPoint) -localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es }) - -nextEndPointId :: Accessor ValidTransportState EndPointId -nextEndPointId = accessor _nextEndPointId (\eid st -> st { _nextEndPointId = eid }) - -nextConnectionId :: Accessor ValidLocalEndPointState ConnectionId -nextConnectionId = accessor _nextConnectionId (\cix st -> st { _nextConnectionId = cix }) - -localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) -localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) - -nextRemoteId :: Accessor ValidLocalEndPointState Int -nextRemoteId = accessor _nextRemoteId (\rid st -> st { _nextRemoteId = rid }) - -remoteOutgoing :: Accessor ValidRemoteEndPointState Int -remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs }) - -remoteIncoming :: Accessor ValidRemoteEndPointState IntSet -remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) - -pendingCtrlRequests :: Accessor ValidRemoteEndPointState (IntMap (MVar (Either IOException [ByteString]))) -pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) - -nextCtrlRequestId :: Accessor ValidRemoteEndPointState ControlRequestId -nextCtrlRequestId = accessor _nextCtrlRequestId (\cid st -> st { _nextCtrlRequestId = cid }) - -localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) -localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr - -pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidRemoteEndPointState (Maybe (MVar (Either IOException [ByteString]))) -pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral ix) - -localConnectionTo :: EndPointAddress - -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) -localConnectionTo addr = localConnections >>> DAC.mapMaybe addr - -------------------------------------------------------------------------------- --- Debugging -- -------------------------------------------------------------------------------- - -relyViolation :: EndPointPair -> String -> IO a -relyViolation (ourEndPoint, theirEndPoint) str = do - elog (ourEndPoint, theirEndPoint) (str ++ " RELY violation") - fail (str ++ " RELY violation") - -elog :: EndPointPair -> String -> IO () -elog (ourEndPoint, theirEndPoint) msg = do - tid <- myThreadId - putStrLn $ show (localAddress ourEndPoint) - ++ "/" ++ show (remoteAddress theirEndPoint) - ++ "(" ++ show (remoteId theirEndPoint) ++ ")" - ++ "/" ++ show tid - ++ ": " ++ msg diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs deleted file mode 100644 index d912ee6e..00000000 --- a/tests/TestAuxiliary.hs +++ /dev/null @@ -1,108 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -module TestAuxiliary ( -- Running tests - runTest - , runTests - -- Writing tests - , forkTry - , trySome - , randomThreadDelay - ) where - -import Prelude hiding (catch) -import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) -import Control.Concurrent.Chan (Chan) -import Control.Monad (liftM2, unless) -import Control.Exception (SomeException, try, catch) -import System.Timeout (timeout) -import System.IO (stdout, hFlush) -import System.Console.ANSI ( SGR(SetColor, Reset) - , Color(Red, Green) - , ConsoleLayer(Foreground) - , ColorIntensity(Vivid) - , setSGR - ) -import System.Random (randomIO) -import Network.Transport -import Traced (Traceable(..), traceShow) - --- | Like fork, but throw exceptions in the child thread to the parent -forkTry :: IO () -> IO ThreadId -forkTry p = do - tid <- myThreadId - forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) - --- | Like try, but specialized to SomeException -trySome :: IO a -> IO (Either SomeException a) -trySome = try - --- | Run the given test, catching timeouts and exceptions -runTest :: String -> IO () -> IO Bool -runTest description test = do - putStr $ "Running " ++ show description ++ ": " - hFlush stdout - done <- try . timeout 60000000 $ test -- 60 seconds - case done of - Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" - Right Nothing -> failed $ "(timeout)" - Right (Just ()) -> ok - where - failed :: String -> IO Bool - failed err = do - setSGR [SetColor Foreground Vivid Red] - putStr "failed " - setSGR [Reset] - putStrLn err - return False - - ok :: IO Bool - ok = do - setSGR [SetColor Foreground Vivid Green] - putStrLn "ok" - setSGR [Reset] - return True - --- | Run a bunch of tests and throw an exception if any fails -runTests :: [(String, IO ())] -> IO () -runTests tests = do - success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests - unless success $ fail "Some tests failed" - --- | Random thread delay between 0 and the specified max -randomThreadDelay :: Int -> IO () -randomThreadDelay maxDelay = do - delay <- randomIO :: IO Int - threadDelay (delay `mod` maxDelay) - --------------------------------------------------------------------------------- --- traceShow instances -- --------------------------------------------------------------------------------- - -instance Traceable EndPoint where - trace = const Nothing - -instance Traceable Transport where - trace = const Nothing - -instance Traceable Connection where - trace = const Nothing - -instance Traceable Event where - trace = traceShow - -instance Show err => Traceable (TransportError err) where - trace = traceShow - -instance Traceable EndPointAddress where - trace = traceShow . endPointAddressToByteString - -instance Traceable SomeException where - trace = traceShow - -instance Traceable ThreadId where - trace = const Nothing - -instance Traceable (Chan a) where - trace = const Nothing - -instance Traceable Float where - trace = traceShow diff --git a/tests/TestInMemory.hs b/tests/TestInMemory.hs deleted file mode 100644 index f7b6f70f..00000000 --- a/tests/TestInMemory.hs +++ /dev/null @@ -1,8 +0,0 @@ -module Main where - -import TestTransport -import Network.Transport.Chan -import Control.Applicative ((<$>)) - -main :: IO () -main = testTransport (Right <$> createTransport) diff --git a/tests/TestMulticast.hs b/tests/TestMulticast.hs deleted file mode 100644 index 43eb526d..00000000 --- a/tests/TestMulticast.hs +++ /dev/null @@ -1,72 +0,0 @@ -module TestMulticast where - -import Network.Transport -import TestAuxiliary (runTests) -import Control.Monad (replicateM, replicateM_, forM_, when) -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar, readMVar) -import Data.ByteString (ByteString) -import Data.List (elemIndex) - --- | Node for the "No confusion" test -noConfusionNode :: Transport -- ^ Transport - -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to - -> [MVar ()] -- ^ I'm ready : others ready - -> Int -- ^ number of pings - -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') - -> MVar () -- ^ I'm done - -> IO () -noConfusionNode transport groups ready numPings msgs done = do - -- Create a new endpoint - Right endpoint <- newEndPoint transport - - -- Create a new multicast group and broadcast its address - Right myGroup <- newMulticastGroup endpoint - putMVar (head groups) (multicastAddress myGroup) - - -- Subscribe to the given multicast groups - addrs <- mapM readMVar (tail groups) - forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr - multicastSubscribe group - - -- Indicate that we're ready and wait for everybody else to be ready - putMVar (head ready) () - mapM_ readMVar (tail ready) - - -- Send messages.. - forkIO . replicateM_ numPings $ multicastSend myGroup [head msgs] - - -- ..while checking that the messages we receive are the right ones - replicateM_ (2 * numPings) $ do - event <- receive endpoint - case event of - ReceivedMulticast addr [msg] -> - let mix = addr `elemIndex` addrs in - case mix of - Nothing -> error "Message from unexpected source" - Just ix -> when (msgs !! (ix + 1) /= msg) $ error "Unexpected message" - _ -> - error "Unexpected event" - - -- Success - putMVar done () - --- | Test that distinct multicast groups are not confused -testNoConfusion :: Transport -> Int -> IO () -testNoConfusion transport numPings = do - [group1, group2, group3] <- replicateM 3 newEmptyMVar - [readyA, readyB, readyC] <- replicateM 3 newEmptyMVar - [doneA, doneB, doneC] <- replicateM 3 newEmptyMVar - let [msgA, msgB, msgC] = ["A says hi", "B says hi", "C says hi"] - - forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA - forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB - forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC - - mapM_ takeMVar [doneA, doneB, doneC] - --- | Test multicast -testMulticast :: Transport -> IO () -testMulticast transport = - runTests - [ ("NoConfusion", testNoConfusion transport 10000) ] diff --git a/tests/TestMulticastInMemory.hs b/tests/TestMulticastInMemory.hs deleted file mode 100644 index 8494af64..00000000 --- a/tests/TestMulticastInMemory.hs +++ /dev/null @@ -1,7 +0,0 @@ -module Main where - -import TestMulticast -import Network.Transport.Chan (createTransport) - -main :: IO () -main = createTransport >>= testMulticast diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs deleted file mode 100644 index f087145d..00000000 --- a/tests/TestTCP.hs +++ /dev/null @@ -1,786 +0,0 @@ -{-# LANGUAGE RebindableSyntax #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -module Main where - -import Prelude hiding (catch, (>>=), (>>), return, fail) -import TestTransport (testTransport) -import TestAuxiliary (forkTry, runTests) -import Network.Transport -import Network.Transport.TCP ( createTransport - , createTransportExposeInternals - , TransportInternals(..) - , encodeEndPointAddress - , defaultTCPParameters - ) -import Data.Int (Int32) -import Control.Concurrent (threadDelay, killThread) -import Control.Concurrent.MVar ( MVar - , newEmptyMVar - , putMVar - , takeMVar - , readMVar - , isEmptyMVar - , newMVar - , modifyMVar - ) -import Control.Monad (replicateM, guard, forM_, replicateM_, when) -import Control.Applicative ((<$>)) -import Control.Exception (throwIO, try, SomeException) -import Network.Transport.TCP ( ControlHeader(..) - , ConnectionRequestResponse(..) - , socketToEndPoint - ) -import Network.Transport.Internal ( encodeInt32 - , prependLength - , tlog - , tryIO - , void - ) -import Network.Transport.Internal.TCP (recvInt32, forkServer, recvWithLength) -import qualified Network.Socket as N ( sClose - , ServiceName - , Socket - , AddrInfo - , shutdown - , ShutdownCmd(ShutdownSend) - ) -import Network.Socket.ByteString (sendMany) -import Data.String (fromString) -import Traced -import GHC.IO.Exception (ioe_errno) -import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) -import System.Timeout (timeout) - -instance Traceable ControlHeader where - trace = traceShow - -instance Traceable ConnectionRequestResponse where - trace = traceShow - -instance Traceable N.Socket where - trace = traceShow - -instance Traceable N.AddrInfo where - trace = traceShow - -instance Traceable TransportInternals where - trace = const Nothing - --- Test that the server gets a ConnectionClosed message when the client closes --- the socket without sending an explicit control message to the server first -testEarlyDisconnect :: IO N.ServiceName -> IO () -testEarlyDisconnect nextPort = do - clientAddr <- newEmptyMVar - serverAddr <- newEmptyMVar - serverDone <- newEmptyMVar - - tlog "testEarlyDisconnect" - forkTry $ server serverAddr clientAddr serverDone - forkTry $ client serverAddr clientAddr - - takeMVar serverDone - where - server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () - server serverAddr clientAddr serverDone = do - tlog "Server" - Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - theirAddr <- readMVar clientAddr - - -- TEST 1: they connect to us, then drop the connection - do - ConnectionOpened cid _ addr <- receive endpoint - True <- return $ addr == theirAddr - - ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid']) _) <- receive endpoint - True <- return $ addr' == theirAddr && cid' == cid - - return () - - -- TEST 2: after they dropped their connection to us, we now try to - -- establish a connection to them. This should re-establish the broken - -- TCP connection. - tlog "Trying to connect to client" - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - -- TEST 3: To test the connection, we do a simple ping test; as before, - -- however, the remote client won't close the connection nicely but just - -- closes the socket - do - Right () <- send conn ["ping"] - - ConnectionOpened cid _ addr <- receive endpoint - True <- return $ addr == theirAddr - - Received cid' ["pong"] <- receive endpoint - True <- return $ cid == cid' - - ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid'']) _) <- receive endpoint - True <- return $ addr' == theirAddr && cid'' == cid - - return () - - -- TEST 4: A subsequent send on an already-open connection will now break - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- *Pfew* - putMVar serverDone () - - client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () - client serverAddr clientAddr = do - tlog "Client" - clientPort <- nextPort - let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 - putMVar clientAddr ourAddress - - -- Listen for incoming messages - forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do - -- Initial setup - 0 <- recvInt32 sock :: IO Int - _ <- recvWithLength sock - sendMany sock [encodeInt32 ConnectionRequestAccepted] - - -- Server requests a logical connection - RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) - reqId <- recvInt32 sock :: IO Int - sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) - - -- Server sends a message - 10001 <- recvInt32 sock :: IO Int - ["ping"] <- recvWithLength sock - - -- Reply - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] - ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) - 10002 <- recvInt32 sock :: IO Int - [cid] <- recvWithLength sock - sendMany sock (cid : prependLength ["pong"]) - - -- Close the socket - N.sClose sock - - -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - - -- Request a new connection, but don't wait for the response - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] - - -- Close the socket without closing the connection explicitly - -- The server should receive an error event - N.sClose sock - --- | Test the behaviour of a premature CloseSocket request -testEarlyCloseSocket :: IO N.ServiceName -> IO () -testEarlyCloseSocket nextPort = do - clientAddr <- newEmptyMVar - serverAddr <- newEmptyMVar - serverDone <- newEmptyMVar - - tlog "testEarlyDisconnect" - forkTry $ server serverAddr clientAddr serverDone - forkTry $ client serverAddr clientAddr - - takeMVar serverDone - where - server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () - server serverAddr clientAddr serverDone = do - tlog "Server" - Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - theirAddr <- readMVar clientAddr - - -- TEST 1: they connect to us, then send a CloseSocket. Since we don't - -- have any outgoing connections, this means we will agree to close the - -- socket - do - ConnectionOpened cid _ addr <- receive endpoint - True <- return $ addr == theirAddr - - ConnectionClosed cid' <- receive endpoint - True <- return $ cid' == cid - - return () - - -- TEST 2: after they dropped their connection to us, we now try to - -- establish a connection to them. This should re-establish the broken - -- TCP connection. - tlog "Trying to connect to client" - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - -- TEST 3: To test the connection, we do a simple ping test; as before, - -- however, the remote client won't close the connection nicely but just - -- sends a CloseSocket -- except that now we *do* have outgoing - -- connections, so we won't agree and hence will receive an error when - -- the socket gets closed - do - Right () <- send conn ["ping"] - - ConnectionOpened cid _ addr <- receive endpoint - True <- return $ addr == theirAddr - - Received cid' ["pong"] <- receive endpoint - True <- return $ cid' == cid - - ConnectionClosed cid'' <- receive endpoint - True <- return $ cid'' == cid - - ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint - True <- return $ addr' == theirAddr - - return () - - -- TEST 4: A subsequent send on an already-open connection will now break - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- *Pfew* - putMVar serverDone () - - client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () - client serverAddr clientAddr = do - tlog "Client" - clientPort <- nextPort - let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 - putMVar clientAddr ourAddress - - -- Listen for incoming messages - forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do - -- Initial setup - 0 <- recvInt32 sock :: IO Int - _ <- recvWithLength sock - sendMany sock [encodeInt32 ConnectionRequestAccepted] - - -- Server requests a logical connection - RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) - reqId <- recvInt32 sock :: IO Int - sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) - - -- Server sends a message - 10001 <- recvInt32 sock :: IO Int - ["ping"] <- recvWithLength sock - - -- Reply - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] - ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) - 10002 <- recvInt32 sock :: IO Int - [cid] <- recvWithLength sock - sendMany sock (cid : prependLength ["pong"]) - - -- Send a CloseSocket even though there are still connections *in both - -- directions* - sendMany sock [encodeInt32 CloseSocket] - N.sClose sock - - -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - - -- Request a new connection, but don't wait for the response - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] - - -- Send a CloseSocket without sending a closeconnecton - -- The server should still receive a ConnectionClosed message - sendMany sock [encodeInt32 CloseSocket] - N.sClose sock - --- | Test the creation of a transport with an invalid address -testInvalidAddress :: IO N.ServiceName -> IO () -testInvalidAddress nextPort = do - Left _ <- nextPort >>= \port -> createTransport "invalidHostName" port defaultTCPParameters - return () - --- | Test connecting to invalid or non-existing endpoints -testInvalidConnect :: IO N.ServiceName -> IO () -testInvalidConnect nextPort = do - port <- nextPort - Right transport <- createTransport "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - - -- Syntax error in the endpoint address - Left (TransportError ConnectFailed _) <- - connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered defaultConnectHints - - -- Syntax connect, but invalid hostname (TCP address lookup failure) - Left (TransportError ConnectNotFound _) <- - connect endpoint (encodeEndPointAddress "invalidHost" "port" 0) ReliableOrdered defaultConnectHints - - -- TCP address correct, but nobody home at that address - Left (TransportError ConnectNotFound _) <- - connect endpoint (encodeEndPointAddress "127.0.0.1" "9000" 0) ReliableOrdered defaultConnectHints - - -- Valid TCP address but invalid endpoint number - Left (TransportError ConnectNotFound _) <- - connect endpoint (encodeEndPointAddress "127.0.0.1" port 1) ReliableOrdered defaultConnectHints - - return () - --- | Test that an endpoint can ignore CloseSocket requests (in "reality" this --- would happen when the endpoint sends a new connection request before --- receiving an (already underway) CloseSocket request) -testIgnoreCloseSocket :: IO N.ServiceName -> IO () -testIgnoreCloseSocket nextPort = do - serverAddr <- newEmptyMVar - clientDone <- newEmptyMVar - Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - - forkTry $ server transport serverAddr - forkTry $ client transport serverAddr clientDone - - takeMVar clientDone - - where - server :: Transport -> MVar EndPointAddress -> IO () - server transport serverAddr = do - tlog "Server" - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- Wait for the client to connect and disconnect - tlog "Waiting for ConnectionOpened" - ConnectionOpened _ _ _ <- receive endpoint - tlog "Waiting for ConnectionClosed" - ConnectionClosed _ <- receive endpoint - - -- At this point the server will have sent a CloseSocket request to the - -- client, which however ignores it, instead it requests and closes - -- another connection - tlog "Waiting for ConnectionOpened" - ConnectionOpened _ _ _ <- receive endpoint - tlog "Waiting for ConnectionClosed" - ConnectionClosed _ <- receive endpoint - - - tlog "Server waiting.." - - client :: Transport -> MVar EndPointAddress -> MVar () -> IO () - client transport serverAddr clientDone = do - tlog "Client" - Right endpoint <- newEndPoint transport - let ourAddress = address endpoint - - -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - - -- Request a new connection - tlog "Requesting connection" - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] - response <- replicateM 4 $ recvInt32 sock :: IO [Int32] - - -- Close the connection again - tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] - - -- Server will now send a CloseSocket request as its refcount reached 0 - tlog "Waiting for CloseSocket request" - CloseSocket <- toEnum <$> recvInt32 sock - - -- But we ignore it and request another connection - tlog "Ignoring it, requesting another connection" - let reqId' = 1 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] - response' <- replicateM 4 $ recvInt32 sock :: IO [Int32] - - -- Close it again - tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response' !! 3)] - - -- We now get a CloseSocket again, and this time we heed it - tlog "Waiting for second CloseSocket request" - CloseSocket <- toEnum <$> recvInt32 sock - - tlog "Closing socket" - sendMany sock [encodeInt32 CloseSocket] - N.sClose sock - - putMVar clientDone () - --- | Like 'testIgnoreSocket', but now the server requests a connection after the --- client closed their connection. In the meantime, the server will have sent a --- CloseSocket request to the client, and must block until the client responds. -testBlockAfterCloseSocket :: IO N.ServiceName -> IO () -testBlockAfterCloseSocket nextPort = do - serverAddr <- newEmptyMVar - clientAddr <- newEmptyMVar - clientDone <- newEmptyMVar - port <- nextPort - Right transport <- createTransport "127.0.0.1" port defaultTCPParameters - - forkTry $ server transport serverAddr clientAddr - forkTry $ client transport serverAddr clientAddr clientDone - - takeMVar clientDone - - where - server :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> IO () - server transport serverAddr clientAddr = do - tlog "Server" - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- Wait for the client to connect and disconnect - tlog "Waiting for ConnectionOpened" - ConnectionOpened _ _ _ <- receive endpoint - tlog "Waiting for ConnectionClosed" - ConnectionClosed _ <- receive endpoint - - -- At this point the server will have sent a CloseSocket request to the - -- client, and must block until the client responds - tlog "Server waiting to connect to the client.." - Right _ <- readMVar clientAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - tlog "Server waiting.." - - client :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () - client transport serverAddr clientAddr clientDone = do - tlog "Client" - Right endpoint <- newEndPoint transport - putMVar clientAddr (address endpoint) - let ourAddress = address endpoint - - -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - - -- Request a new connection - tlog "Requesting connection" - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] - response <- replicateM 4 $ recvInt32 sock :: IO [Int32] - - -- Close the connection again - tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] - - -- Server will now send a CloseSocket request as its refcount reached 0 - tlog "Waiting for CloseSocket request" - CloseSocket <- toEnum <$> recvInt32 sock - - unblocked <- newEmptyMVar - - -- We should not hear from the server until we unblock him by - -- responding to the CloseSocket request (in this case, we - -- respond by sending a ConnectionRequest) - forkTry $ do - recvInt32 sock :: IO Int32 - isEmptyMVar unblocked >>= (guard . not) - putMVar clientDone () - - threadDelay 1000000 - - tlog "Client ignores close socket and sends connection request" - tlog "This should unblock the server" - putMVar unblocked () - let reqId' = 1 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] - --- | Test what happens when a remote endpoint sends a connection request to our --- transport for an endpoint it already has a connection to -testUnnecessaryConnect :: IO N.ServiceName -> Int -> IO () -testUnnecessaryConnect nextPort numThreads = do - clientDone <- newEmptyMVar - serverAddr <- newEmptyMVar - - forkTry $ do - Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - forkTry $ do - -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check - let ourAddress = EndPointAddress "126.0.0.1" - - -- We should only get a single 'Accepted' reply - gotAccepted <- newEmptyMVar - dones <- replicateM numThreads $ do - done <- newEmptyMVar - forkTry $ do - -- It is possible that the remote endpoint just rejects the request by closing the socket - -- immediately (depending on far the remote endpoint got with the initialization) - response <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - case response of - Right (_, ConnectionRequestAccepted) -> - -- We don't close this socket because we want to keep this connection open - putMVar gotAccepted () - -- We might get either Invalid or Crossed (the transport does not - -- maintain enough history to be able to tell) - Right (sock, ConnectionRequestInvalid) -> - N.sClose sock - Right (sock, ConnectionRequestCrossed) -> - N.sClose sock - Left _ -> - return () - putMVar done () - return done - - mapM_ readMVar (gotAccepted : dones) - putMVar clientDone () - - takeMVar clientDone - --- | Test that we can create "many" transport instances -testMany :: IO N.ServiceName -> IO () -testMany nextPort = do - Right masterTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - Right masterEndPoint <- newEndPoint masterTransport - - replicateM_ 10 $ do - mTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - case mTransport of - Left ex -> do - putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex) - case (ioe_errno ex) of - Just no | Errno no == eADDRNOTAVAIL -> putStrLn "(ADDRNOTAVAIL)" - _ -> return () - throwIO ex - Right transport -> - replicateM_ 2 $ do - Right endpoint <- newEndPoint transport - Right _ <- connect endpoint (address masterEndPoint) ReliableOrdered defaultConnectHints - return () - --- | Test what happens when the transport breaks completely -testBreakTransport :: IO N.ServiceName -> IO () -testBreakTransport nextPort = do - Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - - killThread (transportThread internals) -- Uh oh - - ErrorEvent (TransportError EventTransportFailed _) <- receive endpoint - - return () - --- | Test that a second call to 'connect' might succeed even if the first --- failed. This is a TCP specific test rather than an endpoint specific test --- because we must manually create the endpoint address to match an endpoint we --- have yet to set up. --- Then test that we get a connection lost message after the remote endpoint --- suddenly closes the socket, and that a subsequent 'connect' allows us to --- re-establish a connection to the same endpoint -testReconnect :: IO N.ServiceName -> IO () -testReconnect nextPort = do - serverPort <- nextPort - serverDone <- newEmptyMVar - firstAttempt <- newEmptyMVar - endpointCreated <- newEmptyMVar - - -- Server - forkTry $ do - -- Wait for the client to do its first attempt - readMVar firstAttempt - - counter <- newMVar (0 :: Int) - - forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do - -- Accept the connection - Right 0 <- tryIO $ (recvInt32 sock :: IO Int) - Right _ <- tryIO $ recvWithLength sock - Right () <- tryIO $ sendMany sock [encodeInt32 ConnectionRequestAccepted] - - -- The first time we close the socket before accepting the logical connection - count <- modifyMVar counter $ \i -> return (i + 1, i) - - when (count > 0) $ do - -- Client requests a logical connection - Right RequestConnectionId <- tryIO $ toEnum <$> (recvInt32 sock :: IO Int) - Right reqId <- tryIO $ (recvInt32 sock :: IO Int) - Right () <- tryIO $ sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) - return () - - when (count > 1) $ do - -- Client sends a message - Right 10001 <- tryIO $ (recvInt32 sock :: IO Int) - Right ["ping"] <- tryIO $ recvWithLength sock - putMVar serverDone () - - Right () <- tryIO $ N.sClose sock - return () - - putMVar endpointCreated () - - -- Client - forkTry $ do - Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 - - -- The first attempt will fail because no endpoint is yet set up - -- Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - putMVar firstAttempt () - - -- The second attempt will fail because the server closes the socket before we can request a connection - takeMVar endpointCreated - -- This might time out or not, depending on whether the server closes the - -- socket before or after we can send the RequestConnectionId request - resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints - case resultConnect of - Nothing -> return () - Just (Left (TransportError ConnectFailed _)) -> return () - Just (Left err) -> throwIO err - Just (Right _) -> throwIO $ userError "testConnect: unexpected connect success" - - -- The third attempt succeeds - Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - -- But a send will fail because the server has closed the connection again - Left (TransportError SendFailed _) <- send conn1 ["ping"] - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint - - -- But a subsequent call to connect should reestablish the connection - Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - -- Send should now succeed - Right () <- send conn2 ["ping"] - return () - - takeMVar serverDone - --- Test what happens if we close the socket one way only. This means that the --- 'recv' in 'handleIncomingMessages' will not fail, but a 'send' or 'connect' --- *will* fail. We are testing that error handling everywhere does the right --- thing. -testUnidirectionalError :: IO N.ServiceName -> IO () -testUnidirectionalError nextPort = do - clientDone <- newEmptyMVar - serverPort <- nextPort - serverGotPing <- newEmptyMVar - - -- Server - forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do - -- We accept connections, but when an exception occurs we don't do - -- anything (in particular, we don't close the socket). This is important - -- because when we shutdown one direction of the socket a recv here will - -- fail, but we don't want to close that socket at that point (which - -- would shutdown the socket in the other direction) - void . (try :: IO () -> IO (Either SomeException ())) $ do - 0 <- recvInt32 sock :: IO Int - _ <- recvWithLength sock - () <- sendMany sock [encodeInt32 ConnectionRequestAccepted] - - RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) - reqId <- recvInt32 sock :: IO Int - sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) - - 10001 <- recvInt32 sock :: IO Int - ["ping"] <- recvWithLength sock - putMVar serverGotPing () - - -- Client - forkTry $ do - Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters - Right endpoint <- newEndPoint transport - let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 - - -- Establish a connection to the server - Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn1 ["ping"] - takeMVar serverGotPing - - -- Close the *outgoing* part of the socket only - sock <- socketBetween internals (address endpoint) theirAddr - N.shutdown sock N.ShutdownSend - - -- At this point we cannot notice the problem yet so we shouldn't receive an event yet - Nothing <- timeout 500000 $ receive endpoint - - -- But when we send we find the error - Left (TransportError SendFailed _) <- send conn1 ["ping"] - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint - - -- A call to connect should now re-establish the connection - Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn2 ["ping"] - takeMVar serverGotPing - - -- Again, close the outgoing part of the socket - sock' <- socketBetween internals (address endpoint) theirAddr - N.shutdown sock' N.ShutdownSend - - -- We now find the error when we attempt to close the connection - Nothing <- timeout 500000 $ receive endpoint - close conn2 - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint - Right conn3 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn3 ["ping"] - takeMVar serverGotPing - - -- We repeat once more. - sock'' <- socketBetween internals (address endpoint) theirAddr - N.shutdown sock'' N.ShutdownSend - - -- Now we notice the problem when we try to connect - Nothing <- timeout 500000 $ receive endpoint - Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint - Right conn4 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn4 ["ping"] - takeMVar serverGotPing - - putMVar clientDone () - - takeMVar clientDone - -testInvalidCloseConnection :: IO N.ServiceName -> IO () -testInvalidCloseConnection nextPort = do - Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters - serverAddr <- newEmptyMVar - clientDone <- newEmptyMVar - serverDone <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - ConnectionOpened _ _ _ <- receive endpoint - - -- At this point the client sends an invalid request, so we terminate the - -- connection - ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint - - putMVar serverDone () - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - let ourAddr = address endpoint - - -- Connect so that we have a TCP connection - theirAddr <- readMVar serverAddr - Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - -- Get a handle on the TCP connection and manually send an invalid CloseConnection request - sock <- socketBetween internals ourAddr theirAddr - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (12345 :: Int)] - - putMVar clientDone () - - mapM_ takeMVar [clientDone, serverDone] - -main :: IO () -main = do - portMVar <- newEmptyMVar - forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show - let nextPort = takeMVar portMVar - tcpResult <- tryIO $ runTests - [ ("EarlyDisconnect", testEarlyDisconnect nextPort) - , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) - , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) - , ("BlockAfterCloseSocket", testBlockAfterCloseSocket nextPort) - , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort 10) - , ("InvalidAddress", testInvalidAddress nextPort) - , ("InvalidConnect", testInvalidConnect nextPort) - , ("Many", testMany nextPort) - , ("BreakTransport", testBreakTransport nextPort) - , ("Reconnect", testReconnect nextPort) - , ("UnidirectionalError", testUnidirectionalError nextPort) - , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) - ] - -- Run the generic tests even if the TCP specific tests failed.. - testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) - -- ..but if the generic tests pass, still fail if the specific tests did not - case tcpResult of - Left err -> throwIO err - Right () -> return () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs deleted file mode 100644 index e528e327..00000000 --- a/tests/TestTransport.hs +++ /dev/null @@ -1,956 +0,0 @@ -{-# LANGUAGE RebindableSyntax #-} -module TestTransport where - -import Prelude hiding (catch, (>>=), (>>), return, fail) -import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) -import Control.Concurrent (forkIO, killThread, yield) -import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) -import Control.Exception (evaluate, throw, throwIO, bracket) -import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) -import Control.Monad.Error () -import Control.Applicative ((<$>)) -import Network.Transport -import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) -import Network.Transport.Util (spawn) -import System.Random (randomIO) -import Data.ByteString (ByteString) -import Data.ByteString.Char8 (pack) -import Data.Map (Map) -import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) -import Data.String (fromString) -import Data.List (permutations) -import Traced - --- | Server that echoes messages straight back to the origin endpoint. -echoServer :: EndPoint -> IO () -echoServer endpoint = do - go Map.empty - where - go :: Map ConnectionId Connection -> IO () - go cs = do - event <- receive endpoint - case event of - ConnectionOpened cid rel addr -> do - tlog $ "Opened new connection " ++ show cid - Right conn <- connect endpoint addr rel defaultConnectHints - go (Map.insert cid conn cs) - Received cid payload -> do - send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload - go cs - ConnectionClosed cid -> do - tlog $ "Close connection " ++ show cid - close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) - go (Map.delete cid cs) - ReceivedMulticast _ _ -> - -- Ignore - go cs - ErrorEvent _ -> - putStrLn $ "Echo server received error event: " ++ show event - EndPointClosed -> - return () - --- | Ping client used in a few tests -ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () -ping endpoint server numPings msg = do - -- Open connection to the server - tlog "Connect to echo server" - Right conn <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Wait for the server to open reply connection - tlog "Wait for ConnectionOpened message" - ConnectionOpened cid _ _ <- receive endpoint - - -- Send pings and wait for reply - tlog "Send ping and wait for reply" - replicateM_ numPings $ do - send conn [msg] - Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg - return () - - -- Close the connection - tlog "Close the connection" - close conn - - -- Wait for the server to close its connection to us - tlog "Wait for ConnectionClosed message" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - - -- Done - tlog "Ping client done" - --- | Basic ping test -testPingPong :: Transport -> Int -> IO () -testPingPong transport numPings = do - tlog "Starting ping pong test" - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - tlog "Ping client" - Right endpoint <- newEndPoint transport - ping endpoint server numPings "ping" - putMVar result () - - takeMVar result - --- | Test that endpoints don't get confused -testEndPoints :: Transport -> Int -> IO () -testEndPoints transport numPings = do - server <- spawn transport echoServer - dones <- replicateM 2 newEmptyMVar - - forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do - let name' :: ByteString - name' = pack [name] - Right endpoint <- newEndPoint transport - tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) - ping endpoint server numPings name' - putMVar done () - - forM_ dones takeMVar - --- Test that connections don't get confused -testConnections :: Transport -> Int -> IO () -testConnections transport numPings = do - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - - -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv1 _ _ <- receive endpoint - - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv2 _ _ <- receive endpoint - - -- One thread to send "pingA" on the first connection - forkTry $ replicateM_ numPings $ send conn1 ["pingA"] - - -- One thread to send "pingB" on the second connection - forkTry $ replicateM_ numPings $ send conn2 ["pingB"] - - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do - event <- receive endpoint - case event of - Received cid [payload] -> do - when (cid == serv1 && payload /= "pingA") $ error "Wrong message" - when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n - verifyResponse (2 * numPings) - - takeMVar result - --- | Test that closing one connection does not close the other -testCloseOneConnection :: Transport -> Int -> IO () -testCloseOneConnection transport numPings = do - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - - -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv1 _ _ <- receive endpoint - - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv2 _ _ <- receive endpoint - - -- One thread to send "pingA" on the first connection - forkTry $ do - replicateM_ numPings $ send conn1 ["pingA"] - close conn1 - - -- One thread to send "pingB" on the second connection - forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] - - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do - event <- receive endpoint - case event of - Received cid [payload] -> do - when (cid == serv1 && payload /= "pingA") $ error "Wrong message" - when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n - verifyResponse (3 * numPings) - - takeMVar result - --- | Test that if A connects to B and B connects to A, B can still send to A after --- A closes its connection to B (for instance, in the TCP transport, the socket pair --- connecting A and B should not yet be closed). -testCloseOneDirection :: Transport -> Int -> IO () -testCloseOneDirection transport numPings = do - addrA <- newEmptyMVar - addrB <- newEmptyMVar - doneA <- newEmptyMVar - doneB <- newEmptyMVar - - -- A - forkTry $ do - tlog "A" - Right endpoint <- newEndPoint transport - tlog (show (address endpoint)) - putMVar addrA (address endpoint) - - -- Connect to B - tlog "Connect to B" - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - -- Wait for B to connect to us - tlog "Wait for B" - ConnectionOpened cid _ _ <- receive endpoint - - -- Send pings to B - tlog "Send pings to B" - replicateM_ numPings $ send conn ["ping"] - - -- Close our connection to B - tlog "Close connection" - close conn - - -- Wait for B's pongs - tlog "Wait for pongs from B" - replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () - - -- Wait for B to close it's connection to us - tlog "Wait for B to close connection" - ConnectionClosed cid' <- receive endpoint - guard (cid == cid') - - -- Done - tlog "Done" - putMVar doneA () - - -- B - forkTry $ do - tlog "B" - Right endpoint <- newEndPoint transport - tlog (show (address endpoint)) - putMVar addrB (address endpoint) - - -- Wait for A to connect - tlog "Wait for A to connect" - ConnectionOpened cid _ _ <- receive endpoint - - -- Connect to A - tlog "Connect to A" - Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - -- Wait for A's pings - tlog "Wait for pings from A" - replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () - - -- Wait for A to close it's connection to us - tlog "Wait for A to close connection" - ConnectionClosed cid' <- receive endpoint - guard (cid == cid') - - -- Send pongs to A - tlog "Send pongs to A" - replicateM_ numPings $ send conn ["pong"] - - -- Close our connection to A - tlog "Close connection to A" - close conn - - -- Done - tlog "Done" - putMVar doneB () - - mapM_ takeMVar [doneA, doneB] - --- | Collect events and order them by connection ID -collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] -collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty - where - -- TODO: for more serious use of this function we'd need to make these arguments strict - go (Just 0) open closed = finish open closed - go n open closed = do - mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint - case mEvent of - Left _ -> finish open closed - Right event -> do - let n' = (\x -> x - 1) <$> n - case event of - ConnectionOpened cid _ _ -> - go n' (Map.insert cid [] open) closed - ConnectionClosed cid -> - let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in - go n' (Map.delete cid open) (Map.insert cid list closed) - Received cid msg -> - go n' (Map.adjust (msg :) cid open) closed - ReceivedMulticast _ _ -> - fail "Unexpected multicast" - ErrorEvent _ -> - fail "Unexpected error" - EndPointClosed -> - fail "Unexpected endpoint closure" - - finish open closed = - if Map.null open - then return . Map.toList . Map.map reverse $ closed - else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) - --- | Open connection, close it, then reopen it --- (In the TCP transport this means the socket will be closed, then reopened) --- --- Note that B cannot expect to receive all of A's messages on the first connection --- before receiving the messages on the second connection. What might (and sometimes --- does) happen is that finishes sending all of its messages on the first connection --- (in the TCP transport, the first socket pair) while B is behind on reading _from_ --- this connection (socket pair) -- the messages are "in transit" on the network --- (these tests are done on localhost, so there are in some OS buffer). Then when --- A opens the second connection (socket pair) B will spawn a new thread for this --- connection, and hence might start interleaving messages from the first and second --- connection. --- --- This is correct behaviour, however: the transport API guarantees reliability and --- ordering _per connection_, but not _across_ connections. -testCloseReopen :: Transport -> Int -> IO () -testCloseReopen transport numPings = do - addrB <- newEmptyMVar - doneB <- newEmptyMVar - - let numRepeats = 2 :: Int - - -- A - forkTry $ do - Right endpoint <- newEndPoint transport - - forM_ [1 .. numRepeats] $ \i -> do - tlog "A connecting" - -- Connect to B - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - tlog "A pinging" - -- Say hi - forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] - - tlog "A closing" - -- Disconnect again - close conn - - tlog "A finishing" - - -- B - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar addrB (address endpoint) - - eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing - - forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do - forM_ (zip [1 .. numPings] events) $ \(j, event) -> do - guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) - - putMVar doneB () - - takeMVar doneB - --- | Test lots of parallel connection attempts -testParallelConnects :: Transport -> Int -> IO () -testParallelConnects transport numPings = do - server <- spawn transport echoServer - done <- newEmptyMVar - - Right endpoint <- newEndPoint transport - - -- Spawn lots of clients - forM_ [1 .. numPings] $ \i -> forkTry $ do - Right conn <- connect endpoint server ReliableOrdered defaultConnectHints - send conn [pack $ "ping" ++ show i] - send conn [pack $ "ping" ++ show i] - close conn - - forkTry $ do - eventss <- collect endpoint (Just (numPings * 4)) Nothing - -- Check that no pings got sent to the wrong connection - forM_ eventss $ \(_, [[ping1], [ping2]]) -> - guard (ping1 == ping2) - putMVar done () - - takeMVar done - --- | Test that sending on a closed connection gives an error -testSendAfterClose :: Transport -> Int -> IO () -testSendAfterClose transport numRepeats = do - server <- spawn transport echoServer - clientDone <- newEmptyMVar - - forkTry $ do - Right endpoint <- newEndPoint transport - - -- We request two lightweight connections - replicateM numRepeats $ do - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Close the second, but leave the first open; then output on the second - -- connection (i.e., on a closed connection while there is still another - -- connection open) - close conn2 - Left (TransportError SendClosed _) <- send conn2 ["ping2"] - - -- Now close the first connection, and output on it (i.e., output while - -- there are no lightweight connection at all anymore) - close conn1 - Left (TransportError SendClosed _) <- send conn2 ["ping2"] - - return () - - putMVar clientDone () - - takeMVar clientDone - --- | Test that closing the same connection twice has no effect -testCloseTwice :: Transport -> Int -> IO () -testCloseTwice transport numRepeats = do - server <- spawn transport echoServer - clientDone <- newEmptyMVar - - forkTry $ do - Right endpoint <- newEndPoint transport - - replicateM numRepeats $ do - -- We request two lightweight connections - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Close the second one twice - close conn2 - close conn2 - - -- Then send a message on the first and close that twice too - send conn1 ["ping"] - close conn1 - - -- Verify expected response from the echo server - ConnectionOpened cid1 _ _ <- receive endpoint - ConnectionOpened cid2 _ _ <- receive endpoint - ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 - Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 - ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 - - return () - - putMVar clientDone () - - takeMVar clientDone - --- | Test that we can connect an endpoint to itself -testConnectToSelf :: Transport -> Int -> IO () -testConnectToSelf transport numPings = do - done <- newEmptyMVar - Right endpoint <- newEndPoint transport - - tlog "Creating self-connection" - Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - - tlog "Talk to myself" - - -- One thread to write to the endpoint - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn ["ping"] - - tlog $ "Closing connection" - close conn - - -- And one thread to read - forkTry $ do - tlog $ "reading" - - tlog "Waiting for ConnectionOpened" - ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint - - tlog "Waiting for Received" - replicateM_ numPings $ do - Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - return () - - tlog "Waiting for ConnectionClosed" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - - tlog "Done" - putMVar done () - - takeMVar done - --- | Test that we can connect an endpoint to itself multiple times -testConnectToSelfTwice :: Transport -> Int -> IO () -testConnectToSelfTwice transport numPings = do - done <- newEmptyMVar - Right endpoint <- newEndPoint transport - - tlog "Creating self-connection" - Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - - tlog "Talk to myself" - - -- One thread to write to the endpoint using the first connection - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn1 ["pingA"] - - tlog $ "Closing connection" - close conn1 - - -- One thread to write to the endpoint using the second connection - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn2 ["pingB"] - - tlog $ "Closing connection" - close conn2 - - -- And one thread to read - forkTry $ do - tlog $ "reading" - - [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing - True <- return $ events1 == replicate numPings ["pingA"] - True <- return $ events2 == replicate numPings ["pingB"] - - tlog "Done" - putMVar done () - - takeMVar done - --- | Test that we self-connections no longer work once we close our endpoint --- or our transport -testCloseSelf :: IO (Either String Transport) -> IO () -testCloseSelf newTransport = do - Right transport <- newTransport - Right endpoint1 <- newEndPoint transport - Right endpoint2 <- newEndPoint transport - Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - - -- Close the conneciton and try to send - close conn1 - Left (TransportError SendClosed _) <- send conn1 ["ping"] - - -- Close the first endpoint. We should not be able to use the first - -- connection anymore, or open more self connections, but the self connection - -- to the second endpoint should still be fine - closeEndPoint endpoint1 - Left (TransportError SendFailed _) <- send conn2 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right () <- send conn3 ["ping"] - - -- Close the transport; now the second should no longer work - closeTransport transport - Left (TransportError SendFailed _) <- send conn3 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - - return () - --- | Test various aspects of 'closeEndPoint' -testCloseEndPoint :: Transport -> Int -> IO () -testCloseEndPoint transport _ = do - serverDone <- newEmptyMVar - clientDone <- newEmptyMVar - clientAddr1 <- newEmptyMVar - clientAddr2 <- newEmptyMVar - serverAddr <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- First test (see client) - do - theirAddr <- readMVar clientAddr1 - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - return () - - -- Second test - do - theirAddr <- readMVar clientAddr2 - - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn ["pong"] - - ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' - ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint ; True <- return $ addr' == theirAddr - - Left (TransportError SendFailed _) <- send conn ["pong2"] - - return () - - putMVar serverDone () - - -- Client - forkTry $ do - theirAddr <- readMVar serverAddr - - -- First test: close endpoint with one outgoing but no incoming connections - do - Right endpoint <- newEndPoint transport - putMVar clientAddr1 (address endpoint) - - -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - closeEndPoint endpoint - EndPointClosed <- receive endpoint - return () - - -- Second test: close endpoint with one outgoing and one incoming connection - do - Right endpoint <- newEndPoint transport - putMVar clientAddr2 (address endpoint) - - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn ["ping"] - - -- Reply from the server - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' - - -- Close the endpoint - closeEndPoint endpoint - EndPointClosed <- receive endpoint - - -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- An attempt to close the already closed connection should just return - () <- close conn - - -- And so should an attempt to connect - Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - return () - - putMVar clientDone () - - mapM_ takeMVar [serverDone, clientDone] - --- Test closeTransport --- --- This tests many of the same things that testEndPoint does, and some more -testCloseTransport :: IO (Either String Transport) -> IO () -testCloseTransport newTransport = do - serverDone <- newEmptyMVar - clientDone <- newEmptyMVar - clientAddr1 <- newEmptyMVar - clientAddr2 <- newEmptyMVar - serverAddr <- newEmptyMVar - - -- Server - forkTry $ do - Right transport <- newTransport - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- Client sets up first endpoint - theirAddr1 <- readMVar clientAddr1 - ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 - - -- Client sets up second endpoint - theirAddr2 <- readMVar clientAddr2 - - ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 - Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 - - Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints - send conn ["pong"] - - -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) - evs <- replicateM 3 $ receive endpoint - let expected = [ ConnectionClosed cid1 - , ConnectionClosed cid2 - , ErrorEvent (TransportError (EventConnectionLost (Just theirAddr2) []) "") - ] - True <- return $ any (== expected) (permutations evs) - - -- An attempt to send to the endpoint should now fail - Left (TransportError SendFailed _) <- send conn ["pong2"] - - putMVar serverDone () - - -- Client - forkTry $ do - Right transport <- newTransport - theirAddr <- readMVar serverAddr - - -- Set up endpoint with one outgoing but no incoming connections - Right endpoint1 <- newEndPoint transport - putMVar clientAddr1 (address endpoint1) - - -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints - - -- Set up an endpoint with one outgoing and out incoming connection - Right endpoint2 <- newEndPoint transport - putMVar clientAddr2 (address endpoint2) - - Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints - send conn ["ping"] - - -- Reply from the server - ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr - Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' - - -- Now shut down the entire transport - closeTransport transport - - -- Both endpoints should report that they have been closed - EndPointClosed <- receive endpoint1 - EndPointClosed <- receive endpoint2 - - -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- An attempt to close the already closed connection should just return - () <- close conn - - -- And so should an attempt to connect on either endpoint - Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints - Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints - - -- And finally, so should an attempt to create a new endpoint - Left (TransportError NewEndPointFailed _) <- newEndPoint transport - - putMVar clientDone () - - mapM_ takeMVar [serverDone, clientDone] - --- | Remote node attempts to connect to a closed local endpoint -testConnectClosedEndPoint :: Transport -> IO () -testConnectClosedEndPoint transport = do - serverAddr <- newEmptyMVar - serverClosed <- newEmptyMVar - clientDone <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - closeEndPoint endpoint - putMVar serverClosed () - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - readMVar serverClosed - - Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - putMVar clientDone () - - takeMVar clientDone - --- | We should receive an exception when doing a 'receive' after we have been --- notified that an endpoint has been closed -testExceptionOnReceive :: IO (Either String Transport) -> IO () -testExceptionOnReceive newTransport = do - Right transport <- newTransport - - -- Test one: when we close an endpoint specifically - Right endpoint1 <- newEndPoint transport - closeEndPoint endpoint1 - EndPointClosed <- receive endpoint1 - Left _ <- trySome (receive endpoint1 >>= evaluate) - - -- Test two: when we close the entire transport - Right endpoint2 <- newEndPoint transport - closeTransport transport - EndPointClosed <- receive endpoint2 - Left _ <- trySome (receive endpoint2 >>= evaluate) - - return () - --- | Test what happens when the argument to 'send' is an exceptional value -testSendException :: IO (Either String Transport) -> IO () -testSendException newTransport = do - Right transport <- newTransport - Right endpoint1 <- newEndPoint transport - Right endpoint2 <- newEndPoint transport - - -- Connect endpoint1 to endpoint2 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - ConnectionOpened _ _ _ <- receive endpoint2 - - -- Send an exceptional value - Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") - - -- This will have been as a failure to send by endpoint1, which will - -- therefore have closed the socket. In turn this will have caused endpoint2 - -- to report that the connection was lost - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint1 - ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 - - -- A new connection will re-establish the connection - Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - send conn2 ["ping"] - close conn2 - - ConnectionOpened _ _ _ <- receive endpoint2 - Received _ ["ping"] <- receive endpoint2 - ConnectionClosed _ <- receive endpoint2 - - return () - --- | If threads get killed while executing a 'connect', 'send', or 'close', this --- should not affect other threads. --- --- The intention of this test is to see what happens when a asynchronous --- exception happes _while executing a send_. This is exceedingly difficult to --- guarantee, however. Hence we run a large number of tests and insert random --- thread delays -- and even then it might not happen. Moreover, it will only --- happen when we run on multiple cores. -testKill :: IO (Either String Transport) -> Int -> IO () -testKill newTransport numThreads = do - Right transport1 <- newTransport - Right transport2 <- newTransport - Right endpoint1 <- newEndPoint transport1 - Right endpoint2 <- newEndPoint transport2 - - threads <- replicateM numThreads . forkIO $ do - randomThreadDelay 100 - bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) - -- Note that we should not insert a randomThreadDelay into the - -- exception handler itself as this means that the exception handler - -- could be interrupted and we might not close - (\(Right conn) -> close conn) - (\(Right conn) -> do randomThreadDelay 100 - Right () <- send conn ["ping"] - randomThreadDelay 100) - - numAlive <- newMVar (0 :: Int) - - -- Kill half of those threads - forkIO . forM_ threads $ \tid -> do - shouldKill <- randomIO - if shouldKill - then randomThreadDelay 600 >> killThread tid - else modifyMVar_ numAlive (return . (+ 1)) - - -- Since it is impossible to predict when the kill exactly happens, we don't - -- know how many connects were opened and how many pings were sent. But we - -- should not have any open connections (if we do, collect will throw an - -- error) and we should have at least the number of pings equal to the number - -- of threads we did *not* kill - eventss <- collect endpoint2 Nothing (Just 1000000) - let actualPings = sum . map (length . snd) $ eventss - expectedPings <- takeMVar numAlive - unless (actualPings >= expectedPings) $ - throwIO (userError "Missing pings") - --- print (actualPings, expectedPings) - - --- | Set up conditions with a high likelyhood of "crossing" (for transports --- that multiplex lightweight connections across heavyweight connections) -testCrossing :: Transport -> Int -> IO () -testCrossing transport numRepeats = do - [aAddr, bAddr] <- replicateM 2 newEmptyMVar - [aDone, bDone] <- replicateM 2 newEmptyMVar - [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar - go <- newEmptyMVar - - let hints = defaultConnectHints { - connectTimeout = Just 5000000 - } - - -- A - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar aAddr (address endpoint) - theirAddress <- readMVar bAddr - - replicateM_ numRepeats $ do - takeMVar go >> yield - -- Because we are creating lots of connections, it's possible that - -- connect times out (for instance, in the TCP transport, - -- Network.Socket.connect may time out). We shouldn't regard this as an - -- error in the Transport, though. - connectResult <- connect endpoint theirAddress ReliableOrdered hints - case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar aTimeout () - Left (TransportError ConnectFailed _) -> readMVar bTimeout - Left err -> throwIO . userError $ "testCrossed: " ++ show err - putMVar aDone () - - -- B - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar bAddr (address endpoint) - theirAddress <- readMVar aAddr - - replicateM_ numRepeats $ do - takeMVar go >> yield - connectResult <- connect endpoint theirAddress ReliableOrdered hints - case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar bTimeout () - Left (TransportError ConnectFailed _) -> readMVar aTimeout - Left err -> throwIO . userError $ "testCrossed: " ++ show err - putMVar bDone () - - -- Driver - forM_ [1 .. numRepeats] $ \_i -> do - -- putStrLn $ "Round " ++ show _i - tryTakeMVar aTimeout - tryTakeMVar bTimeout - putMVar go () - putMVar go () - takeMVar aDone - takeMVar bDone - --- Transport tests -testTransport :: IO (Either String Transport) -> IO () -testTransport newTransport = do - Right transport <- newTransport - runTests - [ ("PingPong", testPingPong transport numPings) - , ("EndPoints", testEndPoints transport numPings) - , ("Connections", testConnections transport numPings) - , ("CloseOneConnection", testCloseOneConnection transport numPings) - , ("CloseOneDirection", testCloseOneDirection transport numPings) - , ("CloseReopen", testCloseReopen transport numPings) - , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport 1000) - , ("Crossing", testCrossing transport 100) - , ("CloseTwice", testCloseTwice transport 100) - , ("ConnectToSelf", testConnectToSelf transport numPings) - , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) - , ("CloseSelf", testCloseSelf newTransport) - , ("CloseEndPoint", testCloseEndPoint transport numPings) - , ("CloseTransport", testCloseTransport newTransport) - , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) - , ("ExceptionOnReceive", testExceptionOnReceive newTransport) - , ("SendException", testSendException newTransport) - , ("Kill", testKill newTransport 10000) - ] - where - numPings = 10000 :: Int diff --git a/tests/Traced.hs b/tests/Traced.hs deleted file mode 100644 index a7735efa..00000000 --- a/tests/Traced.hs +++ /dev/null @@ -1,191 +0,0 @@ --- | Add tracing to the IO monad (see examples). --- --- [Usage] --- --- > {-# LANGUAGE RebindableSyntax #-} --- > import Prelude hiding (catch, (>>=), (>>), return, fail) --- > import Traced --- --- [Example] --- --- > test1 :: IO Int --- > test1 = do --- > Left x <- return (Left 1 :: Either Int Int) --- > putStrLn "Hello world" --- > Right y <- return (Left 2 :: Either Int Int) --- > return (x + y) --- --- outputs --- --- > Hello world --- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) --- > Trace: --- > 0 Left 2 --- > 1 Left 1 --- --- [Guards] --- --- Use the following idiom instead of using 'Control.Monad.guard': --- --- > test2 :: IO Int --- > test2 = do --- > Left x <- return (Left 1 :: Either Int Int) --- > True <- return (x == 3) --- > return x --- --- The advantage of this idiom is that it gives you line number information when the guard fails: --- --- > *Traced> test2 --- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) --- > Trace: --- > 0 Left 1 -module Traced ( MonadS(..) - , return - , (>>=) - , (>>) - , fail - , ifThenElse - , Showable(..) - , Traceable(..) - , traceShow - ) where - -import Prelude hiding ((>>=), return, fail, catch, (>>)) -import qualified Prelude -import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) -import Control.Applicative ((<$>)) -import Data.Typeable (Typeable) -import Data.Maybe (catMaybes) -import Data.ByteString (ByteString) -import Data.Int (Int32) -import Control.Concurrent.MVar (MVar) - --------------------------------------------------------------------------------- --- MonadS class -- --------------------------------------------------------------------------------- - --- | Like 'Monad' but bind is only defined for 'Trace'able instances -class MonadS m where - returnS :: a -> m a - bindS :: Traceable a => m a -> (a -> m b) -> m b - failS :: String -> m a - seqS :: m a -> m b -> m b - --- | Redefinition of 'Prelude.>>=' -(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b -(>>=) = bindS - --- | Redefinition of 'Prelude.>>' -(>>) :: MonadS m => m a -> m b -> m b -(>>) = seqS - --- | Redefinition of 'Prelude.return' -return :: MonadS m => a -> m a -return = returnS - --- | Redefinition of 'Prelude.fail' -fail :: MonadS m => String -> m a -fail = failS - --------------------------------------------------------------------------------- --- Trace typeclass (for adding elements to a trace -- --------------------------------------------------------------------------------- - -data Showable = forall a. Show a => Showable a - -instance Show Showable where - show (Showable x) = show x - -mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable -mapShowable f (Showable x) = f x - -traceShow :: Show a => a -> Maybe Showable -traceShow = Just . Showable - -class Traceable a where - trace :: a -> Maybe Showable - -instance (Traceable a, Traceable b) => Traceable (Either a b) where - trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x - trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y - -instance (Traceable a, Traceable b) => Traceable (a, b) where - trace (x, y) = case (trace x, trace y) of - (Nothing, Nothing) -> Nothing - (Just t1, Nothing) -> traceShow t1 - (Nothing, Just t2) -> traceShow t2 - (Just t1, Just t2) -> traceShow (t1, t2) - -instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where - trace (x, y, z) = case (trace x, trace y, trace z) of - (Nothing, Nothing, Nothing) -> Nothing - (Just t1, Nothing, Nothing) -> traceShow t1 - (Nothing, Just t2, Nothing) -> traceShow t2 - (Just t1, Just t2, Nothing) -> traceShow (t1, t2) - (Nothing, Nothing, Just t3) -> traceShow t3 - (Just t1, Nothing, Just t3) -> traceShow (t1, t3) - (Nothing, Just t2, Just t3) -> traceShow (t2, t3) - (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) - -instance Traceable a => Traceable (Maybe a) where - trace Nothing = traceShow (Nothing :: Maybe ()) - trace (Just x) = mapShowable (Showable . Just) <$> trace x - -instance Traceable a => Traceable [a] where - trace = traceShow . catMaybes . map trace - -instance Traceable () where - trace = const Nothing - -instance Traceable Int where - trace = traceShow - -instance Traceable Int32 where - trace = traceShow - -instance Traceable Bool where - trace = const Nothing - -instance Traceable ByteString where - trace = traceShow - -instance Traceable (MVar a) where - trace = const Nothing - -instance Traceable [Char] where - trace = traceShow - -instance Traceable IOException where - trace = traceShow - --------------------------------------------------------------------------------- --- IO instance for MonadS -- --------------------------------------------------------------------------------- - -data TracedException = TracedException [String] SomeException - deriving Typeable - -instance Exception TracedException - --- | Add tracing to 'IO' (see examples) -instance MonadS IO where - returnS = Prelude.return - bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) - failS = Prelude.fail - seqS = (Prelude.>>) - -instance Show TracedException where - show (TracedException ts ex) = - show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) - -traceHandlers :: Traceable a => a -> [Handler b] -traceHandlers a = case trace a of - Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] - Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex - , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) - ] - --- | Definition of 'ifThenElse' for use with RebindableSyntax -ifThenElse :: Bool -> a -> a -> a -ifThenElse True x _ = x -ifThenElse False _ y = y From a0f277b90305c4c4d809b42d736d53745ee04a7d Mon Sep 17 00:00:00 2001 From: ghc704 Date: Fri, 6 Jul 2012 19:25:31 +0100 Subject: [PATCH 0110/2357] Split Network.Transport. THIS BREAKS THE CH BUILD. Starting to prepare for release. Have not yet updated the CH build to reflect the changes. --- LICENSE | 31 + Setup.hs | 2 + network-transport-inmemory.cabal | 64 +++ src/Network/Transport/Chan.hs | 157 +++++ tests/TestAuxiliary.hs | 108 ++++ tests/TestInMemory.hs | 8 + tests/TestMulticast.hs | 72 +++ tests/TestMulticastInMemory.hs | 7 + tests/TestTransport.hs | 956 +++++++++++++++++++++++++++++++ tests/Traced.hs | 191 ++++++ 10 files changed, 1596 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 network-transport-inmemory.cabal create mode 100644 src/Network/Transport/Chan.hs create mode 100644 tests/TestAuxiliary.hs create mode 100644 tests/TestInMemory.hs create mode 100644 tests/TestMulticast.hs create mode 100644 tests/TestMulticastInMemory.hs create mode 100644 tests/TestTransport.hs create mode 100644 tests/Traced.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f3459e44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011-2012 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/network-transport-inmemory.cabal b/network-transport-inmemory.cabal new file mode 100644 index 00000000..a1b6e282 --- /dev/null +++ b/network-transport-inmemory.cabal @@ -0,0 +1,64 @@ +Name: network-transport-inmemory +Version: 0.2.0 +Cabal-Version: >=1.8 +Build-Type: Simple +License: BSD3 +License-file: LICENSE +Copyright: Well-Typed LLP +Author: Duncan Coutts, Nicolas Wu, Edsko de Vries +Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process +Bug-Reports: mailto:edsko@well-typed.com +Synopsis: In-memory instantation of Network.Transport +Description: In-memory instantation of Network.Transport +Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 +Category: Network + +Library + Build-Depends: base >= 4.3 && < 5, + network-transport >= 0.2 && < 0.3, + data-accessor >= 0.2 && < 0.3, + bytestring >= 0.9 && < 0.10, + containers >= 0.4 && < 0.5 + Exposed-modules: Network.Transport.Chan + ghc-options: -Wall -fno-warn-unused-do-bind + HS-Source-Dirs: src + +Test-Suite TestMulticastInMemory + Type: exitcode-stdio-1.0 + Build-Depends: base >= 4.3 && < 5, + network-transport >= 0.2 && < 0.3, + data-accessor >= 0.2 && < 0.3, + bytestring >= 0.9 && < 0.10, + containers >= 0.4 && < 0.5, + random >= 1.0 && < 1.1, + ansi-terminal >= 0.5 && < 0.6 + Main-Is: TestMulticastInMemory.hs + ghc-options: -Wall -fno-warn-unused-do-bind + Extensions: ExistentialQuantification, + FlexibleInstances, + DeriveDataTypeable, + RankNTypes, + OverloadedStrings + HS-Source-Dirs: tests src + +Test-Suite TestInMemory + Type: exitcode-stdio-1.0 + Build-Depends: base >= 4.3 && < 5, + network-transport >= 0.2 && < 0.3, + data-accessor >= 0.2 && < 0.3, + bytestring >= 0.9 && < 0.10, + containers >= 0.4 && < 0.5, + random >= 1.0 && < 1.1, + ansi-terminal >= 0.5 && < 0.6, + mtl >= 2.0 && < 2.2 + Main-Is: TestInMemory.hs + ghc-options: -Wall -fno-warn-unused-do-bind + Extensions: ExistentialQuantification, + FlexibleInstances, + DeriveDataTypeable, + RankNTypes, + OverloadedStrings, + OverlappingInstances + HS-Source-Dirs: tests src diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs new file mode 100644 index 00000000..1c194e24 --- /dev/null +++ b/src/Network/Transport/Chan.hs @@ -0,0 +1,157 @@ +-- | In-memory implementation of the Transport API. +module Network.Transport.Chan (createTransport) where + +import Network.Transport +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) +import Control.Applicative ((<$>)) +import Control.Category ((>>>)) +import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_, readMVar) +import Control.Exception (throwIO) +import Control.Monad (forM_, when) +import Data.Map (Map) +import qualified Data.Map as Map (empty, insert, size, delete, findWithDefault) +import Data.Set (Set) +import qualified Data.Set as Set (empty, elems, insert, delete) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as BSC (pack) +import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) +import qualified Data.Accessor.Container as DAC (mapMaybe) + +-- Global state: next available "address", mapping from addresses to channels and next available connection +data TransportState = State { _channels :: Map EndPointAddress (Chan Event) + , _nextConnectionId :: Map EndPointAddress ConnectionId + , _multigroups :: Map MulticastAddress (MVar (Set EndPointAddress)) + } + +-- | Create a new Transport. +-- +-- Only a single transport should be created per Haskell process +-- (threads can, and should, create their own endpoints though). +createTransport :: IO Transport +createTransport = do + state <- newMVar State { _channels = Map.empty + , _nextConnectionId = Map.empty + , _multigroups = Map.empty + } + return Transport { newEndPoint = apiNewEndPoint state + , closeTransport = throwIO (userError "closeEndPoint not implemented") + } + +-- | Create a new end point +apiNewEndPoint :: MVar TransportState -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) +apiNewEndPoint state = do + chan <- newChan + addr <- modifyMVar state $ \st -> do + let addr = EndPointAddress . BSC.pack . show . Map.size $ st ^. channels + return ((channelAt addr ^= chan) . (nextConnectionIdAt addr ^= 1) $ st, addr) + return . Right $ EndPoint { receive = readChan chan + , address = addr + , connect = apiConnect addr state + , closeEndPoint = throwIO (userError "closeEndPoint not implemented") + , newMulticastGroup = apiNewMulticastGroup state addr + , resolveMulticastGroup = apiResolveMulticastGroup state addr + } + +-- | Create a new connection +apiConnect :: EndPointAddress + -> MVar TransportState + -> EndPointAddress + -> Reliability + -> ConnectHints + -> IO (Either (TransportError ConnectErrorCode) Connection) +apiConnect myAddress state theirAddress _reliability _hints = do + (chan, conn) <- modifyMVar state $ \st -> do + let chan = st ^. channelAt theirAddress + let conn = st ^. nextConnectionIdAt theirAddress + return (nextConnectionIdAt theirAddress ^: (+ 1) $ st, (chan, conn)) + writeChan chan $ ConnectionOpened conn ReliableOrdered myAddress + connAlive <- newMVar True + return . Right $ Connection { send = apiSend chan conn connAlive + , close = apiClose chan conn connAlive + } + +-- | Send a message over a connection +apiSend :: Chan Event -> ConnectionId -> MVar Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) +apiSend chan conn connAlive msg = + modifyMVar connAlive $ \alive -> + if alive + then do + writeChan chan (Received conn msg) + return (alive, Right ()) + else + return (alive, Left (TransportError SendFailed "Connection closed")) + +-- | Close a connection +apiClose :: Chan Event -> ConnectionId -> MVar Bool -> IO () +apiClose chan conn connAlive = + modifyMVar_ connAlive $ \alive -> do + when alive . writeChan chan $ ConnectionClosed conn + return False + +-- | Create a new multicast group +apiNewMulticastGroup :: MVar TransportState -> EndPointAddress -> IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) +apiNewMulticastGroup state ourAddress = do + group <- newMVar Set.empty + groupAddr <- modifyMVar state $ \st -> do + let addr = MulticastAddress . BSC.pack . show . Map.size $ st ^. multigroups + return (multigroupAt addr ^= group $ st, addr) + return . Right $ createMulticastGroup state ourAddress groupAddr group + +-- | Construct a multicast group +-- +-- When the group is deleted some endpoints may still receive messages, but +-- subsequent calls to resolveMulticastGroup will fail. This mimicks the fact +-- that some multicast messages may still be in transit when the group is +-- deleted. +createMulticastGroup :: MVar TransportState -> EndPointAddress -> MulticastAddress -> MVar (Set EndPointAddress) -> MulticastGroup +createMulticastGroup state ourAddress groupAddress group = + MulticastGroup { multicastAddress = groupAddress + , deleteMulticastGroup = modifyMVar_ state $ return . (multigroups ^: Map.delete groupAddress) + , maxMsgSize = Nothing + , multicastSend = \payload -> do + cs <- (^. channels) <$> readMVar state + es <- readMVar group + forM_ (Set.elems es) $ \ep -> do + let ch = cs ^. at ep "Invalid endpoint" + writeChan ch (ReceivedMulticast groupAddress payload) + , multicastSubscribe = modifyMVar_ group $ return . Set.insert ourAddress + , multicastUnsubscribe = modifyMVar_ group $ return . Set.delete ourAddress + , multicastClose = return () + } + +-- | Resolve a multicast group +apiResolveMulticastGroup :: MVar TransportState + -> EndPointAddress + -> MulticastAddress + -> IO (Either (TransportError ResolveMulticastGroupErrorCode) MulticastGroup) +apiResolveMulticastGroup state ourAddress groupAddress = do + group <- (^. (multigroups >>> DAC.mapMaybe groupAddress)) <$> readMVar state + case group of + Nothing -> return . Left $ TransportError ResolveMulticastGroupNotFound ("Group " ++ show groupAddress ++ " not found") + Just mvar -> return . Right $ createMulticastGroup state ourAddress groupAddress mvar + +-------------------------------------------------------------------------------- +-- Lens definitions -- +-------------------------------------------------------------------------------- + +channels :: Accessor TransportState (Map EndPointAddress (Chan Event)) +channels = accessor _channels (\ch st -> st { _channels = ch }) + +nextConnectionId :: Accessor TransportState (Map EndPointAddress ConnectionId) +nextConnectionId = accessor _nextConnectionId (\cid st -> st { _nextConnectionId = cid }) + +multigroups :: Accessor TransportState (Map MulticastAddress (MVar (Set EndPointAddress))) +multigroups = accessor _multigroups (\gs st -> st { _multigroups = gs }) + +at :: Ord k => k -> String -> Accessor (Map k v) v +at k err = accessor (Map.findWithDefault (error err) k) (Map.insert k) + +channelAt :: EndPointAddress -> Accessor TransportState (Chan Event) +channelAt addr = channels >>> at addr "Invalid channel" + +nextConnectionIdAt :: EndPointAddress -> Accessor TransportState ConnectionId +nextConnectionIdAt addr = nextConnectionId >>> at addr "Invalid connection ID" + +multigroupAt :: MulticastAddress -> Accessor TransportState (MVar (Set EndPointAddress)) +multigroupAt addr = multigroups >>> at addr "Invalid multigroup" + diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs new file mode 100644 index 00000000..d912ee6e --- /dev/null +++ b/tests/TestAuxiliary.hs @@ -0,0 +1,108 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module TestAuxiliary ( -- Running tests + runTest + , runTests + -- Writing tests + , forkTry + , trySome + , randomThreadDelay + ) where + +import Prelude hiding (catch) +import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) +import Control.Concurrent.Chan (Chan) +import Control.Monad (liftM2, unless) +import Control.Exception (SomeException, try, catch) +import System.Timeout (timeout) +import System.IO (stdout, hFlush) +import System.Console.ANSI ( SGR(SetColor, Reset) + , Color(Red, Green) + , ConsoleLayer(Foreground) + , ColorIntensity(Vivid) + , setSGR + ) +import System.Random (randomIO) +import Network.Transport +import Traced (Traceable(..), traceShow) + +-- | Like fork, but throw exceptions in the child thread to the parent +forkTry :: IO () -> IO ThreadId +forkTry p = do + tid <- myThreadId + forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) + +-- | Like try, but specialized to SomeException +trySome :: IO a -> IO (Either SomeException a) +trySome = try + +-- | Run the given test, catching timeouts and exceptions +runTest :: String -> IO () -> IO Bool +runTest description test = do + putStr $ "Running " ++ show description ++ ": " + hFlush stdout + done <- try . timeout 60000000 $ test -- 60 seconds + case done of + Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" + Right Nothing -> failed $ "(timeout)" + Right (Just ()) -> ok + where + failed :: String -> IO Bool + failed err = do + setSGR [SetColor Foreground Vivid Red] + putStr "failed " + setSGR [Reset] + putStrLn err + return False + + ok :: IO Bool + ok = do + setSGR [SetColor Foreground Vivid Green] + putStrLn "ok" + setSGR [Reset] + return True + +-- | Run a bunch of tests and throw an exception if any fails +runTests :: [(String, IO ())] -> IO () +runTests tests = do + success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests + unless success $ fail "Some tests failed" + +-- | Random thread delay between 0 and the specified max +randomThreadDelay :: Int -> IO () +randomThreadDelay maxDelay = do + delay <- randomIO :: IO Int + threadDelay (delay `mod` maxDelay) + +-------------------------------------------------------------------------------- +-- traceShow instances -- +-------------------------------------------------------------------------------- + +instance Traceable EndPoint where + trace = const Nothing + +instance Traceable Transport where + trace = const Nothing + +instance Traceable Connection where + trace = const Nothing + +instance Traceable Event where + trace = traceShow + +instance Show err => Traceable (TransportError err) where + trace = traceShow + +instance Traceable EndPointAddress where + trace = traceShow . endPointAddressToByteString + +instance Traceable SomeException where + trace = traceShow + +instance Traceable ThreadId where + trace = const Nothing + +instance Traceable (Chan a) where + trace = const Nothing + +instance Traceable Float where + trace = traceShow diff --git a/tests/TestInMemory.hs b/tests/TestInMemory.hs new file mode 100644 index 00000000..f7b6f70f --- /dev/null +++ b/tests/TestInMemory.hs @@ -0,0 +1,8 @@ +module Main where + +import TestTransport +import Network.Transport.Chan +import Control.Applicative ((<$>)) + +main :: IO () +main = testTransport (Right <$> createTransport) diff --git a/tests/TestMulticast.hs b/tests/TestMulticast.hs new file mode 100644 index 00000000..43eb526d --- /dev/null +++ b/tests/TestMulticast.hs @@ -0,0 +1,72 @@ +module TestMulticast where + +import Network.Transport +import TestAuxiliary (runTests) +import Control.Monad (replicateM, replicateM_, forM_, when) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar, readMVar) +import Data.ByteString (ByteString) +import Data.List (elemIndex) + +-- | Node for the "No confusion" test +noConfusionNode :: Transport -- ^ Transport + -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to + -> [MVar ()] -- ^ I'm ready : others ready + -> Int -- ^ number of pings + -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') + -> MVar () -- ^ I'm done + -> IO () +noConfusionNode transport groups ready numPings msgs done = do + -- Create a new endpoint + Right endpoint <- newEndPoint transport + + -- Create a new multicast group and broadcast its address + Right myGroup <- newMulticastGroup endpoint + putMVar (head groups) (multicastAddress myGroup) + + -- Subscribe to the given multicast groups + addrs <- mapM readMVar (tail groups) + forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr + multicastSubscribe group + + -- Indicate that we're ready and wait for everybody else to be ready + putMVar (head ready) () + mapM_ readMVar (tail ready) + + -- Send messages.. + forkIO . replicateM_ numPings $ multicastSend myGroup [head msgs] + + -- ..while checking that the messages we receive are the right ones + replicateM_ (2 * numPings) $ do + event <- receive endpoint + case event of + ReceivedMulticast addr [msg] -> + let mix = addr `elemIndex` addrs in + case mix of + Nothing -> error "Message from unexpected source" + Just ix -> when (msgs !! (ix + 1) /= msg) $ error "Unexpected message" + _ -> + error "Unexpected event" + + -- Success + putMVar done () + +-- | Test that distinct multicast groups are not confused +testNoConfusion :: Transport -> Int -> IO () +testNoConfusion transport numPings = do + [group1, group2, group3] <- replicateM 3 newEmptyMVar + [readyA, readyB, readyC] <- replicateM 3 newEmptyMVar + [doneA, doneB, doneC] <- replicateM 3 newEmptyMVar + let [msgA, msgB, msgC] = ["A says hi", "B says hi", "C says hi"] + + forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA + forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB + forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC + + mapM_ takeMVar [doneA, doneB, doneC] + +-- | Test multicast +testMulticast :: Transport -> IO () +testMulticast transport = + runTests + [ ("NoConfusion", testNoConfusion transport 10000) ] diff --git a/tests/TestMulticastInMemory.hs b/tests/TestMulticastInMemory.hs new file mode 100644 index 00000000..8494af64 --- /dev/null +++ b/tests/TestMulticastInMemory.hs @@ -0,0 +1,7 @@ +module Main where + +import TestMulticast +import Network.Transport.Chan (createTransport) + +main :: IO () +main = createTransport >>= testMulticast diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs new file mode 100644 index 00000000..e528e327 --- /dev/null +++ b/tests/TestTransport.hs @@ -0,0 +1,956 @@ +{-# LANGUAGE RebindableSyntax #-} +module TestTransport where + +import Prelude hiding (catch, (>>=), (>>), return, fail) +import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) +import Control.Concurrent (forkIO, killThread, yield) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) +import Control.Exception (evaluate, throw, throwIO, bracket) +import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) +import Control.Monad.Error () +import Control.Applicative ((<$>)) +import Network.Transport +import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) +import Network.Transport.Util (spawn) +import System.Random (randomIO) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack) +import Data.Map (Map) +import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) +import Data.String (fromString) +import Data.List (permutations) +import Traced + +-- | Server that echoes messages straight back to the origin endpoint. +echoServer :: EndPoint -> IO () +echoServer endpoint = do + go Map.empty + where + go :: Map ConnectionId Connection -> IO () + go cs = do + event <- receive endpoint + case event of + ConnectionOpened cid rel addr -> do + tlog $ "Opened new connection " ++ show cid + Right conn <- connect endpoint addr rel defaultConnectHints + go (Map.insert cid conn cs) + Received cid payload -> do + send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload + go cs + ConnectionClosed cid -> do + tlog $ "Close connection " ++ show cid + close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) + go (Map.delete cid cs) + ReceivedMulticast _ _ -> + -- Ignore + go cs + ErrorEvent _ -> + putStrLn $ "Echo server received error event: " ++ show event + EndPointClosed -> + return () + +-- | Ping client used in a few tests +ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () +ping endpoint server numPings msg = do + -- Open connection to the server + tlog "Connect to echo server" + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Wait for the server to open reply connection + tlog "Wait for ConnectionOpened message" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings and wait for reply + tlog "Send ping and wait for reply" + replicateM_ numPings $ do + send conn [msg] + Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg + return () + + -- Close the connection + tlog "Close the connection" + close conn + + -- Wait for the server to close its connection to us + tlog "Wait for ConnectionClosed message" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + -- Done + tlog "Ping client done" + +-- | Basic ping test +testPingPong :: Transport -> Int -> IO () +testPingPong transport numPings = do + tlog "Starting ping pong test" + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + tlog "Ping client" + Right endpoint <- newEndPoint transport + ping endpoint server numPings "ping" + putMVar result () + + takeMVar result + +-- | Test that endpoints don't get confused +testEndPoints :: Transport -> Int -> IO () +testEndPoints transport numPings = do + server <- spawn transport echoServer + dones <- replicateM 2 newEmptyMVar + + forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do + let name' :: ByteString + name' = pack [name] + Right endpoint <- newEndPoint transport + tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) + ping endpoint server numPings name' + putMVar done () + + forM_ dones takeMVar + +-- Test that connections don't get confused +testConnections :: Transport -> Int -> IO () +testConnections transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ replicateM_ numPings $ send conn1 ["pingA"] + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ numPings $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (2 * numPings) + + takeMVar result + +-- | Test that closing one connection does not close the other +testCloseOneConnection :: Transport -> Int -> IO () +testCloseOneConnection transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ do + replicateM_ numPings $ send conn1 ["pingA"] + close conn1 + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (3 * numPings) + + takeMVar result + +-- | Test that if A connects to B and B connects to A, B can still send to A after +-- A closes its connection to B (for instance, in the TCP transport, the socket pair +-- connecting A and B should not yet be closed). +testCloseOneDirection :: Transport -> Int -> IO () +testCloseOneDirection transport numPings = do + addrA <- newEmptyMVar + addrB <- newEmptyMVar + doneA <- newEmptyMVar + doneB <- newEmptyMVar + + -- A + forkTry $ do + tlog "A" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrA (address endpoint) + + -- Connect to B + tlog "Connect to B" + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + -- Wait for B to connect to us + tlog "Wait for B" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings to B + tlog "Send pings to B" + replicateM_ numPings $ send conn ["ping"] + + -- Close our connection to B + tlog "Close connection" + close conn + + -- Wait for B's pongs + tlog "Wait for pongs from B" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for B to close it's connection to us + tlog "Wait for B to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Done + tlog "Done" + putMVar doneA () + + -- B + forkTry $ do + tlog "B" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrB (address endpoint) + + -- Wait for A to connect + tlog "Wait for A to connect" + ConnectionOpened cid _ _ <- receive endpoint + + -- Connect to A + tlog "Connect to A" + Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + -- Wait for A's pings + tlog "Wait for pings from A" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for A to close it's connection to us + tlog "Wait for A to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Send pongs to A + tlog "Send pongs to A" + replicateM_ numPings $ send conn ["pong"] + + -- Close our connection to A + tlog "Close connection to A" + close conn + + -- Done + tlog "Done" + putMVar doneB () + + mapM_ takeMVar [doneA, doneB] + +-- | Collect events and order them by connection ID +collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] +collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty + where + -- TODO: for more serious use of this function we'd need to make these arguments strict + go (Just 0) open closed = finish open closed + go n open closed = do + mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint + case mEvent of + Left _ -> finish open closed + Right event -> do + let n' = (\x -> x - 1) <$> n + case event of + ConnectionOpened cid _ _ -> + go n' (Map.insert cid [] open) closed + ConnectionClosed cid -> + let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in + go n' (Map.delete cid open) (Map.insert cid list closed) + Received cid msg -> + go n' (Map.adjust (msg :) cid open) closed + ReceivedMulticast _ _ -> + fail "Unexpected multicast" + ErrorEvent _ -> + fail "Unexpected error" + EndPointClosed -> + fail "Unexpected endpoint closure" + + finish open closed = + if Map.null open + then return . Map.toList . Map.map reverse $ closed + else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) + +-- | Open connection, close it, then reopen it +-- (In the TCP transport this means the socket will be closed, then reopened) +-- +-- Note that B cannot expect to receive all of A's messages on the first connection +-- before receiving the messages on the second connection. What might (and sometimes +-- does) happen is that finishes sending all of its messages on the first connection +-- (in the TCP transport, the first socket pair) while B is behind on reading _from_ +-- this connection (socket pair) -- the messages are "in transit" on the network +-- (these tests are done on localhost, so there are in some OS buffer). Then when +-- A opens the second connection (socket pair) B will spawn a new thread for this +-- connection, and hence might start interleaving messages from the first and second +-- connection. +-- +-- This is correct behaviour, however: the transport API guarantees reliability and +-- ordering _per connection_, but not _across_ connections. +testCloseReopen :: Transport -> Int -> IO () +testCloseReopen transport numPings = do + addrB <- newEmptyMVar + doneB <- newEmptyMVar + + let numRepeats = 2 :: Int + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + + forM_ [1 .. numRepeats] $ \i -> do + tlog "A connecting" + -- Connect to B + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + tlog "A pinging" + -- Say hi + forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] + + tlog "A closing" + -- Disconnect again + close conn + + tlog "A finishing" + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar addrB (address endpoint) + + eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing + + forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do + forM_ (zip [1 .. numPings] events) $ \(j, event) -> do + guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) + + putMVar doneB () + + takeMVar doneB + +-- | Test lots of parallel connection attempts +testParallelConnects :: Transport -> Int -> IO () +testParallelConnects transport numPings = do + server <- spawn transport echoServer + done <- newEmptyMVar + + Right endpoint <- newEndPoint transport + + -- Spawn lots of clients + forM_ [1 .. numPings] $ \i -> forkTry $ do + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints + send conn [pack $ "ping" ++ show i] + send conn [pack $ "ping" ++ show i] + close conn + + forkTry $ do + eventss <- collect endpoint (Just (numPings * 4)) Nothing + -- Check that no pings got sent to the wrong connection + forM_ eventss $ \(_, [[ping1], [ping2]]) -> + guard (ping1 == ping2) + putMVar done () + + takeMVar done + +-- | Test that sending on a closed connection gives an error +testSendAfterClose :: Transport -> Int -> IO () +testSendAfterClose transport numRepeats = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + -- We request two lightweight connections + replicateM numRepeats $ do + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Close the second, but leave the first open; then output on the second + -- connection (i.e., on a closed connection while there is still another + -- connection open) + close conn2 + Left (TransportError SendClosed _) <- send conn2 ["ping2"] + + -- Now close the first connection, and output on it (i.e., output while + -- there are no lightweight connection at all anymore) + close conn1 + Left (TransportError SendClosed _) <- send conn2 ["ping2"] + + return () + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that closing the same connection twice has no effect +testCloseTwice :: Transport -> Int -> IO () +testCloseTwice transport numRepeats = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + replicateM numRepeats $ do + -- We request two lightweight connections + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Close the second one twice + close conn2 + close conn2 + + -- Then send a message on the first and close that twice too + send conn1 ["ping"] + close conn1 + + -- Verify expected response from the echo server + ConnectionOpened cid1 _ _ <- receive endpoint + ConnectionOpened cid2 _ _ <- receive endpoint + ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 + Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 + ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 + + return () + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that we can connect an endpoint to itself +testConnectToSelf :: Transport -> Int -> IO () +testConnectToSelf transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + + tlog "Talk to myself" + + -- One thread to write to the endpoint + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn ["ping"] + + tlog $ "Closing connection" + close conn + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + tlog "Waiting for ConnectionOpened" + ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint + + tlog "Waiting for Received" + replicateM_ numPings $ do + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + return () + + tlog "Waiting for ConnectionClosed" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we can connect an endpoint to itself multiple times +testConnectToSelfTwice :: Transport -> Int -> IO () +testConnectToSelfTwice transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + + tlog "Talk to myself" + + -- One thread to write to the endpoint using the first connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn1 ["pingA"] + + tlog $ "Closing connection" + close conn1 + + -- One thread to write to the endpoint using the second connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn2 ["pingB"] + + tlog $ "Closing connection" + close conn2 + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing + True <- return $ events1 == replicate numPings ["pingA"] + True <- return $ events2 == replicate numPings ["pingB"] + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we self-connections no longer work once we close our endpoint +-- or our transport +testCloseSelf :: IO (Either String Transport) -> IO () +testCloseSelf newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints + + -- Close the conneciton and try to send + close conn1 + Left (TransportError SendClosed _) <- send conn1 ["ping"] + + -- Close the first endpoint. We should not be able to use the first + -- connection anymore, or open more self connections, but the self connection + -- to the second endpoint should still be fine + closeEndPoint endpoint1 + Left (TransportError SendFailed _) <- send conn2 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right () <- send conn3 ["ping"] + + -- Close the transport; now the second should no longer work + closeTransport transport + Left (TransportError SendFailed _) <- send conn3 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints + + return () + +-- | Test various aspects of 'closeEndPoint' +testCloseEndPoint :: Transport -> Int -> IO () +testCloseEndPoint transport _ = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- First test (see client) + do + theirAddr <- readMVar clientAddr1 + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + return () + + -- Second test + do + theirAddr <- readMVar clientAddr2 + + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn ["pong"] + + ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' + ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint ; True <- return $ addr' == theirAddr + + Left (TransportError SendFailed _) <- send conn ["pong2"] + + return () + + putMVar serverDone () + + -- Client + forkTry $ do + theirAddr <- readMVar serverAddr + + -- First test: close endpoint with one outgoing but no incoming connections + do + Right endpoint <- newEndPoint transport + putMVar clientAddr1 (address endpoint) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + closeEndPoint endpoint + EndPointClosed <- receive endpoint + return () + + -- Second test: close endpoint with one outgoing and one incoming connection + do + Right endpoint <- newEndPoint transport + putMVar clientAddr2 (address endpoint) + + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' + + -- Close the endpoint + closeEndPoint endpoint + EndPointClosed <- receive endpoint + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + return () + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- Test closeTransport +-- +-- This tests many of the same things that testEndPoint does, and some more +testCloseTransport :: IO (Either String Transport) -> IO () +testCloseTransport newTransport = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right transport <- newTransport + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Client sets up first endpoint + theirAddr1 <- readMVar clientAddr1 + ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 + + -- Client sets up second endpoint + theirAddr2 <- readMVar clientAddr2 + + ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 + Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 + + Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints + send conn ["pong"] + + -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) + evs <- replicateM 3 $ receive endpoint + let expected = [ ConnectionClosed cid1 + , ConnectionClosed cid2 + , ErrorEvent (TransportError (EventConnectionLost (Just theirAddr2) []) "") + ] + True <- return $ any (== expected) (permutations evs) + + -- An attempt to send to the endpoint should now fail + Left (TransportError SendFailed _) <- send conn ["pong2"] + + putMVar serverDone () + + -- Client + forkTry $ do + Right transport <- newTransport + theirAddr <- readMVar serverAddr + + -- Set up endpoint with one outgoing but no incoming connections + Right endpoint1 <- newEndPoint transport + putMVar clientAddr1 (address endpoint1) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + + -- Set up an endpoint with one outgoing and out incoming connection + Right endpoint2 <- newEndPoint transport + putMVar clientAddr2 (address endpoint2) + + Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' + + -- Now shut down the entire transport + closeTransport transport + + -- Both endpoints should report that they have been closed + EndPointClosed <- receive endpoint1 + EndPointClosed <- receive endpoint2 + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect on either endpoint + Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints + + -- And finally, so should an attempt to create a new endpoint + Left (TransportError NewEndPointFailed _) <- newEndPoint transport + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- | Remote node attempts to connect to a closed local endpoint +testConnectClosedEndPoint :: Transport -> IO () +testConnectClosedEndPoint transport = do + serverAddr <- newEmptyMVar + serverClosed <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + closeEndPoint endpoint + putMVar serverClosed () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + readMVar serverClosed + + Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + putMVar clientDone () + + takeMVar clientDone + +-- | We should receive an exception when doing a 'receive' after we have been +-- notified that an endpoint has been closed +testExceptionOnReceive :: IO (Either String Transport) -> IO () +testExceptionOnReceive newTransport = do + Right transport <- newTransport + + -- Test one: when we close an endpoint specifically + Right endpoint1 <- newEndPoint transport + closeEndPoint endpoint1 + EndPointClosed <- receive endpoint1 + Left _ <- trySome (receive endpoint1 >>= evaluate) + + -- Test two: when we close the entire transport + Right endpoint2 <- newEndPoint transport + closeTransport transport + EndPointClosed <- receive endpoint2 + Left _ <- trySome (receive endpoint2 >>= evaluate) + + return () + +-- | Test what happens when the argument to 'send' is an exceptional value +testSendException :: IO (Either String Transport) -> IO () +testSendException newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + + -- Connect endpoint1 to endpoint2 + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + ConnectionOpened _ _ _ <- receive endpoint2 + + -- Send an exceptional value + Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") + + -- This will have been as a failure to send by endpoint1, which will + -- therefore have closed the socket. In turn this will have caused endpoint2 + -- to report that the connection was lost + ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint1 + ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 + + -- A new connection will re-establish the connection + Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + send conn2 ["ping"] + close conn2 + + ConnectionOpened _ _ _ <- receive endpoint2 + Received _ ["ping"] <- receive endpoint2 + ConnectionClosed _ <- receive endpoint2 + + return () + +-- | If threads get killed while executing a 'connect', 'send', or 'close', this +-- should not affect other threads. +-- +-- The intention of this test is to see what happens when a asynchronous +-- exception happes _while executing a send_. This is exceedingly difficult to +-- guarantee, however. Hence we run a large number of tests and insert random +-- thread delays -- and even then it might not happen. Moreover, it will only +-- happen when we run on multiple cores. +testKill :: IO (Either String Transport) -> Int -> IO () +testKill newTransport numThreads = do + Right transport1 <- newTransport + Right transport2 <- newTransport + Right endpoint1 <- newEndPoint transport1 + Right endpoint2 <- newEndPoint transport2 + + threads <- replicateM numThreads . forkIO $ do + randomThreadDelay 100 + bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) + -- Note that we should not insert a randomThreadDelay into the + -- exception handler itself as this means that the exception handler + -- could be interrupted and we might not close + (\(Right conn) -> close conn) + (\(Right conn) -> do randomThreadDelay 100 + Right () <- send conn ["ping"] + randomThreadDelay 100) + + numAlive <- newMVar (0 :: Int) + + -- Kill half of those threads + forkIO . forM_ threads $ \tid -> do + shouldKill <- randomIO + if shouldKill + then randomThreadDelay 600 >> killThread tid + else modifyMVar_ numAlive (return . (+ 1)) + + -- Since it is impossible to predict when the kill exactly happens, we don't + -- know how many connects were opened and how many pings were sent. But we + -- should not have any open connections (if we do, collect will throw an + -- error) and we should have at least the number of pings equal to the number + -- of threads we did *not* kill + eventss <- collect endpoint2 Nothing (Just 1000000) + let actualPings = sum . map (length . snd) $ eventss + expectedPings <- takeMVar numAlive + unless (actualPings >= expectedPings) $ + throwIO (userError "Missing pings") + +-- print (actualPings, expectedPings) + + +-- | Set up conditions with a high likelyhood of "crossing" (for transports +-- that multiplex lightweight connections across heavyweight connections) +testCrossing :: Transport -> Int -> IO () +testCrossing transport numRepeats = do + [aAddr, bAddr] <- replicateM 2 newEmptyMVar + [aDone, bDone] <- replicateM 2 newEmptyMVar + [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar + go <- newEmptyMVar + + let hints = defaultConnectHints { + connectTimeout = Just 5000000 + } + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar aAddr (address endpoint) + theirAddress <- readMVar bAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + -- Because we are creating lots of connections, it's possible that + -- connect times out (for instance, in the TCP transport, + -- Network.Socket.connect may time out). We shouldn't regard this as an + -- error in the Transport, though. + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar aTimeout () + Left (TransportError ConnectFailed _) -> readMVar bTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err + putMVar aDone () + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar bAddr (address endpoint) + theirAddress <- readMVar aAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar bTimeout () + Left (TransportError ConnectFailed _) -> readMVar aTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err + putMVar bDone () + + -- Driver + forM_ [1 .. numRepeats] $ \_i -> do + -- putStrLn $ "Round " ++ show _i + tryTakeMVar aTimeout + tryTakeMVar bTimeout + putMVar go () + putMVar go () + takeMVar aDone + takeMVar bDone + +-- Transport tests +testTransport :: IO (Either String Transport) -> IO () +testTransport newTransport = do + Right transport <- newTransport + runTests + [ ("PingPong", testPingPong transport numPings) + , ("EndPoints", testEndPoints transport numPings) + , ("Connections", testConnections transport numPings) + , ("CloseOneConnection", testCloseOneConnection transport numPings) + , ("CloseOneDirection", testCloseOneDirection transport numPings) + , ("CloseReopen", testCloseReopen transport numPings) + , ("ParallelConnects", testParallelConnects transport numPings) + , ("SendAfterClose", testSendAfterClose transport 1000) + , ("Crossing", testCrossing transport 100) + , ("CloseTwice", testCloseTwice transport 100) + , ("ConnectToSelf", testConnectToSelf transport numPings) + , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) + , ("CloseSelf", testCloseSelf newTransport) + , ("CloseEndPoint", testCloseEndPoint transport numPings) + , ("CloseTransport", testCloseTransport newTransport) + , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) + , ("ExceptionOnReceive", testExceptionOnReceive newTransport) + , ("SendException", testSendException newTransport) + , ("Kill", testKill newTransport 10000) + ] + where + numPings = 10000 :: Int diff --git a/tests/Traced.hs b/tests/Traced.hs new file mode 100644 index 00000000..a7735efa --- /dev/null +++ b/tests/Traced.hs @@ -0,0 +1,191 @@ +-- | Add tracing to the IO monad (see examples). +-- +-- [Usage] +-- +-- > {-# LANGUAGE RebindableSyntax #-} +-- > import Prelude hiding (catch, (>>=), (>>), return, fail) +-- > import Traced +-- +-- [Example] +-- +-- > test1 :: IO Int +-- > test1 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > putStrLn "Hello world" +-- > Right y <- return (Left 2 :: Either Int Int) +-- > return (x + y) +-- +-- outputs +-- +-- > Hello world +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) +-- > Trace: +-- > 0 Left 2 +-- > 1 Left 1 +-- +-- [Guards] +-- +-- Use the following idiom instead of using 'Control.Monad.guard': +-- +-- > test2 :: IO Int +-- > test2 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > True <- return (x == 3) +-- > return x +-- +-- The advantage of this idiom is that it gives you line number information when the guard fails: +-- +-- > *Traced> test2 +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) +-- > Trace: +-- > 0 Left 1 +module Traced ( MonadS(..) + , return + , (>>=) + , (>>) + , fail + , ifThenElse + , Showable(..) + , Traceable(..) + , traceShow + ) where + +import Prelude hiding ((>>=), return, fail, catch, (>>)) +import qualified Prelude +import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) +import Control.Applicative ((<$>)) +import Data.Typeable (Typeable) +import Data.Maybe (catMaybes) +import Data.ByteString (ByteString) +import Data.Int (Int32) +import Control.Concurrent.MVar (MVar) + +-------------------------------------------------------------------------------- +-- MonadS class -- +-------------------------------------------------------------------------------- + +-- | Like 'Monad' but bind is only defined for 'Trace'able instances +class MonadS m where + returnS :: a -> m a + bindS :: Traceable a => m a -> (a -> m b) -> m b + failS :: String -> m a + seqS :: m a -> m b -> m b + +-- | Redefinition of 'Prelude.>>=' +(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b +(>>=) = bindS + +-- | Redefinition of 'Prelude.>>' +(>>) :: MonadS m => m a -> m b -> m b +(>>) = seqS + +-- | Redefinition of 'Prelude.return' +return :: MonadS m => a -> m a +return = returnS + +-- | Redefinition of 'Prelude.fail' +fail :: MonadS m => String -> m a +fail = failS + +-------------------------------------------------------------------------------- +-- Trace typeclass (for adding elements to a trace -- +-------------------------------------------------------------------------------- + +data Showable = forall a. Show a => Showable a + +instance Show Showable where + show (Showable x) = show x + +mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable +mapShowable f (Showable x) = f x + +traceShow :: Show a => a -> Maybe Showable +traceShow = Just . Showable + +class Traceable a where + trace :: a -> Maybe Showable + +instance (Traceable a, Traceable b) => Traceable (Either a b) where + trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x + trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y + +instance (Traceable a, Traceable b) => Traceable (a, b) where + trace (x, y) = case (trace x, trace y) of + (Nothing, Nothing) -> Nothing + (Just t1, Nothing) -> traceShow t1 + (Nothing, Just t2) -> traceShow t2 + (Just t1, Just t2) -> traceShow (t1, t2) + +instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where + trace (x, y, z) = case (trace x, trace y, trace z) of + (Nothing, Nothing, Nothing) -> Nothing + (Just t1, Nothing, Nothing) -> traceShow t1 + (Nothing, Just t2, Nothing) -> traceShow t2 + (Just t1, Just t2, Nothing) -> traceShow (t1, t2) + (Nothing, Nothing, Just t3) -> traceShow t3 + (Just t1, Nothing, Just t3) -> traceShow (t1, t3) + (Nothing, Just t2, Just t3) -> traceShow (t2, t3) + (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) + +instance Traceable a => Traceable (Maybe a) where + trace Nothing = traceShow (Nothing :: Maybe ()) + trace (Just x) = mapShowable (Showable . Just) <$> trace x + +instance Traceable a => Traceable [a] where + trace = traceShow . catMaybes . map trace + +instance Traceable () where + trace = const Nothing + +instance Traceable Int where + trace = traceShow + +instance Traceable Int32 where + trace = traceShow + +instance Traceable Bool where + trace = const Nothing + +instance Traceable ByteString where + trace = traceShow + +instance Traceable (MVar a) where + trace = const Nothing + +instance Traceable [Char] where + trace = traceShow + +instance Traceable IOException where + trace = traceShow + +-------------------------------------------------------------------------------- +-- IO instance for MonadS -- +-------------------------------------------------------------------------------- + +data TracedException = TracedException [String] SomeException + deriving Typeable + +instance Exception TracedException + +-- | Add tracing to 'IO' (see examples) +instance MonadS IO where + returnS = Prelude.return + bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) + failS = Prelude.fail + seqS = (Prelude.>>) + +instance Show TracedException where + show (TracedException ts ex) = + show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) + +traceHandlers :: Traceable a => a -> [Handler b] +traceHandlers a = case trace a of + Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] + Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex + , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) + ] + +-- | Definition of 'ifThenElse' for use with RebindableSyntax +ifThenElse :: Bool -> a -> a -> a +ifThenElse True x _ = x +ifThenElse False _ y = y From 44eb79b03d763c164c6fc86c192e6686bc0449dc Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 6 Jul 2012 20:23:11 +0100 Subject: [PATCH 0111/2357] Split off distributed-process-simplelocalnet And prepare for release --- LICENSE | 31 ++ Setup.hs | 2 + distributed-process-simplelocalnet.cabal | 58 ++++ .../Process/Backend/SimpleLocalnet.hs | 291 ++++++++++++++++++ .../SimpleLocalnet/Internal/Multicast.hs | 126 ++++++++ tests/TestSimpleLocalnet.hs | 24 ++ tests/runTestSimpleLocalnet.hs | 11 + 7 files changed, 543 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 distributed-process-simplelocalnet.cabal create mode 100644 src/Control/Distributed/Process/Backend/SimpleLocalnet.hs create mode 100644 src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs create mode 100644 tests/TestSimpleLocalnet.hs create mode 100755 tests/runTestSimpleLocalnet.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f3459e44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011-2012 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal new file mode 100644 index 00000000..178b3d7f --- /dev/null +++ b/distributed-process-simplelocalnet.cabal @@ -0,0 +1,58 @@ +Name: distributed-process-simplelocalnet +Version: 0.2.0 +Cabal-Version: >=1.8 +Build-Type: Simple +License: BSD3 +License-File: LICENSE +Copyright: Well-Typed LLP +Author: Duncan Coutts, Nicolas Wu, Edsko de Vries +Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process +Bug-Reports: mailto:edsko@well-typed.com +Synopsis: Simple zero-configuration backend for Cloud Haskell +Description: Simple backend based on the TCP transport which offers node + discovery based on UDP multicast. This is a zero-configuration + backend designed to get you going with Cloud Haskell quickly + without imposing any structure on your application. +Tested-With: GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 +Category: Control + +Library + Build-Depends: base >= 4.4 && < 5, + bytestring >= 0.9 && < 0.10, + network >= 2.3 && < 2.4, + network-multicast >= 0.0 && < 0.1, + data-accessor >= 0.2 && < 0.3, + binary >= 0.5 && < 0.6, + containers >= 0.4 && < 0.5, + transformers >= 0.2 && < 0.4, + network-transport >= 0.2 && < 0.3, + network-transport-tcp >= 0.2 && < 0.3, + distributed-process >= 0.2 && < 0.3 + Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, + Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast + Extensions: RankNTypes, + DeriveDataTypeable + ghc-options: -Wall + HS-Source-Dirs: src + +-- Not a proper test, but we want to use cabal to compile it +Test-Suite TestSimpleLocalnet + Type: exitcode-stdio-1.0 + Main-Is: TestSimpleLocalnet.hs + Build-Depends: base >= 4.4 && < 5, + bytestring >= 0.9 && < 0.10, + network >= 2.3 && < 2.4, + network-multicast >= 0.0 && < 0.1, + data-accessor >= 0.2 && < 0.3, + binary >= 0.5 && < 0.6, + containers >= 0.4 && < 0.5, + transformers >= 0.2 && < 0.4, + network-transport >= 0.2 && < 0.3, + network-transport-tcp >= 0.2 && < 0.3, + distributed-process >= 0.2 && < 0.3 + Extensions: RankNTypes, + DeriveDataTypeable + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + HS-Source-Dirs: tests src diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs new file mode 100644 index 00000000..68d284e5 --- /dev/null +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -0,0 +1,291 @@ +-- | Simple backend based on the TCP transport which offers node discovery +-- based on UDP multicast. This is a zero-configuration backend designed to +-- get you going with Cloud Haskell quickly without imposing any structure +-- on your application. +-- +-- To simplify getting started we provide special support for /master/ and +-- /slave/ nodes (see 'startSlave' and 'startMaster'). Use of these functions +-- is completely optional; you can use the local backend without making use +-- of the predefined master and slave nodes. +-- +-- [Minimal example] +-- +-- > import System.Environment (getArgs) +-- > import Control.Distributed.Process +-- > import Control.Distributed.Process.Node (initRemoteTable) +-- > import Control.Distributed.Process.Backend.Local +-- > +-- > master :: Backend -> [NodeId] -> Process () +-- > master backend slaves = do +-- > -- Do something interesting with the slaves +-- > liftIO . putStrLn $ "Slaves: " ++ show slaves +-- > -- Terminate the slaves when the master terminates (this is optional) +-- > terminateAllSlaves backend +-- > +-- > main :: IO () +-- > main = do +-- > args <- getArgs +-- > +-- > case args of +-- > ["master", host, port] -> do +-- > backend <- initializeBackend host port initRemoteTable +-- > startMaster backend (master backend) +-- > ["slave", host, port] -> do +-- > backend <- initializeBackend host port initRemoteTable +-- > startSlave backend +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Control.Distributed.Process.Backend.SimpleLocalnet + ( -- * Initialization + Backend(..) + , initializeBackend + -- * Slave nodes + , startSlave + , terminateSlave + , findSlaves + , terminateAllSlaves + -- * Master nodes + , startMaster + ) where + +import System.IO (fixIO) +import Data.Maybe (catMaybes) +import Data.Binary (Binary(get, put), getWord8, putWord8) +import Data.Accessor (Accessor, accessor, (^:), (^.)) +import Data.Set (Set) +import qualified Data.Set as Set (insert, empty, toList) +import Data.Foldable (forM_) +import Data.Typeable (Typeable) +import Control.Applicative ((<$>)) +import Control.Exception (throw) +import Control.Monad (forever, forM) +import Control.Monad.IO.Class (liftIO) +import Control.Concurrent (forkIO, threadDelay, ThreadId) +import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_) +import Control.Distributed.Process + ( RemoteTable + , NodeId + , Process + , WhereIsReply(..) + , whereis + , whereisRemoteAsync + , registerRemote + , getSelfPid + , register + , expect + , nsendRemote + , receiveWait + , matchIf + , processNodeId + ) +import qualified Control.Distributed.Process.Node as Node + ( LocalNode + , newLocalNode + , localNodeId + , runProcess + ) +import qualified Network.Transport.TCP as NT + ( createTransport + , defaultTCPParameters + ) +import qualified Network.Transport as NT (Transport) +import qualified Network.Socket as N (HostName, ServiceName, SockAddr) +import Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast (initMulticast) + +-- | Local backend +data Backend = Backend { + -- | Create a new local node + newLocalNode :: IO Node.LocalNode + -- | @findPeers t@ sends out a /who's there?/ request, waits 't' msec, + -- and then collects and returns the answers + , findPeers :: Int -> IO [NodeId] + -- | Make sure that all log messages are printed by the logger on the + -- current node + , redirectLogsHere :: Process () + } + +data BackendState = BackendState { + _localNodes :: [Node.LocalNode] + , _peers :: Set NodeId + , discoveryDaemon :: ThreadId + } + +-- | Initialize the backend +initializeBackend :: N.HostName -> N.ServiceName -> RemoteTable -> IO Backend +initializeBackend host port rtable = do + mTransport <- NT.createTransport host port NT.defaultTCPParameters + (recv, send) <- initMulticast "224.0.0.99" 9999 1024 + (_, backendState) <- fixIO $ \ ~(tid, _) -> do + backendState <- newMVar BackendState + { _localNodes = [] + , _peers = Set.empty + , discoveryDaemon = tid + } + tid' <- forkIO $ peerDiscoveryDaemon backendState recv send + return (tid', backendState) + case mTransport of + Left err -> throw err + Right transport -> + let backend = Backend { + newLocalNode = apiNewLocalNode transport rtable backendState + , findPeers = apiFindPeers send backendState + , redirectLogsHere = apiRedirectLogsHere backend + } + in return backend + +-- | Create a new local node +apiNewLocalNode :: NT.Transport + -> RemoteTable + -> MVar BackendState + -> IO Node.LocalNode +apiNewLocalNode transport rtable backendState = do + localNode <- Node.newLocalNode transport rtable + modifyMVar_ backendState $ return . (localNodes ^: (localNode :)) + return localNode + +-- | Peer discovery +apiFindPeers :: (PeerDiscoveryMsg -> IO ()) + -> MVar BackendState + -> Int + -> IO [NodeId] +apiFindPeers send backendState delay = do + send PeerDiscoveryRequest + threadDelay delay + Set.toList . (^. peers) <$> readMVar backendState + +data PeerDiscoveryMsg = + PeerDiscoveryRequest + | PeerDiscoveryReply NodeId + +instance Binary PeerDiscoveryMsg where + put PeerDiscoveryRequest = putWord8 0 + put (PeerDiscoveryReply nid) = putWord8 1 >> put nid + get = do + header <- getWord8 + case header of + 0 -> return PeerDiscoveryRequest + 1 -> PeerDiscoveryReply <$> get + _ -> fail "PeerDiscoveryMsg.get: invalid" + +-- | Respond to peer discovery requests sent by other nodes +peerDiscoveryDaemon :: MVar BackendState + -> IO (PeerDiscoveryMsg, N.SockAddr) + -> (PeerDiscoveryMsg -> IO ()) + -> IO () +peerDiscoveryDaemon backendState recv send = forever go + where + go = do + (msg, _) <- recv + case msg of + PeerDiscoveryRequest -> do + nodes <- (^. localNodes) <$> readMVar backendState + forM_ nodes $ send . PeerDiscoveryReply . Node.localNodeId + PeerDiscoveryReply nid -> + modifyMVar_ backendState $ return . (peers ^: Set.insert nid) + +-------------------------------------------------------------------------------- +-- Back-end specific primitives -- +-------------------------------------------------------------------------------- + +-- | Make sure that all log messages are printed by the logger on this node +apiRedirectLogsHere :: Backend -> Process () +apiRedirectLogsHere backend = do + mLogger <- whereis "logger" + forM_ mLogger $ \logger -> do + nids <- liftIO $ findPeers backend 1000000 + forM_ nids $ \nid -> registerRemote nid "logger" logger + +-------------------------------------------------------------------------------- +-- Slaves -- +-------------------------------------------------------------------------------- + +-- | Messages to slave nodes +-- +-- This datatype is not exposed; instead, we expose primitives for dealing +-- with slaves. +data SlaveControllerMsg = + SlaveTerminate + deriving (Typeable, Show) + +instance Binary SlaveControllerMsg where + put SlaveTerminate = putWord8 0 + get = do + header <- getWord8 + case header of + 0 -> return SlaveTerminate + _ -> fail "SlaveControllerMsg.get: invalid" + +-- | Calling 'slave' sets up a new local node and then waits. You start +-- processes on the slave by calling 'spawn' from other nodes. +-- +-- This function does not return. The only way to exit the slave is to CTRL-C +-- the process or call terminateSlave from another node. +startSlave :: Backend -> IO () +startSlave backend = do + node <- newLocalNode backend + Node.runProcess node slaveController + +-- | The slave controller interprets 'SlaveControllerMsg's +slaveController :: Process () +slaveController = do + pid <- getSelfPid + register "slaveController" pid + go + where + go = do + msg <- expect + case msg of + SlaveTerminate -> return () + +-- | Terminate the slave at the given node ID +terminateSlave :: NodeId -> Process () +terminateSlave nid = nsendRemote nid "slaveController" SlaveTerminate + +-- | Find slave nodes +findSlaves :: Backend -> Process [NodeId] +findSlaves backend = do + nodes <- liftIO $ findPeers backend 1000000 + -- Fire of asynchronous requests for the slave controller + forM_ nodes $ \nid -> whereisRemoteAsync nid "slaveController" + -- Wait for the replies + catMaybes <$> forM nodes (\_ -> + receiveWait + [ matchIf (\(WhereIsReply label _) -> label == "slaveController") + (\(WhereIsReply _ mPid) -> return (processNodeId <$> mPid)) + ]) + +-- | Terminate all slaves +terminateAllSlaves :: Backend -> Process () +terminateAllSlaves backend = do + slaves <- findSlaves backend + forM_ slaves terminateSlave + liftIO $ threadDelay 1000000 + +-------------------------------------------------------------------------------- +-- Master nodes +-------------------------------------------------------------------------------- + +-- | 'startMaster' finds all slaves currently available on the local network +-- (which should therefore be started first), redirects all log messages to +-- itself, and then calls the specified process, passing the list of slaves +-- nodes. +-- +-- Terminates when the specified process terminates. If you want to terminate +-- the slaves when the master terminates, you should manually call +-- 'terminateAllSlaves'. +startMaster :: Backend -> ([NodeId] -> Process ()) -> IO () +startMaster backend proc = do + node <- newLocalNode backend + Node.runProcess node $ do + slaves <- findSlaves backend + redirectLogsHere backend + proc slaves + +-------------------------------------------------------------------------------- +-- Accessors -- +-------------------------------------------------------------------------------- + +localNodes :: Accessor BackendState [Node.LocalNode] +localNodes = accessor _localNodes (\ns st -> st { _localNodes = ns }) + +peers :: Accessor BackendState (Set NodeId) +peers = accessor _peers (\ps st -> st { _peers = ps }) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs new file mode 100644 index 00000000..db91d6a1 --- /dev/null +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs @@ -0,0 +1,126 @@ +-- | Multicast utilities +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast (initMulticast) where + +import Data.Function (on) +import Data.Map (Map) +import qualified Data.Map as Map (empty) +import Data.Binary (Binary, decode, encode) +import Data.IORef (IORef, newIORef, readIORef, modifyIORef) +import qualified Data.ByteString as BSS (ByteString, concat) +import qualified Data.ByteString.Lazy as BSL + ( ByteString + , empty + , append + , fromChunks + , toChunks + , length + , splitAt + ) +import Data.Accessor (Accessor, (^:), (^.), (^=)) +import qualified Data.Accessor.Container as DAC (mapDefault) +import Control.Applicative ((<$>)) +import Network.Socket (HostName, PortNumber, Socket, SockAddr) +import qualified Network.Socket.ByteString as NBS (recvFrom, sendManyTo) +import Network.Transport.Internal (decodeInt32, encodeInt32) +import Network.Multicast (multicastSender, multicastReceiver) + +-------------------------------------------------------------------------------- +-- Top-level API -- +-------------------------------------------------------------------------------- + +-- | Given a hostname and a port number, initialize the multicast system. +-- +-- Note: it is important that you never send messages larger than the maximum +-- message size; if you do, all subsequent communication will probably fail. +-- +-- Returns a reader and a writer. +-- +-- NOTE: By rights the two functions should be "locally" polymorphic in 'a', +-- but this requires impredicative types. +initMulticast :: forall a. Binary a + => HostName -- ^ Multicast IP + -> PortNumber -- ^ Port number + -> Int -- ^ Maximum message size + -> IO (IO (a, SockAddr), a -> IO ()) +initMulticast host port bufferSize = do + (sendSock, sendAddr) <- multicastSender host port + readSock <- multicastReceiver host port + st <- newIORef Map.empty + return (recvBinary readSock st bufferSize, writer sendSock sendAddr) + where + writer :: forall a. Binary a => Socket -> SockAddr -> a -> IO () + writer sock addr val = do + let bytes = encode val + len = encodeInt32 (BSL.length bytes) + NBS.sendManyTo sock (len : BSL.toChunks bytes) addr + +-------------------------------------------------------------------------------- +-- UDP multicast read, dealing with multiple senders -- +-------------------------------------------------------------------------------- + +type UDPState = Map SockAddr BSL.ByteString + +-- TODO: This is inefficient and an orphan instance. +-- Requested official instance (https://github.com/haskell/network/issues/38) +instance Ord SockAddr where + compare = compare `on` show + +bufferFor :: SockAddr -> Accessor UDPState BSL.ByteString +bufferFor = DAC.mapDefault BSL.empty + +bufferAppend :: SockAddr -> BSS.ByteString -> UDPState -> UDPState +bufferAppend addr bytes = + bufferFor addr ^: flip BSL.append (BSL.fromChunks [bytes]) + +recvBinary :: Binary a => Socket -> IORef UDPState -> Int -> IO (a, SockAddr) +recvBinary sock st bufferSize = do + (bytes, addr) <- recvWithLength sock st bufferSize + return (decode bytes, addr) + +recvWithLength :: Socket + -> IORef UDPState + -> Int + -> IO (BSL.ByteString, SockAddr) +recvWithLength sock st bufferSize = do + (len, addr) <- recvExact sock 4 st bufferSize + let n = decodeInt32 . BSS.concat . BSL.toChunks $ len + bytes <- recvExactFrom addr sock n st bufferSize + return (bytes, addr) + +-- Receive all bytes currently in the buffer +recvAll :: Socket -> IORef UDPState -> Int -> IO SockAddr +recvAll sock st bufferSize = do + (bytes, addr) <- NBS.recvFrom sock bufferSize + modifyIORef st $ bufferAppend addr bytes + return addr + +recvExact :: Socket + -> Int + -> IORef UDPState + -> Int + -> IO (BSL.ByteString, SockAddr) +recvExact sock n st bufferSize = do + addr <- recvAll sock st bufferSize + bytes <- recvExactFrom addr sock n st bufferSize + return (bytes, addr) + +recvExactFrom :: SockAddr + -> Socket + -> Int + -> IORef UDPState + -> Int + -> IO BSL.ByteString +recvExactFrom addr sock n st bufferSize = go + where + go :: IO BSL.ByteString + go = do + accAddr <- (^. bufferFor addr) <$> readIORef st + if BSL.length accAddr >= fromIntegral n + then do + let (bytes, accAddr') = BSL.splitAt (fromIntegral n) accAddr + modifyIORef st $ bufferFor addr ^= accAddr' + return bytes + else do + _ <- recvAll sock st bufferSize + go diff --git a/tests/TestSimpleLocalnet.hs b/tests/TestSimpleLocalnet.hs new file mode 100644 index 00000000..83b9c836 --- /dev/null +++ b/tests/TestSimpleLocalnet.hs @@ -0,0 +1,24 @@ +import System.Environment (getArgs, getProgName) +import Control.Distributed.Process (NodeId, Process, liftIO) +import Control.Distributed.Process.Node (initRemoteTable) +import Control.Distributed.Process.Backend.SimpleLocalnet + +master :: Backend -> [NodeId] -> Process () +master backend slaves = do + liftIO . putStrLn $ "Slaves: " ++ show slaves + terminateAllSlaves backend + +main :: IO () +main = do + prog <- getProgName + args <- getArgs + + case args of + ["master", host, port] -> do + backend <- initializeBackend host port initRemoteTable + startMaster backend (master backend) + ["slave", host, port] -> do + backend <- initializeBackend host port initRemoteTable + startSlave backend + _ -> + putStrLn $ "usage: " ++ prog ++ " (master | slave) host port" diff --git a/tests/runTestSimpleLocalnet.hs b/tests/runTestSimpleLocalnet.hs new file mode 100755 index 00000000..e8b3bf4c --- /dev/null +++ b/tests/runTestSimpleLocalnet.hs @@ -0,0 +1,11 @@ +#!/bin/bash +TestSimpleLocalnet=dist/build/TestSimpleLocalnet/TestSimpleLocalnet +$TestSimpleLocalnet slave 127.0.0.1 8080 & +sleep 1 +$TestSimpleLocalnet slave 127.0.0.1 8081 & +sleep 1 +$TestSimpleLocalnet slave 127.0.0.1 8082 & +sleep 1 +$TestSimpleLocalnet slave 127.0.0.1 8083 & +sleep 1 +$TestSimpleLocalnet master 127.0.0.1 8084 From 1b2aa0bd497ae2b726deba41dd60696a5e47ce21 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 6 Jul 2012 20:55:40 +0100 Subject: [PATCH 0112/2357] Add source repositories --- distributed-process-simplelocalnet.cabal | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 178b3d7f..25c98852 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -18,6 +18,11 @@ Description: Simple backend based on the TCP transport which offers node Tested-With: GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Control +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/distributed-process + SubDir: distributed-process-simplelocalnet + Library Build-Depends: base >= 4.4 && < 5, bytestring >= 0.9 && < 0.10, From 925d829515e1e5a710198ff672b7b05a3425aba4 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 6 Jul 2012 20:55:40 +0100 Subject: [PATCH 0113/2357] Add source repositories --- network-transport-tcp.cabal | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index a739aeed..a7bf0726 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -15,6 +15,11 @@ Description: TCP instantation of Network.Transport Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Network +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/distributed-process + SubDir: network-transport-tcp + Library Build-Depends: base >= 4.3 && < 5, network-transport >= 0.2 && < 0.3, From cf1cbcc65f36c1642a28c85cfb30143f56f746b2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 6 Jul 2012 20:55:40 +0100 Subject: [PATCH 0114/2357] Add source repositories --- network-transport.cabal | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/network-transport.cabal b/network-transport.cabal index 0979c44c..0bc0eb50 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,6 +1,6 @@ Name: network-transport Version: 0.2.0 -Cabal-Version: >=1.2.3 +Cabal-Version: >=1.6 Build-Type: Simple License: BSD3 License-File: LICENSE @@ -58,6 +58,11 @@ Description: "Network.Transport" is a Network Abstraction Layer which provides Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Network +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/distributed-process + SubDir: network-transport + Library Build-Depends: base >= 4.3 && < 5, binary >= 0.5 && < 0.6, From 9c257a80baa06418b17c2476724a9638f59d275d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 9 Jul 2012 14:52:50 +0100 Subject: [PATCH 0115/2357] Bugfix: Documentation referred to old name --- src/Control/Distributed/Process/Backend/SimpleLocalnet.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index 68d284e5..d523c447 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -13,7 +13,7 @@ -- > import System.Environment (getArgs) -- > import Control.Distributed.Process -- > import Control.Distributed.Process.Node (initRemoteTable) --- > import Control.Distributed.Process.Backend.Local +-- > import Control.Distributed.Process.Backend.SimpleLocalnet -- > -- > master :: Backend -> [NodeId] -> Process () -- > master backend slaves = do From 3b2d9f98d9b8e1d9ebdac1bd95ce3d8747dbc323 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 9 Jul 2012 15:10:52 +0100 Subject: [PATCH 0116/2357] Add ChangeLogs; update version numbers --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..a1fb8070 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,3 @@ +2012-07-07 Edsko de Vries 0.2.0 + +* Initial release. From ed5d3b6b08e56173b03a41953f526f2f54107e2d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 9 Jul 2012 15:10:52 +0100 Subject: [PATCH 0117/2357] Add ChangeLogs; update version numbers --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..a1fb8070 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,3 @@ +2012-07-07 Edsko de Vries 0.2.0 + +* Initial release. From e87392eb0f449a0983f9ac77e59541f2605e094a Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 9 Jul 2012 15:10:52 +0100 Subject: [PATCH 0118/2357] Add ChangeLogs; update version numbers --- ChangeLog | 7 +++++++ distributed-process-simplelocalnet.cabal | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..7d1d98df --- /dev/null +++ b/ChangeLog @@ -0,0 +1,7 @@ +2012-07-09 Edsko de Vries 0.2.0.1 + +* Bugfix: Documentation referred to old module name + +2012-07-07 Edsko de Vries 0.2.0 + +* Initial release. diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 25c98852..c47362d2 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0 +Version: 0.2.0.1 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 From 69643c5152ec1ac77db9c9fd4623b4be071cbd46 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 12 Jul 2012 14:12:40 +0100 Subject: [PATCH 0119/2357] Fixed bug in Network.Transport.TCP --- src/Network/Transport/TCP/Internal.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs index eabcf0a6..e39f5642 100644 --- a/src/Network/Transport/TCP/Internal.hs +++ b/src/Network/Transport/TCP/Internal.hs @@ -104,7 +104,7 @@ tryCloseSocket sock = void . tryIO $ recvExact :: N.Socket -- ^ Socket to read from -> Int32 -- ^ Number of bytes to read -> IO [ByteString] -recvExact _ len | len <= 0 = throwIO (userError "recvExact: Negative length") +recvExact _ len | len < 0 = throwIO (userError "recvExact: Negative length") recvExact sock len = go [] len where go :: [ByteString] -> Int32 -> IO [ByteString] From 0b8f6f681c55b481f883a387a415c441ac392d2c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 12 Jul 2012 15:56:10 +0100 Subject: [PATCH 0120/2357] Bump version number --- ChangeLog | 4 ++++ network-transport-tcp.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index a1fb8070..bc0d3f00 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-07-12 Edsko de Vries 0.2.0.1 + +* Fix bug in recvExact + 2012-07-07 Edsko de Vries 0.2.0 * Initial release. diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index a7bf0726..69c348a5 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -1,5 +1,5 @@ Name: network-transport-tcp -Version: 0.2.0 +Version: 0.2.0.1 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 From 530a3b4e9fb458292481bdb504acaa19a81f7b39 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 16 Jul 2012 13:34:43 +0100 Subject: [PATCH 0121/2357] Hide catch only for base < 4.6 --- ChangeLog | 4 ++++ network-transport.cabal | 5 +++-- src/Network/Transport/Internal.hs | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index a1fb8070..ecaedcfd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-07-16 Edsko de Vries 0.2.0.1 + +* Hide catch only for base < 4.6 + 2012-07-07 Edsko de Vries 0.2.0 * Initial release. diff --git a/network-transport.cabal b/network-transport.cabal index 0bc0eb50..d9ecd61d 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,5 +1,5 @@ Name: network-transport -Version: 0.2.0 +Version: 0.2.0.1 Cabal-Version: >=1.6 Build-Type: Simple License: BSD3 @@ -75,6 +75,7 @@ Library RankNTypes, ScopedTypeVariables, DeriveDataTypeable, - GeneralizedNewtypeDeriving + GeneralizedNewtypeDeriving, + CPP GHC-Options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 7545c014..14812a8f 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -18,7 +18,10 @@ module Network.Transport.Internal ( -- * Encoders/decoders , tlog ) where +#if ! MIN_VERSION_base(4,6,0) import Prelude hiding (catch) +#endif + import Foreign.Storable (pokeByteOff, peekByteOff) import Foreign.C (CInt(..), CShort(..)) import Foreign.ForeignPtr (withForeignPtr) From 5c4693ce926911d898efeef25d2e44421d42f9e9 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 16 Jul 2012 14:50:18 +0100 Subject: [PATCH 0122/2357] Base 4.6 compatibility --- ChangeLog | 5 +++++ network-transport-tcp.cabal | 14 ++++++++------ src/Network/Transport/TCP.hs | 8 +++++++- src/Network/Transport/TCP/Internal.hs | 3 +++ tests/TestAuxiliary.hs | 3 +++ tests/TestTCP.hs | 10 +++++++++- tests/TestTransport.hs | 10 +++++++++- tests/Traced.hs | 10 +++++++++- 8 files changed, 53 insertions(+), 10 deletions(-) diff --git a/ChangeLog b/ChangeLog index bc0d3f00..5a6d69d8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2012-07-16 Edsko de Vries 0.2.0.2 + +* Base 4.6 compatibility +* Relax package contraints on bytestring and containers + 2012-07-12 Edsko de Vries 0.2.0.1 * Fix bug in recvExact diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 69c348a5..a31e74e0 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -1,5 +1,5 @@ Name: network-transport-tcp -Version: 0.2.0.1 +Version: 0.2.0.2 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -24,11 +24,12 @@ Library Build-Depends: base >= 4.3 && < 5, network-transport >= 0.2 && < 0.3, data-accessor >= 0.2 && < 0.3, - containers >= 0.4 && < 0.5, - bytestring >= 0.9 && < 0.10, + containers >= 0.4 && < 0.6, + bytestring >= 0.9 && < 0.11, network >= 2.3 && < 2.4 Exposed-modules: Network.Transport.TCP, Network.Transport.TCP.Internal + Extensions: CPP ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src @@ -38,8 +39,8 @@ Test-Suite TestTCP Build-Depends: base >= 4.3 && < 5, network-transport >= 0.2 && < 0.3, data-accessor >= 0.2 && < 0.3, - containers >= 0.4 && < 0.5, - bytestring >= 0.9 && < 0.10, + containers >= 0.4 && < 0.6, + bytestring >= 0.9 && < 0.11, network >= 2.3 && < 2.4, random >= 1.0 && < 1.1, ansi-terminal >= 0.5 && < 0.6, @@ -50,5 +51,6 @@ Test-Suite TestTCP DeriveDataTypeable, RankNTypes, OverlappingInstances, - OverloadedStrings + OverloadedStrings, + CPP HS-Source-Dirs: tests src diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index dd203313..b40c51ef 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -28,7 +28,13 @@ module Network.Transport.TCP ( -- * Main API -- $design ) where -import Prelude hiding (catch, mapM_) +import Prelude hiding + ( mapM_ +#if ! MIN_VERSION_base(4,6,0) + , catch +#endif + ) + import Network.Transport import Network.Transport.TCP.Internal ( forkServer , recvWithLength diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs index e39f5642..b7301f13 100644 --- a/src/Network/Transport/TCP/Internal.hs +++ b/src/Network/Transport/TCP/Internal.hs @@ -6,7 +6,10 @@ module Network.Transport.TCP.Internal ( forkServer , tryCloseSocket ) where +#if ! MIN_VERSION_base(4,6,0) import Prelude hiding (catch) +#endif + import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) import qualified Network.Socket as N ( HostName , ServiceName diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs index d912ee6e..f76bf819 100644 --- a/tests/TestAuxiliary.hs +++ b/tests/TestAuxiliary.hs @@ -8,7 +8,10 @@ module TestAuxiliary ( -- Running tests , randomThreadDelay ) where +#if ! MIN_VERSION_base(4,6,0) import Prelude hiding (catch) +#endif + import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) import Control.Concurrent.Chan (Chan) import Control.Monad (liftM2, unless) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index fcc387e5..83aaa89e 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -2,7 +2,15 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Prelude hiding (catch, (>>=), (>>), return, fail) +import Prelude hiding + ( (>>=) + , return + , fail + , (>>) +#if ! MIN_VERSION_base(4,6,0) + , catch +#endif + ) import TestTransport (testTransport) import TestAuxiliary (forkTry, runTests) import Network.Transport diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index e528e327..7543c948 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -1,7 +1,15 @@ {-# LANGUAGE RebindableSyntax #-} module TestTransport where -import Prelude hiding (catch, (>>=), (>>), return, fail) +import Prelude hiding + ( (>>=) + , return + , fail + , (>>) +#if ! MIN_VERSION_base(4,6,0) + , catch +#endif + ) import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) import Control.Concurrent (forkIO, killThread, yield) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) diff --git a/tests/Traced.hs b/tests/Traced.hs index a7735efa..d001c7a2 100644 --- a/tests/Traced.hs +++ b/tests/Traced.hs @@ -50,7 +50,15 @@ module Traced ( MonadS(..) , traceShow ) where -import Prelude hiding ((>>=), return, fail, catch, (>>)) +import Prelude hiding + ( (>>=) + , return + , fail + , (>>) +#if ! MIN_VERSION_base(4,6,0) + , catch +#endif + ) import qualified Prelude import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) import Control.Applicative ((<$>)) From 4fc8594efe7e4a300384ed28218552e8f33d0e62 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 16 Jul 2012 14:50:18 +0100 Subject: [PATCH 0123/2357] Base 4.6 compatibility --- ChangeLog | 4 ++++ distributed-process-simplelocalnet.cabal | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7d1d98df..85a6e03b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-07-16 Edsko de Vries 0.2.0.2 + +* Relax contraints on bytestring and containers + 2012-07-09 Edsko de Vries 0.2.0.1 * Bugfix: Documentation referred to old module name diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index c47362d2..4898cfc1 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.1 +Version: 0.2.0.2 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -25,12 +25,12 @@ Source-Repository head Library Build-Depends: base >= 4.4 && < 5, - bytestring >= 0.9 && < 0.10, + bytestring >= 0.9 && < 0.11, network >= 2.3 && < 2.4, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, binary >= 0.5 && < 0.6, - containers >= 0.4 && < 0.5, + containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, network-transport >= 0.2 && < 0.3, network-transport-tcp >= 0.2 && < 0.3, @@ -47,12 +47,12 @@ Test-Suite TestSimpleLocalnet Type: exitcode-stdio-1.0 Main-Is: TestSimpleLocalnet.hs Build-Depends: base >= 4.4 && < 5, - bytestring >= 0.9 && < 0.10, + bytestring >= 0.9 && < 0.11, network >= 2.3 && < 2.4, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, binary >= 0.5 && < 0.6, - containers >= 0.4 && < 0.5, + containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, network-transport >= 0.2 && < 0.3, network-transport-tcp >= 0.2 && < 0.3, From c971d251f7757643d343a7e59275eeb4b1837c95 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 16 Jul 2012 14:50:18 +0100 Subject: [PATCH 0124/2357] Base 4.6 compatibility --- ChangeLog | 5 +++++ network-transport.cabal | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index ecaedcfd..0fccd757 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2012-07-16 Edsko de Vries 0.2.0.2 + +* Base 4.6 compatible test suites +* Relax package constraints for bytestring + 2012-07-16 Edsko de Vries 0.2.0.1 * Hide catch only for base < 4.6 diff --git a/network-transport.cabal b/network-transport.cabal index d9ecd61d..d1fa9edc 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,5 +1,5 @@ Name: network-transport -Version: 0.2.0.1 +Version: 0.2.0.2 Cabal-Version: >=1.6 Build-Type: Simple License: BSD3 @@ -66,7 +66,7 @@ Source-Repository head Library Build-Depends: base >= 4.3 && < 5, binary >= 0.5 && < 0.6, - bytestring >= 0.9 && < 0.10, + bytestring >= 0.9 && < 0.11, transformers >= 0.2 && < 0.4 Exposed-Modules: Network.Transport, Network.Transport.Util From 1bdb542375ab157e24d7153c899066a2c9f47cff Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 16 Jul 2012 15:54:54 +0100 Subject: [PATCH 0125/2357] Avoid resource exhaustion in tests --- tests/TestTransport.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index 7543c948..e06ad7ff 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -947,8 +947,8 @@ testTransport newTransport = do , ("CloseOneDirection", testCloseOneDirection transport numPings) , ("CloseReopen", testCloseReopen transport numPings) , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport 1000) - , ("Crossing", testCrossing transport 100) + , ("SendAfterClose", testSendAfterClose transport 100) + , ("Crossing", testCrossing transport 10) , ("CloseTwice", testCloseTwice transport 100) , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) @@ -958,7 +958,7 @@ testTransport newTransport = do , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) , ("ExceptionOnReceive", testExceptionOnReceive newTransport) , ("SendException", testSendException newTransport) - , ("Kill", testKill newTransport 10000) + , ("Kill", testKill newTransport 1000) ] where numPings = 10000 :: Int From b48b2a261860c7dfb61405819fe59c5543085767 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 17 Jul 2012 11:34:22 +0100 Subject: [PATCH 0126/2357] Improve documentation (issue 13) --- .../Process/Backend/SimpleLocalnet.hs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index d523c447..cdcc314c 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -33,6 +33,46 @@ -- > ["slave", host, port] -> do -- > backend <- initializeBackend host port initRemoteTable -- > startSlave backend +-- +-- [Compiling and Running] +-- +-- Save to @example.hs@ and compile using +-- +-- > ghc -threaded example.hs +-- +-- Fire up some slave nodes (for the example, we run them on a single machine): +-- +-- > ./example slave localhost 8080 & +-- > ./example slave localhost 8081 & +-- > ./example slave localhost 8082 & +-- > ./example slave localhost 8083 & +-- +-- And start the master node: +-- +-- > ./example master localhost 8084 +-- +-- which should then output: +-- +-- > Slaves: [nid://localhost:8083:0,nid://localhost:8082:0,nid://localhost:8081:0,nid://localhost:8080:0] +-- +-- at which point the slaves should exit. +-- +-- To run the example on multiple machines, you could run +-- +-- > ./example slave 198.51.100.1 8080 & +-- > ./example slave 198.51.100.2 8080 & +-- > ./example slave 198.51.100.3 8080 & +-- > ./example slave 198.51.100.4 8080 & +-- +-- on four different machines (with IP addresses 198.51.100.1..4), and run the +-- master on a fifth node (or on any of the four machines that run the slave +-- nodes). +-- +-- It is important that every node has a unique (hostname, port number) pair, +-- and that the hostname you use to initialize the node can be resolved by +-- peer nodes. In other words, if you start a node and pass hostname @localhost@ +-- then peer nodes won't be able to reach it because @localhost@ will resolve +-- to a different IP address for them. {-# OPTIONS_GHC -fno-warn-orphans #-} module Control.Distributed.Process.Backend.SimpleLocalnet ( -- * Initialization From 7c1a890a11fc58dfa75e4cfd6caa1853afab4eec Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 17 Jul 2012 11:38:05 +0100 Subject: [PATCH 0127/2357] Improve docs --- src/Control/Distributed/Process/Backend/SimpleLocalnet.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index cdcc314c..d1d34b13 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -135,8 +135,9 @@ import Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast (in data Backend = Backend { -- | Create a new local node newLocalNode :: IO Node.LocalNode - -- | @findPeers t@ sends out a /who's there?/ request, waits 't' msec, - -- and then collects and returns the answers + -- | @findPeers t@ broadcasts a /who's there?/ message on the local + -- network, waits 't' msec, and then collects and returns the answers. + -- You can use this to dynamically discover peer nodes. , findPeers :: Int -> IO [NodeId] -- | Make sure that all log messages are printed by the logger on the -- current node From 1bdb0493f648a55b7477c58cca8cd85e24c1e384 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 17 Jul 2012 13:19:37 +0100 Subject: [PATCH 0128/2357] Bumb version number --- ChangeLog | 4 ++++ distributed-process-simplelocalnet.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 85a6e03b..2086d0db 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-07-17 Edsko de Vries 0.2.0.3 + +* Improve documentation + 2012-07-16 Edsko de Vries 0.2.0.2 * Relax contraints on bytestring and containers diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 4898cfc1..618361ab 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.2 +Version: 0.2.0.3 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 From f18a746d23726dc0c7a7d785212c4438b2092091 Mon Sep 17 00:00:00 2001 From: Mark Wright Date: Sun, 22 Jul 2012 11:13:24 +1000 Subject: [PATCH 0129/2357] include the test files in the sdist tarball --- network-transport-tcp.cabal | 3 +++ 1 file changed, 3 insertions(+) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index a31e74e0..c7d7d2ad 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -45,6 +45,9 @@ Test-Suite TestTCP random >= 1.0 && < 1.1, ansi-terminal >= 0.5 && < 0.6, mtl >= 2.0 && < 2.2 + Other-modules: TestAuxiliary, + TestTransport, + Traced ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N Extensions: ExistentialQuantification, FlexibleInstances, From 01e2b13374226cf6a4b368821abd5ac819081386 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 27 Jul 2012 12:34:37 +0100 Subject: [PATCH 0130/2357] Skeleton Azure backend --- LICENSE | 31 +++++++++++++++++++ Setup.hs | 2 ++ distributed-process-azure.cabal | 26 ++++++++++++++++ .../Distributed/Process/Backend/Azure.hs | 3 ++ 4 files changed, 62 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 distributed-process-azure.cabal create mode 100644 src/Control/Distributed/Process/Backend/Azure.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f3459e44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011-2012 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal new file mode 100644 index 00000000..71c07995 --- /dev/null +++ b/distributed-process-azure.cabal @@ -0,0 +1,26 @@ +Name: distributed-process-azure +Version: 0.1.0 +Cabal-Version: >=1.8 +Build-Type: Simple +License: BSD3 +License-File: LICENSE +Copyright: Well-Typed LLP +Author: Duncan Coutts, Nicolas Wu, Edsko de Vries +Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process +Bug-Reports: mailto:edsko@well-typed.com +Synopsis: Microsoft Azure backend for Cloud Haskell +Category: Control + +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/distributed-process + SubDir: distributed-process-azure + +Library + Build-Depends: base >= 4.4 && < 5, + azure-service-api + Exposed-modules: Control.Distributed.Process.Backend.Azure + ghc-options: -Wall + HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs new file mode 100644 index 00000000..ccb89595 --- /dev/null +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -0,0 +1,3 @@ +module Control.Distributed.Process.Backend.Azure where + +import Network.Azure.ServiceManagement From 818a5b0579ca841c1616ad49b1a8a75778148237 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 27 Jul 2012 12:44:08 +0100 Subject: [PATCH 0131/2357] Move test script to the CH backend --- distributed-process-azure.cabal | 8 +++ .../Process/Backend/Azure/CommandLine.hs | 71 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/Control/Distributed/Process/Backend/Azure/CommandLine.hs diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 71c07995..a19b1c20 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -24,3 +24,11 @@ Library Exposed-modules: Control.Distributed.Process.Backend.Azure ghc-options: -Wall HS-Source-Dirs: src + +Executable cloud-haskell-azure + Main-Is: src/Control/Distributed/Process/Backend/Azure/CommandLine.hs + Build-Depends: base >= 4.4 && < 5, + utf8-string >= 0.3 && < 0.4, + libssh2 >= 0.2 && < 0.3, + azure-service-api >= 0.1 && < 0.2, + filepath >= 1.3 && < 1.4 diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs new file mode 100644 index 00000000..8b0aaf80 --- /dev/null +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -0,0 +1,71 @@ +-- Base +import System.Environment (getArgs, getEnv) +import System.FilePath (()) +import System.Posix.Types (Fd) + +-- SSL +import Network.Azure.ServiceManagement + ( azureSetup + , virtualMachines + , vmSshEndpoints + ) + +-- SSH +import Network.SSH.Client.LibSSH2 + ( withSSH2 + , readAllChannel + , retryIfNeeded + , Session + , Channel + ) +import Network.SSH.Client.LibSSH2.Foreign + ( initialize + , exit + , channelExecute + ) +import Codec.Binary.UTF8.String (decodeString) + +main :: IO () +main = do + args <- getArgs + case args of + ["azure", subscriptionId, pathToCert, pathToKey] -> + tryConnectToAzure subscriptionId pathToCert pathToKey + ["command", user, host, port, cmd] -> + runCommand user host (read port) cmd + _ -> + putStrLn "Invalid command line arguments" + +-------------------------------------------------------------------------------- +-- Taken from libssh2/ssh-client -- +-------------------------------------------------------------------------------- + +runCommand :: String -> String -> Int -> String -> IO () +runCommand login host port command = + ssh login host port $ \fd s ch -> do + _ <- retryIfNeeded fd s $ channelExecute ch command + result <- readAllChannel fd ch + let r = decodeString result + print (length result) + print (length r) + putStrLn r + +ssh :: String -> String -> Int -> (Fd -> Session -> Channel -> IO a) -> IO () +ssh login host port actions = do + _ <- initialize True + home <- getEnv "HOME" + let known_hosts = home ".ssh" "known_hosts" + public = home ".ssh" "id_rsa.pub" + private = home ".ssh" "id_rsa" + _ <- withSSH2 known_hosts public private login host port $ actions + exit + +-------------------------------------------------------------------------------- +-- Azure tests -- +-------------------------------------------------------------------------------- + +tryConnectToAzure :: String -> String -> String -> IO () +tryConnectToAzure sid pathToCert pathToKey = do + setup <- azureSetup sid pathToCert pathToKey + vms <- virtualMachines setup + mapM_ print (map vmSshEndpoints vms) From 379c8033b87ab1c941bff383fd2d6bf8a6f0e63b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 27 Jul 2012 13:16:25 +0100 Subject: [PATCH 0132/2357] Start work on the Azure backend proper --- distributed-process-azure.cabal | 7 ++- .../Distributed/Process/Backend/Azure.hs | 57 ++++++++++++++++++- .../Process/Backend/Azure/CommandLine.hs | 18 +++--- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index a19b1c20..bea9900d 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -20,7 +20,8 @@ Source-Repository head Library Build-Depends: base >= 4.4 && < 5, - azure-service-api + azure-service-api >= 0.1 && < 0.2, + filepath >= 1.3 && < 1.4 Exposed-modules: Control.Distributed.Process.Backend.Azure ghc-options: -Wall HS-Source-Dirs: src @@ -30,5 +31,5 @@ Executable cloud-haskell-azure Build-Depends: base >= 4.4 && < 5, utf8-string >= 0.3 && < 0.4, libssh2 >= 0.2 && < 0.3, - azure-service-api >= 0.1 && < 0.2, - filepath >= 1.3 && < 1.4 + filepath >= 1.3 && < 1.4, + distributed-process-azure >= 0.1 && < 0.2 diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index ccb89595..e6840134 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -1,3 +1,58 @@ -module Control.Distributed.Process.Backend.Azure where +module Control.Distributed.Process.Backend.Azure + ( -- * Initialization + Backend(..) + , AzureParameters(..) + , defaultAzureParameters + , initializeBackend + -- * Re-exports from Azure Service Management + , VirtualMachine(..) + ) where +import System.Environment (getEnv) +import System.FilePath (()) import Network.Azure.ServiceManagement + ( VirtualMachine + , virtualMachines + , azureSetup + ) + +-- | Azure backend +data Backend = Backend { + findVMs :: IO [VirtualMachine] + } + +data AzureParameters = AzureParameters { + azureSubscriptionId :: String + , azureAuthCertificate :: FilePath + , azureAuthPrivateKey :: FilePath + , azureSshPublicKey :: FilePath + , azureSshPrivateKey :: FilePath + , azureSshKnownHosts :: FilePath + } + +-- | Create default azure parameters +defaultAzureParameters :: String -- ^ Azure subscription ID + -> FilePath -- ^ Path to X509 certificate + -> FilePath -- ^ Path to private key + -> IO AzureParameters +defaultAzureParameters sid x509 pkey = do + home <- getEnv "HOME" + return AzureParameters + { azureSubscriptionId = sid + , azureAuthCertificate = x509 + , azureAuthPrivateKey = pkey + , azureSshPublicKey = home ".ssh" "id_rsa.pub" + , azureSshPrivateKey = home ".ssh" "id_rsa" + , azureSshKnownHosts = home ".ssh" "known_hosts" + } + +-- | Initialize the backend +initializeBackend :: AzureParameters -> IO Backend +initializeBackend params = do + setup <- azureSetup (azureSubscriptionId params) + (azureAuthCertificate params) + (azureAuthPrivateKey params) + return Backend { + findVMs = virtualMachines setup + } + diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 8b0aaf80..07261d4e 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,13 +1,10 @@ --- Base import System.Environment (getArgs, getEnv) import System.FilePath (()) import System.Posix.Types (Fd) - --- SSL -import Network.Azure.ServiceManagement - ( azureSetup - , virtualMachines - , vmSshEndpoints +import Control.Distributed.Process.Backend.Azure + ( defaultAzureParameters + , initializeBackend + , findVMs ) -- SSH @@ -66,6 +63,7 @@ ssh login host port actions = do tryConnectToAzure :: String -> String -> String -> IO () tryConnectToAzure sid pathToCert pathToKey = do - setup <- azureSetup sid pathToCert pathToKey - vms <- virtualMachines setup - mapM_ print (map vmSshEndpoints vms) + params <- defaultAzureParameters sid pathToCert pathToKey + backend <- initializeBackend params + vms <- findVMs backend + mapM_ print vms From 5a4d782f330582745970f6e28406f464d27fc644 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 27 Jul 2012 14:35:29 +0100 Subject: [PATCH 0133/2357] Better naming --- distributed-process-azure.cabal | 3 ++- .../Distributed/Process/Backend/Azure.hs | 27 +++++++++++-------- .../Process/Backend/Azure/CommandLine.hs | 6 ++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index bea9900d..67b2c94a 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -21,7 +21,8 @@ Source-Repository head Library Build-Depends: base >= 4.4 && < 5, azure-service-api >= 0.1 && < 0.2, - filepath >= 1.3 && < 1.4 + filepath >= 1.3 && < 1.4, + executable-path >= 0.0.3 && < 0.1 Exposed-modules: Control.Distributed.Process.Backend.Azure ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index e6840134..7b5069cf 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -5,20 +5,24 @@ module Control.Distributed.Process.Backend.Azure , defaultAzureParameters , initializeBackend -- * Re-exports from Azure Service Management - , VirtualMachine(..) + , CloudService(..) ) where import System.Environment (getEnv) import System.FilePath (()) +import System.Environment.Executable (getExecutablePath) import Network.Azure.ServiceManagement - ( VirtualMachine - , virtualMachines + ( CloudService ) +import qualified Network.Azure.ServiceManagement as Azure + ( cloudServices + , AzureSetup , azureSetup - ) + ) -- | Azure backend data Backend = Backend { - findVMs :: IO [VirtualMachine] + -- | Find virtual machines + cloudServices :: IO [CloudService] } data AzureParameters = AzureParameters { @@ -31,7 +35,7 @@ data AzureParameters = AzureParameters { } -- | Create default azure parameters -defaultAzureParameters :: String -- ^ Azure subscription ID +defaultAzureParameters :: String -- ^ Azure subscription ID -> FilePath -- ^ Path to X509 certificate -> FilePath -- ^ Path to private key -> IO AzureParameters @@ -49,10 +53,11 @@ defaultAzureParameters sid x509 pkey = do -- | Initialize the backend initializeBackend :: AzureParameters -> IO Backend initializeBackend params = do - setup <- azureSetup (azureSubscriptionId params) - (azureAuthCertificate params) - (azureAuthPrivateKey params) + setup <- Azure.azureSetup (azureSubscriptionId params) + (azureAuthCertificate params) + (azureAuthPrivateKey params) + exe <- getExecutablePath + print exe return Backend { - findVMs = virtualMachines setup + cloudServices = Azure.cloudServices setup } - diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 07261d4e..e092a04b 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -4,7 +4,7 @@ import System.Posix.Types (Fd) import Control.Distributed.Process.Backend.Azure ( defaultAzureParameters , initializeBackend - , findVMs + , cloudServices ) -- SSH @@ -65,5 +65,5 @@ tryConnectToAzure :: String -> String -> String -> IO () tryConnectToAzure sid pathToCert pathToKey = do params <- defaultAzureParameters sid pathToCert pathToKey backend <- initializeBackend params - vms <- findVMs backend - mapM_ print vms + css <- cloudServices backend + mapM_ print css From 474a0b70a570870737e03083d8e9fea9dfb8d99f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 27 Jul 2012 17:25:00 +0100 Subject: [PATCH 0134/2357] Start adding SSH stuff --- distributed-process-azure.cabal | 3 +- .../Distributed/Process/Backend/Azure.hs | 38 +++++++++- .../Process/Backend/Azure/CommandLine.hs | 75 ++++--------------- 3 files changed, 55 insertions(+), 61 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 67b2c94a..2be92547 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -22,7 +22,8 @@ Library Build-Depends: base >= 4.4 && < 5, azure-service-api >= 0.1 && < 0.2, filepath >= 1.3 && < 1.4, - executable-path >= 0.0.3 && < 0.1 + executable-path >= 0.0.3 && < 0.1, + libssh2 >= 0.2 && < 0.3 Exposed-modules: Control.Distributed.Process.Backend.Azure ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 7b5069cf..fa73020e 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -6,29 +6,46 @@ module Control.Distributed.Process.Backend.Azure , initializeBackend -- * Re-exports from Azure Service Management , CloudService(..) + , VirtualMachine(..) ) where import System.Environment (getEnv) import System.FilePath (()) import System.Environment.Executable (getExecutablePath) +import Control.Concurrent (threadDelay) import Network.Azure.ServiceManagement - ( CloudService ) + ( CloudService(..) + , VirtualMachine(..) + , Endpoint(..) + ) import qualified Network.Azure.ServiceManagement as Azure ( cloudServices , AzureSetup , azureSetup + , vmSshEndpoint ) +import qualified Network.SSH.Client.LibSSH2 as SSH + ( withSSH2 + , retryIfNeeded + ) +import qualified Network.SSH.Client.LibSSH2.Foreign as SSH + ( initialize + , exit + , channelExecute + ) -- | Azure backend data Backend = Backend { -- | Find virtual machines cloudServices :: IO [CloudService] + , startOnVM :: VirtualMachine -> IO () } data AzureParameters = AzureParameters { azureSubscriptionId :: String , azureAuthCertificate :: FilePath , azureAuthPrivateKey :: FilePath + , azureSshUserName :: FilePath , azureSshPublicKey :: FilePath , azureSshPrivateKey :: FilePath , azureSshKnownHosts :: FilePath @@ -41,10 +58,12 @@ defaultAzureParameters :: String -- ^ Azure subscription ID -> IO AzureParameters defaultAzureParameters sid x509 pkey = do home <- getEnv "HOME" + user <- getEnv "USER" return AzureParameters { azureSubscriptionId = sid , azureAuthCertificate = x509 , azureAuthPrivateKey = pkey + , azureSshUserName = user , azureSshPublicKey = home ".ssh" "id_rsa.pub" , azureSshPrivateKey = home ".ssh" "id_rsa" , azureSshKnownHosts = home ".ssh" "known_hosts" @@ -60,4 +79,21 @@ initializeBackend params = do print exe return Backend { cloudServices = Azure.cloudServices setup + , startOnVM = apiStartOnVM params } + +-- | Start a CH node on the given virtual machine +apiStartOnVM :: AzureParameters -> VirtualMachine -> IO () +apiStartOnVM params vm = do + _ <- SSH.initialize True + let ep = Azure.vmSshEndpoint vm + SSH.withSSH2 (azureSshKnownHosts params) + (azureSshPublicKey params) + (azureSshPrivateKey params) + (azureSshUserName params) + (endpointVip ep) + (read $ endpointPort ep) $ \fd s ch -> do + _ <- SSH.retryIfNeeded fd s $ SSH.channelExecute ch "/home/edsko/testservice" + threadDelay $ 10 * 1000000 -- 10 seconds + return () + SSH.exit diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index e092a04b..c3a474da 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,69 +1,26 @@ -import System.Environment (getArgs, getEnv) -import System.FilePath (()) -import System.Posix.Types (Fd) +import System.Environment (getArgs) import Control.Distributed.Process.Backend.Azure - ( defaultAzureParameters + ( AzureParameters(azureSshUserName) + , defaultAzureParameters , initializeBackend , cloudServices + , CloudService(cloudServiceVMs) + , VirtualMachine(vmName) + , startOnVM ) --- SSH -import Network.SSH.Client.LibSSH2 - ( withSSH2 - , readAllChannel - , retryIfNeeded - , Session - , Channel - ) -import Network.SSH.Client.LibSSH2.Foreign - ( initialize - , exit - , channelExecute - ) -import Codec.Binary.UTF8.String (decodeString) - main :: IO () main = do - args <- getArgs - case args of - ["azure", subscriptionId, pathToCert, pathToKey] -> - tryConnectToAzure subscriptionId pathToCert pathToKey - ["command", user, host, port, cmd] -> - runCommand user host (read port) cmd - _ -> - putStrLn "Invalid command line arguments" - --------------------------------------------------------------------------------- --- Taken from libssh2/ssh-client -- --------------------------------------------------------------------------------- - -runCommand :: String -> String -> Int -> String -> IO () -runCommand login host port command = - ssh login host port $ \fd s ch -> do - _ <- retryIfNeeded fd s $ channelExecute ch command - result <- readAllChannel fd ch - let r = decodeString result - print (length result) - print (length r) - putStrLn r - -ssh :: String -> String -> Int -> (Fd -> Session -> Channel -> IO a) -> IO () -ssh login host port actions = do - _ <- initialize True - home <- getEnv "HOME" - let known_hosts = home ".ssh" "known_hosts" - public = home ".ssh" "id_rsa.pub" - private = home ".ssh" "id_rsa" - _ <- withSSH2 known_hosts public private login host port $ actions - exit - --------------------------------------------------------------------------------- --- Azure tests -- --------------------------------------------------------------------------------- + [subscriptionId, pathToCert, pathToKey, user] <- getArgs + tryConnectToAzure subscriptionId pathToCert pathToKey user -tryConnectToAzure :: String -> String -> String -> IO () -tryConnectToAzure sid pathToCert pathToKey = do +tryConnectToAzure :: String -> String -> String -> String -> IO () +tryConnectToAzure sid pathToCert pathToKey user = do params <- defaultAzureParameters sid pathToCert pathToKey - backend <- initializeBackend params + backend <- initializeBackend params { azureSshUserName = user } css <- cloudServices backend - mapM_ print css + let ch = head [ vm | vm <- concatMap cloudServiceVMs css + , vmName vm == "CloudHaskell" + ] + print ch + startOnVM backend ch From 7a6698670e5b2bd9d2e664bbddbcc5fdd677475e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 27 Jul 2012 17:47:49 +0100 Subject: [PATCH 0135/2357] Figure out how to start background process --- src/Control/Distributed/Process/Backend/Azure.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index fa73020e..471ffa61 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -87,13 +87,11 @@ apiStartOnVM :: AzureParameters -> VirtualMachine -> IO () apiStartOnVM params vm = do _ <- SSH.initialize True let ep = Azure.vmSshEndpoint vm - SSH.withSSH2 (azureSshKnownHosts params) + _ <- SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) (azureSshPrivateKey params) (azureSshUserName params) (endpointVip ep) (read $ endpointPort ep) $ \fd s ch -> do - _ <- SSH.retryIfNeeded fd s $ SSH.channelExecute ch "/home/edsko/testservice" - threadDelay $ 10 * 1000000 -- 10 seconds - return () + SSH.retryIfNeeded fd s $ SSH.channelExecute ch "nohup /home/edsko/testservice >/dev/null 2>&1 &" SSH.exit From 18791155c733468f21f5e6768a7d21268a14e3cf Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 30 Jul 2012 13:25:28 +0100 Subject: [PATCH 0136/2357] Use updated libssh2 bindings --- distributed-process-azure.cabal | 1 + .../Distributed/Process/Backend/Azure.hs | 32 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 2be92547..28b88793 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -25,6 +25,7 @@ Library executable-path >= 0.0.3 && < 0.1, libssh2 >= 0.2 && < 0.3 Exposed-modules: Control.Distributed.Process.Backend.Azure + Extensions: ViewPatterns ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 471ffa61..aba9f2fb 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -10,9 +10,8 @@ module Control.Distributed.Process.Backend.Azure ) where import System.Environment (getEnv) -import System.FilePath (()) +import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) -import Control.Concurrent (threadDelay) import Network.Azure.ServiceManagement ( CloudService(..) , VirtualMachine(..) @@ -20,18 +19,16 @@ import Network.Azure.ServiceManagement ) import qualified Network.Azure.ServiceManagement as Azure ( cloudServices - , AzureSetup , azureSetup , vmSshEndpoint ) import qualified Network.SSH.Client.LibSSH2 as SSH ( withSSH2 - , retryIfNeeded + , execCommands ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize , exit - , channelExecute ) -- | Azure backend @@ -48,7 +45,9 @@ data AzureParameters = AzureParameters { , azureSshUserName :: FilePath , azureSshPublicKey :: FilePath , azureSshPrivateKey :: FilePath + , azureSshPassphrase :: String , azureSshKnownHosts :: FilePath + , azureSshRemotePath :: FilePath } -- | Create default azure parameters @@ -59,6 +58,7 @@ defaultAzureParameters :: String -- ^ Azure subscription ID defaultAzureParameters sid x509 pkey = do home <- getEnv "HOME" user <- getEnv "USER" + self <- getExecutablePath return AzureParameters { azureSubscriptionId = sid , azureAuthCertificate = x509 @@ -66,7 +66,9 @@ defaultAzureParameters sid x509 pkey = do , azureSshUserName = user , azureSshPublicKey = home ".ssh" "id_rsa.pub" , azureSshPrivateKey = home ".ssh" "id_rsa" + , azureSshPassphrase = "" , azureSshKnownHosts = home ".ssh" "known_hosts" + , azureSshRemotePath = "/home" user takeFileName self } -- | Initialize the backend @@ -75,8 +77,6 @@ initializeBackend params = do setup <- Azure.azureSetup (azureSubscriptionId params) (azureAuthCertificate params) (azureAuthPrivateKey params) - exe <- getExecutablePath - print exe return Backend { cloudServices = Azure.cloudServices setup , startOnVM = apiStartOnVM params @@ -84,14 +84,16 @@ initializeBackend params = do -- | Start a CH node on the given virtual machine apiStartOnVM :: AzureParameters -> VirtualMachine -> IO () -apiStartOnVM params vm = do +apiStartOnVM params (Azure.vmSshEndpoint -> Just ep) = do _ <- SSH.initialize True - let ep = Azure.vmSshEndpoint vm _ <- SSH.withSSH2 (azureSshKnownHosts params) - (azureSshPublicKey params) - (azureSshPrivateKey params) - (azureSshUserName params) - (endpointVip ep) - (read $ endpointPort ep) $ \fd s ch -> do - SSH.retryIfNeeded fd s $ SSH.channelExecute ch "nohup /home/edsko/testservice >/dev/null 2>&1 &" + (azureSshPublicKey params) + (azureSshPrivateKey params) + (azureSshPassphrase params) + (azureSshUserName params) + (endpointVip ep) + (read $ endpointPort ep) $ \fd s -> do + SSH.execCommands fd s ["nohup /home/edsko/testservice >/dev/null 2>&1 &"] SSH.exit +apiStartOnVM _ _ = + error "startOnVM: No SSH endpoint" From 250d3188aa3e2613701ff27d498536e85f0c6a4d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 30 Jul 2012 16:57:03 +0100 Subject: [PATCH 0137/2357] Use command line options parser --- distributed-process-azure.cabal | 3 +- .../Distributed/Process/Backend/Azure.hs | 7 +- .../Process/Backend/Azure/CommandLine.hs | 136 ++++++++++++++++-- 3 files changed, 129 insertions(+), 17 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 28b88793..76e7bfa8 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -35,4 +35,5 @@ Executable cloud-haskell-azure utf8-string >= 0.3 && < 0.4, libssh2 >= 0.2 && < 0.3, filepath >= 1.3 && < 1.4, - distributed-process-azure >= 0.1 && < 0.2 + distributed-process-azure >= 0.1 && < 0.2, + optparse-applicative >= 0.2 && < 0.3 diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index aba9f2fb..e425580a 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -25,6 +25,7 @@ import qualified Network.Azure.ServiceManagement as Azure import qualified Network.SSH.Client.LibSSH2 as SSH ( withSSH2 , execCommands + , scpSendFile ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize @@ -68,7 +69,7 @@ defaultAzureParameters sid x509 pkey = do , azureSshPrivateKey = home ".ssh" "id_rsa" , azureSshPassphrase = "" , azureSshKnownHosts = home ".ssh" "known_hosts" - , azureSshRemotePath = "/home" user takeFileName self + , azureSshRemotePath = takeFileName self } -- | Initialize the backend @@ -85,6 +86,7 @@ initializeBackend params = do -- | Start a CH node on the given virtual machine apiStartOnVM :: AzureParameters -> VirtualMachine -> IO () apiStartOnVM params (Azure.vmSshEndpoint -> Just ep) = do + self <- getExecutablePath _ <- SSH.initialize True _ <- SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) @@ -93,7 +95,8 @@ apiStartOnVM params (Azure.vmSshEndpoint -> Just ep) = do (azureSshUserName params) (endpointVip ep) (read $ endpointPort ep) $ \fd s -> do - SSH.execCommands fd s ["nohup /home/edsko/testservice >/dev/null 2>&1 &"] + SSH.scpSendFile fd s 0o700 self (azureSshRemotePath params) + -- SSH.execCommands fd s ["nohup /home/edsko/testservice >/dev/null 2>&1 &"] SSH.exit apiStartOnVM _ _ = error "startOnVM: No SSH endpoint" diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index c3a474da..641d55f6 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE Arrows #-} import System.Environment (getArgs) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName) @@ -8,19 +9,126 @@ import Control.Distributed.Process.Backend.Azure , VirtualMachine(vmName) , startOnVM ) +import Control.Arrow (returnA) +import Control.Applicative ((<$>), (<*>)) +import Options.Applicative + ( Parser + , strOption + , long + , (&) + , metavar + , help + , subparser + , command + , info + , progDesc + , execParser + , helper + , fullDesc + , header + ) +import Options.Applicative.Arrows (runA, asA) + +-------------------------------------------------------------------------------- +-- Command line options -- +-------------------------------------------------------------------------------- + +data AzureOptions = AzureOptions { + subscriptionId :: String + , pathToCert :: FilePath + , pathToKey :: FilePath + } + deriving Show + +data SshOptions = SshOptions { + remoteUser :: String + } + deriving Show + +data Command = + List AzureOptions + | Start AzureOptions SshOptions String + deriving Show + +azureOptionsParser :: Parser AzureOptions +azureOptionsParser = AzureOptions + <$> strOption ( long "subscription-id" + & metavar "SID" + & help "Azure subscription ID" + ) + <*> strOption ( long "certificate" + & metavar "CERT" + & help "X509 certificate" + ) + <*> strOption ( long "private" + & metavar "PRI" + & help "Private key in PKCS#1 format" + ) + +sshOptionsParser :: Parser SshOptions +sshOptionsParser = SshOptions + <$> strOption ( long "user" + & metavar "USER" + & help "Remove SSH username" + ) + +listParser :: Parser Command +listParser = List <$> azureOptionsParser + +startParser :: Parser Command +startParser = Start + <$> azureOptionsParser + <*> sshOptionsParser + <*> strOption ( long "vm" + & metavar "VM" + & help "Virtual machine name" + ) + +commandParser :: Parser Command +commandParser = subparser + ( command "list" (info listParser + (progDesc "List Azure cloud services")) + & command "start" (info startParser + (progDesc "Start a new Cloud Haskell node")) + ) + +-------------------------------------------------------------------------------- +-- Main -- +-------------------------------------------------------------------------------- main :: IO () -main = do - [subscriptionId, pathToCert, pathToKey, user] <- getArgs - tryConnectToAzure subscriptionId pathToCert pathToKey user - -tryConnectToAzure :: String -> String -> String -> String -> IO () -tryConnectToAzure sid pathToCert pathToKey user = do - params <- defaultAzureParameters sid pathToCert pathToKey - backend <- initializeBackend params { azureSshUserName = user } - css <- cloudServices backend - let ch = head [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == "CloudHaskell" - ] - print ch - startOnVM backend ch +main = do + cmd <- execParser opts + case cmd of + List azureOpts -> do + params <- azureParameters azureOpts Nothing + backend <- initializeBackend params + css <- cloudServices backend + print css + Start azureOpts sshOpts name -> do + params <- azureParameters azureOpts (Just sshOpts) + backend <- initializeBackend params + css <- cloudServices backend + let ch = head [ vm | vm <- concatMap cloudServiceVMs css + , vmName vm == name + ] + print ch + startOnVM backend ch + where + opts = info (helper <*> commandParser) + ( fullDesc + & header "Cloud Haskell backend for Azure" + ) + +azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters +azureParameters opts Nothing = + defaultAzureParameters (subscriptionId opts) + (pathToCert opts) + (pathToKey opts) +azureParameters opts (Just sshOpts) = do + params <- defaultAzureParameters (subscriptionId opts) + (pathToCert opts) + (pathToKey opts) + return params { + azureSshUserName = remoteUser sshOpts + } From 5ebb7e2c1abf224efdf719b85298f0ec6f0a43dc Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 30 Jul 2012 17:31:12 +0100 Subject: [PATCH 0138/2357] Prettier output --- src/Control/Distributed/Process/Backend/Azure/CommandLine.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 641d55f6..c7ed6e26 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -104,7 +104,7 @@ main = do params <- azureParameters azureOpts Nothing backend <- initializeBackend params css <- cloudServices backend - print css + mapM_ print css Start azureOpts sshOpts name -> do params <- azureParameters azureOpts (Just sshOpts) backend <- initializeBackend params From a444696f046686594167fe91d1fd82b89a40436e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 31 Jul 2012 13:42:04 +0100 Subject: [PATCH 0139/2357] Add MD5 check --- distributed-process-azure.cabal | 4 +- .../Distributed/Process/Backend/Azure.hs | 54 +++++++++++--- .../Process/Backend/Azure/CommandLine.hs | 72 +++++++++++++++---- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 76e7bfa8..e67a371a 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -23,7 +23,9 @@ Library azure-service-api >= 0.1 && < 0.2, filepath >= 1.3 && < 1.4, executable-path >= 0.0.3 && < 0.1, - libssh2 >= 0.2 && < 0.3 + libssh2 >= 0.2 && < 0.3, + pureMD5 >= 2.1 && < 2.2, + bytestring >= 0.9 && < 0.11 Exposed-modules: Control.Distributed.Process.Backend.Azure Extensions: ViewPatterns ghc-options: -Wall diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index e425580a..c7517607 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -12,6 +12,9 @@ module Control.Distributed.Process.Backend.Azure import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) +import Data.Digest.Pure.MD5 (md5, MD5Digest) +import qualified Data.ByteString.Lazy as BSL (readFile) +import Control.Applicative ((<$>)) import Network.Azure.ServiceManagement ( CloudService(..) , VirtualMachine(..) @@ -26,17 +29,25 @@ import qualified Network.SSH.Client.LibSSH2 as SSH ( withSSH2 , execCommands , scpSendFile + , withChannel + , readAllChannel ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize , exit + , openChannelSession + , retryIfNeeded + , channelExecute + , writeChannel + , channelSendEOF ) -- | Azure backend data Backend = Backend { -- | Find virtual machines cloudServices :: IO [CloudService] - , startOnVM :: VirtualMachine -> IO () + , copyToVM :: VirtualMachine -> IO () + , checkMD5 :: VirtualMachine -> IO Bool } data AzureParameters = AzureParameters { @@ -49,6 +60,7 @@ data AzureParameters = AzureParameters { , azureSshPassphrase :: String , azureSshKnownHosts :: FilePath , azureSshRemotePath :: FilePath + , azureSshLocalPath :: FilePath } -- | Create default azure parameters @@ -70,6 +82,7 @@ defaultAzureParameters sid x509 pkey = do , azureSshPassphrase = "" , azureSshKnownHosts = home ".ssh" "known_hosts" , azureSshRemotePath = takeFileName self + , azureSshLocalPath = self } -- | Initialize the backend @@ -80,13 +93,13 @@ initializeBackend params = do (azureAuthPrivateKey params) return Backend { cloudServices = Azure.cloudServices setup - , startOnVM = apiStartOnVM params + , copyToVM = apiCopyToVM params + , checkMD5 = apiCheckMD5 params } -- | Start a CH node on the given virtual machine -apiStartOnVM :: AzureParameters -> VirtualMachine -> IO () -apiStartOnVM params (Azure.vmSshEndpoint -> Just ep) = do - self <- getExecutablePath +apiCopyToVM :: AzureParameters -> VirtualMachine -> IO () +apiCopyToVM params (Azure.vmSshEndpoint -> Just ep) = do _ <- SSH.initialize True _ <- SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) @@ -95,8 +108,33 @@ apiStartOnVM params (Azure.vmSshEndpoint -> Just ep) = do (azureSshUserName params) (endpointVip ep) (read $ endpointPort ep) $ \fd s -> do - SSH.scpSendFile fd s 0o700 self (azureSshRemotePath params) + SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) -- SSH.execCommands fd s ["nohup /home/edsko/testservice >/dev/null 2>&1 &"] SSH.exit -apiStartOnVM _ _ = - error "startOnVM: No SSH endpoint" +apiCopyToVM _ _ = + error "copyToVM: No SSH endpoint" + +apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool +apiCheckMD5 params (Azure.vmSshEndpoint -> Just ep) = do + hash <- localHash params + _ <- SSH.initialize True + match <- SSH.withSSH2 (azureSshKnownHosts params) + (azureSshPublicKey params) + (azureSshPrivateKey params) + (azureSshPassphrase params) + (azureSshUserName params) + (endpointVip ep) + (read $ endpointPort ep) $ \fd s -> do + (r, _) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do + SSH.retryIfNeeded fd s $ SSH.channelExecute ch ("md5sum -c --status") + SSH.writeChannel ch $ show hash ++ " " ++ azureSshRemotePath params + SSH.channelSendEOF ch + SSH.readAllChannel fd ch + return (r == 0) + SSH.exit + return match +apiCheckMD5 _ _ = + error "checkMD5: No SSH endpoint" + +localHash :: AzureParameters -> IO MD5Digest +localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index c7ed6e26..e22ecc8c 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,5 +1,7 @@ {-# LANGUAGE Arrows #-} import System.Environment (getArgs) +import System.Exit (exitSuccess, exitFailure) +import Control.Monad (unless) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName) , defaultAzureParameters @@ -7,7 +9,7 @@ import Control.Distributed.Process.Backend.Azure , cloudServices , CloudService(cloudServiceVMs) , VirtualMachine(vmName) - , startOnVM + , Backend(copyToVM, checkMD5) ) import Control.Arrow (returnA) import Control.Applicative ((<$>), (<*>)) @@ -26,6 +28,7 @@ import Options.Applicative , helper , fullDesc , header + , switch ) import Options.Applicative.Arrows (runA, asA) @@ -46,8 +49,20 @@ data SshOptions = SshOptions { deriving Show data Command = - List AzureOptions - | Start AzureOptions SshOptions String + List { + azureOptions :: AzureOptions + } + | CopyTo { + azureOptions :: AzureOptions + , sshOptions :: SshOptions + , virtualMachine :: String + } + | CheckMD5 { + azureOptions :: AzureOptions + , sshOptions :: SshOptions + , virtualMachine :: String + , status :: Bool + } deriving Show azureOptionsParser :: Parser AzureOptions @@ -75,8 +90,8 @@ sshOptionsParser = SshOptions listParser :: Parser Command listParser = List <$> azureOptionsParser -startParser :: Parser Command -startParser = Start +copyToParser :: Parser Command +copyToParser = CopyTo <$> azureOptionsParser <*> sshOptionsParser <*> strOption ( long "vm" @@ -84,12 +99,26 @@ startParser = Start & help "Virtual machine name" ) +checkMD5Parser :: Parser Command +checkMD5Parser = CheckMD5 + <$> azureOptionsParser + <*> sshOptionsParser + <*> strOption ( long "vm" + & metavar "VM" + & help "Virtual machine name" + ) + <*> switch ( long "status" + & help "Don't output anything, status code shows success" + ) + commandParser :: Parser Command commandParser = subparser ( command "list" (info listParser (progDesc "List Azure cloud services")) - & command "start" (info startParser - (progDesc "Start a new Cloud Haskell node")) + & command "install" (info copyToParser + (progDesc "Install the executable on a virtual machine")) + & command "md5" (info checkMD5Parser + (progDesc "Check if the remote and local MD5 hash match")) ) -------------------------------------------------------------------------------- @@ -100,20 +129,37 @@ main :: IO () main = do cmd <- execParser opts case cmd of - List azureOpts -> do - params <- azureParameters azureOpts Nothing + List {} -> do + params <- azureParameters (azureOptions cmd) Nothing backend <- initializeBackend params css <- cloudServices backend mapM_ print css - Start azureOpts sshOpts name -> do - params <- azureParameters azureOpts (Just sshOpts) + CopyTo {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) backend <- initializeBackend params css <- cloudServices backend let ch = head [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == name + , vmName vm == virtualMachine cmd ] print ch - startOnVM backend ch + copyToVM backend ch + CheckMD5 {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + let ch = head [ vm | vm <- concatMap cloudServiceVMs css + , vmName vm == virtualMachine cmd + ] + match <- checkMD5 backend ch + if match + then do + unless (status cmd) $ + putStrLn "Local and remote MD5 hash match" + exitSuccess + else do + unless (status cmd) $ + putStrLn "Local and remote MD5 hash do NOT match" + exitFailure where opts = info (helper <*> commandParser) ( fullDesc From a9d2201051b69abed9efdf2aba27942b10ee65ca Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 31 Jul 2012 13:46:30 +0100 Subject: [PATCH 0140/2357] Relax bounds on optparse-applicative --- distributed-process-azure.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index e67a371a..e22ee9d2 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -38,4 +38,4 @@ Executable cloud-haskell-azure libssh2 >= 0.2 && < 0.3, filepath >= 1.3 && < 1.4, distributed-process-azure >= 0.1 && < 0.2, - optparse-applicative >= 0.2 && < 0.3 + optparse-applicative >= 0.2 && < 0.4 From 85b53ef3248230c2294f0222801fd6f572c730e6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 31 Jul 2012 14:24:18 +0100 Subject: [PATCH 0141/2357] Extend "install" and "md5" to cloud services --- .../Process/Backend/Azure/CommandLine.hs | 78 +++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index e22ecc8c..d37ca908 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,18 +1,19 @@ {-# LANGUAGE Arrows #-} import System.Environment (getArgs) import System.Exit (exitSuccess, exitFailure) -import Control.Monad (unless) +import System.IO (hFlush, stdout) +import Control.Monad (unless, forM, forM_) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName) , defaultAzureParameters , initializeBackend , cloudServices - , CloudService(cloudServiceVMs) + , CloudService(cloudServiceName, cloudServiceVMs) , VirtualMachine(vmName) , Backend(copyToVM, checkMD5) ) import Control.Arrow (returnA) -import Control.Applicative ((<$>), (<*>)) +import Control.Applicative ((<$>), (<*>), (<|>)) import Options.Applicative ( Parser , strOption @@ -48,6 +49,11 @@ data SshOptions = SshOptions { } deriving Show +data Target = + VirtualMachine String + | CloudService String + deriving Show + data Command = List { azureOptions :: AzureOptions @@ -55,12 +61,12 @@ data Command = | CopyTo { azureOptions :: AzureOptions , sshOptions :: SshOptions - , virtualMachine :: String + , target :: Target } | CheckMD5 { azureOptions :: AzureOptions , sshOptions :: SshOptions - , virtualMachine :: String + , target :: Target , status :: Bool } deriving Show @@ -94,19 +100,27 @@ copyToParser :: Parser Command copyToParser = CopyTo <$> azureOptionsParser <*> sshOptionsParser - <*> strOption ( long "vm" - & metavar "VM" - & help "Virtual machine name" - ) + <*> targetParser + +targetParser :: Parser Target +targetParser = + ( VirtualMachine <$> strOption ( long "virtual-machine" + & metavar "VM" + & help "Virtual machine name" + ) + ) + <|> + ( CloudService <$> strOption ( long "cloud-service" + & metavar "CS" + & help "Cloud service name" + ) + ) checkMD5Parser :: Parser Command checkMD5Parser = CheckMD5 <$> azureOptionsParser <*> sshOptionsParser - <*> strOption ( long "vm" - & metavar "VM" - & help "Virtual machine name" - ) + <*> targetParser <*> switch ( long "status" & help "Don't output anything, status code shows success" ) @@ -138,34 +152,36 @@ main = do params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) backend <- initializeBackend params css <- cloudServices backend - let ch = head [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == virtualMachine cmd - ] - print ch - copyToVM backend ch + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + copyToVM backend vm + putStrLn "Done" CheckMD5 {} -> do params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) backend <- initializeBackend params css <- cloudServices backend - let ch = head [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == virtualMachine cmd - ] - match <- checkMD5 backend ch - if match - then do - unless (status cmd) $ - putStrLn "Local and remote MD5 hash match" - exitSuccess - else do - unless (status cmd) $ - putStrLn "Local and remote MD5 hash do NOT match" - exitFailure + matches <- forM (findTarget (target cmd) css) $ \vm -> do + unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout + match <- checkMD5 backend vm + unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" + return match + if and matches + then exitSuccess + else exitFailure where opts = info (helper <*> commandParser) ( fullDesc & header "Cloud Haskell backend for Azure" ) +findTarget :: Target -> [CloudService] -> [VirtualMachine] +findTarget (CloudService cs) css = + concatMap cloudServiceVMs . filter ((== cs) . cloudServiceName) $ css +findTarget (VirtualMachine virtualMachine) css = + [ vm | vm <- concatMap cloudServiceVMs css + , vmName vm == virtualMachine + ] + azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters azureParameters opts Nothing = defaultAzureParameters (subscriptionId opts) From 67e41ea4230f4abc06d217e403b5daef048a7c9f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 31 Jul 2012 14:35:49 +0100 Subject: [PATCH 0142/2357] Initialize SSH once --- src/Control/Distributed/Process/Backend/Azure.hs | 9 ++------- .../Distributed/Process/Backend/Azure/CommandLine.hs | 8 +++++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index c7517607..f79e789a 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -33,9 +33,7 @@ import qualified Network.SSH.Client.LibSSH2 as SSH , readAllChannel ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH - ( initialize - , exit - , openChannelSession + ( openChannelSession , retryIfNeeded , channelExecute , writeChannel @@ -100,7 +98,6 @@ initializeBackend params = do -- | Start a CH node on the given virtual machine apiCopyToVM :: AzureParameters -> VirtualMachine -> IO () apiCopyToVM params (Azure.vmSshEndpoint -> Just ep) = do - _ <- SSH.initialize True _ <- SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) (azureSshPrivateKey params) @@ -110,14 +107,13 @@ apiCopyToVM params (Azure.vmSshEndpoint -> Just ep) = do (read $ endpointPort ep) $ \fd s -> do SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) -- SSH.execCommands fd s ["nohup /home/edsko/testservice >/dev/null 2>&1 &"] - SSH.exit + return () apiCopyToVM _ _ = error "copyToVM: No SSH endpoint" apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool apiCheckMD5 params (Azure.vmSshEndpoint -> Just ep) = do hash <- localHash params - _ <- SSH.initialize True match <- SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) (azureSshPrivateKey params) @@ -131,7 +127,6 @@ apiCheckMD5 params (Azure.vmSshEndpoint -> Just ep) = do SSH.channelSendEOF ch SSH.readAllChannel fd ch return (r == 0) - SSH.exit return match apiCheckMD5 _ _ = error "checkMD5: No SSH endpoint" diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index d37ca908..55da069f 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -3,6 +3,7 @@ import System.Environment (getArgs) import System.Exit (exitSuccess, exitFailure) import System.IO (hFlush, stdout) import Control.Monad (unless, forM, forM_) +import Control.Arrow (returnA) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName) , defaultAzureParameters @@ -12,7 +13,10 @@ import Control.Distributed.Process.Backend.Azure , VirtualMachine(vmName) , Backend(copyToVM, checkMD5) ) -import Control.Arrow (returnA) +import qualified Network.SSH.Client.LibSSH2.Foreign as SSH + ( initialize + , exit + ) import Control.Applicative ((<$>), (<*>), (<|>)) import Options.Applicative ( Parser @@ -141,6 +145,7 @@ commandParser = subparser main :: IO () main = do + _ <- SSH.initialize True cmd <- execParser opts case cmd of List {} -> do @@ -168,6 +173,7 @@ main = do if and matches then exitSuccess else exitFailure + SSH.exit where opts = info (helper <*> commandParser) ( fullDesc From 72eb21bef2a014301cbf1b3de958fff9c287d3a0 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 31 Jul 2012 16:57:44 +0100 Subject: [PATCH 0143/2357] Cleanup/skeleton onvm run --- .../Distributed/Process/Backend/Azure.hs | 86 +++++++++++++------ .../Process/Backend/Azure/CommandLine.hs | 48 ++++++++++- 2 files changed, 103 insertions(+), 31 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index f79e789a..52a48cb7 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -12,9 +12,12 @@ module Control.Distributed.Process.Backend.Azure import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) +import System.Posix.Types (Fd) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString.Lazy as BSL (readFile) import Control.Applicative ((<$>)) +import Control.Monad (void) +import Control.Exception (catches, Handler(Handler)) import Network.Azure.ServiceManagement ( CloudService(..) , VirtualMachine(..) @@ -31,6 +34,7 @@ import qualified Network.SSH.Client.LibSSH2 as SSH , scpSendFile , withChannel , readAllChannel + , Session ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( openChannelSession @@ -39,13 +43,19 @@ import qualified Network.SSH.Client.LibSSH2.Foreign as SSH , writeChannel , channelSendEOF ) +import qualified Network.SSH.Client.LibSSH2.Errors as SSH + ( ErrorCode + , NULL_POINTER + , getLastError + ) -- | Azure backend data Backend = Backend { -- | Find virtual machines cloudServices :: IO [CloudService] - , copyToVM :: VirtualMachine -> IO () + , copyToVM :: VirtualMachine -> IO () , checkMD5 :: VirtualMachine -> IO Bool + , runOnVM :: VirtualMachine -> IO () } data AzureParameters = AzureParameters { @@ -91,45 +101,67 @@ initializeBackend params = do (azureAuthPrivateKey params) return Backend { cloudServices = Azure.cloudServices setup - , copyToVM = apiCopyToVM params + , copyToVM = apiCopyToVM params , checkMD5 = apiCheckMD5 params + , runOnVM = apiRunOnVM params } -- | Start a CH node on the given virtual machine apiCopyToVM :: AzureParameters -> VirtualMachine -> IO () -apiCopyToVM params (Azure.vmSshEndpoint -> Just ep) = do - _ <- SSH.withSSH2 (azureSshKnownHosts params) - (azureSshPublicKey params) - (azureSshPrivateKey params) - (azureSshPassphrase params) - (azureSshUserName params) - (endpointVip ep) - (read $ endpointPort ep) $ \fd s -> do - SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) - -- SSH.execCommands fd s ["nohup /home/edsko/testservice >/dev/null 2>&1 &"] - return () -apiCopyToVM _ _ = - error "copyToVM: No SSH endpoint" +apiCopyToVM params vm = + void . withSSH2 params vm $ \fd s -> catchSshError s $ + SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) + +-- | Start the executable on the remote machine +apiRunOnVM :: AzureParameters -> VirtualMachine -> IO () +apiRunOnVM params vm = + void . withSSH2 params vm $ \fd s -> do + let exe = "/home/edsko/" ++ azureSshRemotePath params + putStrLn $ "Executing " ++ show exe + r <- SSH.execCommands fd s [exe ++ " onvm run " + ++ "2>&1" + ] -- ++ " >/dev/null 2>&1 &"] /usr/bin/nohup + print r +-- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool -apiCheckMD5 params (Azure.vmSshEndpoint -> Just ep) = do +apiCheckMD5 params vm = do hash <- localHash params - match <- SSH.withSSH2 (azureSshKnownHosts params) - (azureSshPublicKey params) - (azureSshPrivateKey params) - (azureSshPassphrase params) - (azureSshUserName params) - (endpointVip ep) - (read $ endpointPort ep) $ \fd s -> do + withSSH2 params vm $ \fd s -> do (r, _) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do - SSH.retryIfNeeded fd s $ SSH.channelExecute ch ("md5sum -c --status") + SSH.retryIfNeeded fd s $ SSH.channelExecute ch "md5sum -c --status" SSH.writeChannel ch $ show hash ++ " " ++ azureSshRemotePath params SSH.channelSendEOF ch SSH.readAllChannel fd ch return (r == 0) - return match -apiCheckMD5 _ _ = - error "checkMD5: No SSH endpoint" +withSSH2 :: AzureParameters -> VirtualMachine -> (Fd -> SSH.Session -> IO a) -> IO a +withSSH2 params (Azure.vmSshEndpoint -> Just ep) = + SSH.withSSH2 (azureSshKnownHosts params) + (azureSshPublicKey params) + (azureSshPrivateKey params) + (azureSshPassphrase params) + (azureSshUserName params) + (endpointVip ep) + (read $ endpointPort ep) +withSSH2 _ vm = + error $ "withSSH2: No SSH endpoint for virtual machine " ++ vmName vm + +catchSshError :: SSH.Session -> IO a -> IO a +catchSshError s io = + catches io [ Handler handleErrorCode + , Handler handleNullPointer + ] + where + handleErrorCode :: SSH.ErrorCode -> IO a + handleErrorCode _ = do + (_, str) <- SSH.getLastError s + error str + + handleNullPointer :: SSH.NULL_POINTER -> IO a + handleNullPointer _ = do + (_, str) <- SSH.getLastError s + error str + localHash :: AzureParameters -> IO MD5Digest localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 55da069f..739d902f 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE Arrows #-} import System.Environment (getArgs) import System.Exit (exitSuccess, exitFailure) import System.IO (hFlush, stdout) @@ -11,13 +10,13 @@ import Control.Distributed.Process.Backend.Azure , cloudServices , CloudService(cloudServiceName, cloudServiceVMs) , VirtualMachine(vmName) - , Backend(copyToVM, checkMD5) + , Backend(copyToVM, checkMD5, runOnVM) ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize , exit ) -import Control.Applicative ((<$>), (<*>), (<|>)) +import Control.Applicative ((<$>), (<*>), (<|>), pure) import Options.Applicative ( Parser , strOption @@ -73,6 +72,18 @@ data Command = , target :: Target , status :: Bool } + | RunOn { + azureOptions :: AzureOptions + , sshOptions :: SshOptions + , target :: Target + } + | OnVmCommand { + onVmCommand :: OnVmCommand + } + deriving Show + +data OnVmCommand = + OnVmRun deriving Show azureOptionsParser :: Parser AzureOptions @@ -134,9 +145,28 @@ commandParser = subparser ( command "list" (info listParser (progDesc "List Azure cloud services")) & command "install" (info copyToParser - (progDesc "Install the executable on a virtual machine")) + (progDesc "Install the executable")) & command "md5" (info checkMD5Parser (progDesc "Check if the remote and local MD5 hash match")) + & command "run" (info runOnParser + (progDesc "Run the executable")) + & command "onvm" (info onVmCommandParser + (progDesc "Commands used when running ON the vm (usually used internally only)")) + ) + +runOnParser :: Parser Command +runOnParser = RunOn + <$> azureOptionsParser + <*> sshOptionsParser + <*> targetParser + +onVmRunParser :: Parser OnVmCommand +onVmRunParser = pure OnVmRun + +onVmCommandParser :: Parser Command +onVmCommandParser = OnVmCommand <$> subparser + ( command "run" (info onVmRunParser + (progDesc "Run the executable")) ) -------------------------------------------------------------------------------- @@ -173,6 +203,16 @@ main = do if and matches then exitSuccess else exitFailure + RunOn {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + runOnVM backend vm + putStrLn "Done" + OnVmCommand (vmCmd@OnVmRun {}) -> do + putStrLn "Hello" SSH.exit where opts = info (helper <*> commandParser) From 49b971855402125e01638047a9042f9341cb42b6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 31 Jul 2012 17:35:13 +0100 Subject: [PATCH 0144/2357] Start CH node for onvm run We pass the wrong IP address at the moment; we need to figure out how to find the right one --- distributed-process-azure.cabal | 5 ++- .../Distributed/Process/Backend/Azure.hs | 12 ++++--- .../Process/Backend/Azure/CommandLine.hs | 36 +++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index e22ee9d2..e388fdf2 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -38,4 +38,7 @@ Executable cloud-haskell-azure libssh2 >= 0.2 && < 0.3, filepath >= 1.3 && < 1.4, distributed-process-azure >= 0.1 && < 0.2, - optparse-applicative >= 0.2 && < 0.4 + optparse-applicative >= 0.2 && < 0.4, + distributed-process >= 0.2 && < 0.3, + transformers >= 0.3 && < 0.4, + network-transport-tcp >= 0.2 && < 0.3 diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 52a48cb7..1826bad1 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -114,13 +114,15 @@ apiCopyToVM params vm = -- | Start the executable on the remote machine apiRunOnVM :: AzureParameters -> VirtualMachine -> IO () -apiRunOnVM params vm = +apiRunOnVM params vm@(Azure.vmSshEndpoint -> Just ep) = void . withSSH2 params vm $ \fd s -> do - let exe = "/home/edsko/" ++ azureSshRemotePath params + let exe = "/home/edsko/" ++ azureSshRemotePath params + ++ " onvm run " + ++ " --host " ++ endpointVip ep + ++ " --port 8080 " + ++ "2>&1" putStrLn $ "Executing " ++ show exe - r <- SSH.execCommands fd s [exe ++ " onvm run " - ++ "2>&1" - ] -- ++ " >/dev/null 2>&1 &"] /usr/bin/nohup + r <- SSH.execCommands fd s [exe] -- ++ " >/dev/null 2>&1 &"] /usr/bin/nohup print r -- | Check the MD5 hash of the executable on the remote machine diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 739d902f..64a4847a 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -2,7 +2,9 @@ import System.Environment (getArgs) import System.Exit (exitSuccess, exitFailure) import System.IO (hFlush, stdout) import Control.Monad (unless, forM, forM_) +import Control.Monad.IO.Class (liftIO) import Control.Arrow (returnA) +import Control.Exception (throwIO) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName) , defaultAzureParameters @@ -35,6 +37,9 @@ import Options.Applicative , switch ) import Options.Applicative.Arrows (runA, asA) +import Control.Distributed.Process (getSelfPid, RemoteTable) +import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) +import Network.Transport.TCP (createTransport, defaultTCPParameters) -------------------------------------------------------------------------------- -- Command line options -- @@ -83,7 +88,10 @@ data Command = deriving Show data OnVmCommand = - OnVmRun + OnVmRun { + onVmIP :: String + , onVmPort :: String + } deriving Show azureOptionsParser :: Parser AzureOptions @@ -161,7 +169,15 @@ runOnParser = RunOn <*> targetParser onVmRunParser :: Parser OnVmCommand -onVmRunParser = pure OnVmRun +onVmRunParser = OnVmRun + <$> strOption ( long "host" + & metavar "IP" + & help "IP address" + ) + <*> strOption ( long "port" + & metavar "PORT" + & help "port number" + ) onVmCommandParser :: Parser Command onVmCommandParser = OnVmCommand <$> subparser @@ -212,7 +228,8 @@ main = do runOnVM backend vm putStrLn "Done" OnVmCommand (vmCmd@OnVmRun {}) -> do - putStrLn "Hello" + let rtable = initRemoteTable + onVmRun rtable (onVmIP vmCmd) (onVmPort vmCmd) SSH.exit where opts = info (helper <*> commandParser) @@ -240,3 +257,16 @@ azureParameters opts (Just sshOpts) = do return params { azureSshUserName = remoteUser sshOpts } + +onVmRun :: RemoteTable -> String -> String -> IO () +onVmRun rtable host port = do + mTransport <- createTransport host port defaultTCPParameters + case mTransport of + Left err -> throwIO err + Right transport -> do + node <- newLocalNode transport rtable + runProcess node $ do + pid <- getSelfPid + liftIO . putStrLn $ "Azure controller has pid " ++ show pid + + From 24d773f4c4e495903a4697f71add4c2650bc36f0 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 1 Aug 2012 14:51:10 +0100 Subject: [PATCH 0145/2357] Basic implementation of 'run' w/closure libssh2 uses withCString which does character encoding under the hood. We need to add support for bytestrings to libssh2. --- distributed-process-azure.cabal | 8 +- .../Distributed/Process/Backend/Azure.hs | 41 +++- .../Process/Backend/Azure/CommandLine.hs | 206 ++++++++++-------- 3 files changed, 155 insertions(+), 100 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index e388fdf2..be8a7ca9 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -25,7 +25,9 @@ Library executable-path >= 0.0.3 && < 0.1, libssh2 >= 0.2 && < 0.3, pureMD5 >= 2.1 && < 2.2, - bytestring >= 0.9 && < 0.11 + bytestring >= 0.9 && < 0.11, + distributed-process >= 0.2 && < 0.3, + binary >= 0.5 && < 0.6 Exposed-modules: Control.Distributed.Process.Backend.Azure Extensions: ViewPatterns ghc-options: -Wall @@ -41,4 +43,6 @@ Executable cloud-haskell-azure optparse-applicative >= 0.2 && < 0.4, distributed-process >= 0.2 && < 0.3, transformers >= 0.3 && < 0.4, - network-transport-tcp >= 0.2 && < 0.3 + network-transport-tcp >= 0.2 && < 0.3, + binary >= 0.5 && < 0.6, + bytestring >= 0.9 && < 0.11 diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 1826bad1..cf188f65 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -13,11 +13,16 @@ import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) import System.Posix.Types (Fd) +import Data.Binary (encode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString.Lazy as BSL (readFile) +import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Control.Applicative ((<$>)) import Control.Monad (void) import Control.Exception (catches, Handler(Handler)) +import GHC.IO.Encoding (setForeignEncoding, char8) + +-- Azure import Network.Azure.ServiceManagement ( CloudService(..) , VirtualMachine(..) @@ -28,6 +33,8 @@ import qualified Network.Azure.ServiceManagement as Azure , azureSetup , vmSshEndpoint ) + +-- SSH import qualified Network.SSH.Client.LibSSH2 as SSH ( withSSH2 , execCommands @@ -49,13 +56,22 @@ import qualified Network.SSH.Client.LibSSH2.Errors as SSH , getLastError ) +-- CH +import Control.Distributed.Process + ( Closure + , Process + ) + -- | Azure backend data Backend = Backend { -- | Find virtual machines cloudServices :: IO [CloudService] - , copyToVM :: VirtualMachine -> IO () - , checkMD5 :: VirtualMachine -> IO Bool - , runOnVM :: VirtualMachine -> IO () + -- | Copy the executable to a virtual machine + , copyToVM :: VirtualMachine -> IO () + -- | Check the MD5 hash of the remote executable + , checkMD5 :: VirtualMachine -> IO Bool + -- | @runOnVM vm port p@ starts a CH node on port 'port' and runs 'p' + , runOnVM :: VirtualMachine -> String -> Closure (Process ()) -> IO () } data AzureParameters = AzureParameters { @@ -113,16 +129,23 @@ apiCopyToVM params vm = SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) -- | Start the executable on the remote machine -apiRunOnVM :: AzureParameters -> VirtualMachine -> IO () -apiRunOnVM params vm@(Azure.vmSshEndpoint -> Just ep) = +apiRunOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Process ()) -> IO () +apiRunOnVM params vm port proc = void . withSSH2 params vm $ \fd s -> do let exe = "/home/edsko/" ++ azureSshRemotePath params ++ " onvm run " - ++ " --host " ++ endpointVip ep - ++ " --port 8080 " - ++ "2>&1" + ++ " --host " ++ vmIpAddress vm + ++ " --port " ++ port + ++ " 2>&1" putStrLn $ "Executing " ++ show exe - r <- SSH.execCommands fd s [exe] -- ++ " >/dev/null 2>&1 &"] /usr/bin/nohup + r <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do + SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe + let enc = encode proc + putStrLn $ "Sending closure of size " ++ show (length (BSLC.unpack enc)) + setForeignEncoding char8 + SSH.writeChannel ch $ BSLC.unpack enc + SSH.channelSendEOF ch + SSH.readAllChannel fd ch print r -- | Check the MD5 hash of the executable on the remote machine diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 64a4847a..f4b8f608 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,10 +1,19 @@ +{-# LANGUAGE TemplateHaskell #-} +import Prelude hiding (catch) import System.Environment (getArgs) import System.Exit (exitSuccess, exitFailure) -import System.IO (hFlush, stdout) -import Control.Monad (unless, forM, forM_) +import System.IO + ( hFlush + , stdout + , stdin + , hSetBinaryMode + ) +import Data.Binary (decode) +import qualified Data.ByteString.Lazy as BSL (ByteString, length, getContents) +import Control.Monad (unless, forM, forM_, join) import Control.Monad.IO.Class (liftIO) import Control.Arrow (returnA) -import Control.Exception (throwIO) +import Control.Exception (throwIO, SomeException) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName) , defaultAzureParameters @@ -37,10 +46,109 @@ import Options.Applicative , switch ) import Options.Applicative.Arrows (runA, asA) -import Control.Distributed.Process (getSelfPid, RemoteTable) +import Control.Distributed.Process + ( getSelfPid + , RemoteTable + , Closure + , Process + , unClosure + , catch + ) +import Control.Distributed.Process.Closure (remotable, mkClosure) import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) import Network.Transport.TCP (createTransport, defaultTCPParameters) +cprint :: String -> Process () +cprint = liftIO . putStrLn + +remotable ['cprint] + +-------------------------------------------------------------------------------- +-- Main -- +-------------------------------------------------------------------------------- + +main :: IO () +main = do + _ <- SSH.initialize True + cmd <- execParser opts + case cmd of + List {} -> do + params <- azureParameters (azureOptions cmd) Nothing + backend <- initializeBackend params + css <- cloudServices backend + mapM_ print css + CopyTo {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + copyToVM backend vm + putStrLn "Done" + CheckMD5 {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + matches <- forM (findTarget (target cmd) css) $ \vm -> do + unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout + match <- checkMD5 backend vm + unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" + return match + if and matches + then exitSuccess + else exitFailure + RunOn {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + runOnVM backend vm (port cmd) ($(mkClosure 'cprint) "Hello world") + OnVmCommand (vmCmd@OnVmRun {}) -> do + let rtable = __remoteTable initRemoteTable + onVmRun rtable (onVmIP vmCmd) (onVmPort vmCmd) + SSH.exit + where + opts = info (helper <*> commandParser) + ( fullDesc + & header "Cloud Haskell backend for Azure" + ) + +findTarget :: Target -> [CloudService] -> [VirtualMachine] +findTarget (CloudService cs) css = + concatMap cloudServiceVMs . filter ((== cs) . cloudServiceName) $ css +findTarget (VirtualMachine virtualMachine) css = + [ vm | vm <- concatMap cloudServiceVMs css + , vmName vm == virtualMachine + ] + +azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters +azureParameters opts Nothing = + defaultAzureParameters (subscriptionId opts) + (pathToCert opts) + (pathToKey opts) +azureParameters opts (Just sshOpts) = do + params <- defaultAzureParameters (subscriptionId opts) + (pathToCert opts) + (pathToKey opts) + return params { + azureSshUserName = remoteUser sshOpts + } + +onVmRun :: RemoteTable -> String -> String -> IO () +onVmRun rtable host port = do + hSetBinaryMode stdin True + proc <- BSL.getContents :: IO BSL.ByteString + putStrLn $ "Got closure encoding of length " ++ show (BSL.length proc) + mTransport <- createTransport host port defaultTCPParameters + case mTransport of + Left err -> throwIO err + Right transport -> do + node <- newLocalNode transport rtable + runProcess node $ do + liftIO $ putStrLn "Starting remote node" + catch (join . unClosure . decode $ proc) (\e -> liftIO $ print (e :: SomeException)) + -------------------------------------------------------------------------------- -- Command line options -- -------------------------------------------------------------------------------- @@ -81,6 +189,7 @@ data Command = azureOptions :: AzureOptions , sshOptions :: SshOptions , target :: Target + , port :: String } | OnVmCommand { onVmCommand :: OnVmCommand @@ -167,6 +276,10 @@ runOnParser = RunOn <$> azureOptionsParser <*> sshOptionsParser <*> targetParser + <*> strOption ( long "port" + & metavar "PORT" + & help "Port number of the CH instance" + ) onVmRunParser :: Parser OnVmCommand onVmRunParser = OnVmRun @@ -185,88 +298,3 @@ onVmCommandParser = OnVmCommand <$> subparser (progDesc "Run the executable")) ) --------------------------------------------------------------------------------- --- Main -- --------------------------------------------------------------------------------- - -main :: IO () -main = do - _ <- SSH.initialize True - cmd <- execParser opts - case cmd of - List {} -> do - params <- azureParameters (azureOptions cmd) Nothing - backend <- initializeBackend params - css <- cloudServices backend - mapM_ print css - CopyTo {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - copyToVM backend vm - putStrLn "Done" - CheckMD5 {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - matches <- forM (findTarget (target cmd) css) $ \vm -> do - unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout - match <- checkMD5 backend vm - unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" - return match - if and matches - then exitSuccess - else exitFailure - RunOn {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - runOnVM backend vm - putStrLn "Done" - OnVmCommand (vmCmd@OnVmRun {}) -> do - let rtable = initRemoteTable - onVmRun rtable (onVmIP vmCmd) (onVmPort vmCmd) - SSH.exit - where - opts = info (helper <*> commandParser) - ( fullDesc - & header "Cloud Haskell backend for Azure" - ) - -findTarget :: Target -> [CloudService] -> [VirtualMachine] -findTarget (CloudService cs) css = - concatMap cloudServiceVMs . filter ((== cs) . cloudServiceName) $ css -findTarget (VirtualMachine virtualMachine) css = - [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == virtualMachine - ] - -azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters -azureParameters opts Nothing = - defaultAzureParameters (subscriptionId opts) - (pathToCert opts) - (pathToKey opts) -azureParameters opts (Just sshOpts) = do - params <- defaultAzureParameters (subscriptionId opts) - (pathToCert opts) - (pathToKey opts) - return params { - azureSshUserName = remoteUser sshOpts - } - -onVmRun :: RemoteTable -> String -> String -> IO () -onVmRun rtable host port = do - mTransport <- createTransport host port defaultTCPParameters - case mTransport of - Left err -> throwIO err - Right transport -> do - node <- newLocalNode transport rtable - runProcess node $ do - pid <- getSelfPid - liftIO . putStrLn $ "Azure controller has pid " ++ show pid - - From 8949ce9f73b96d44cf374adec7a828bd1a7adad3 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 1 Aug 2012 15:53:58 +0100 Subject: [PATCH 0146/2357] Make use of new libssh2 bytestring functionality --- .../Distributed/Process/Backend/Azure.hs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index cf188f65..0795451e 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -15,12 +15,10 @@ import System.Environment.Executable (getExecutablePath) import System.Posix.Types (Fd) import Data.Binary (encode) import Data.Digest.Pure.MD5 (md5, MD5Digest) -import qualified Data.ByteString.Lazy as BSL (readFile) -import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) +import qualified Data.ByteString.Lazy as BSL (readFile, length) import Control.Applicative ((<$>)) import Control.Monad (void) import Control.Exception (catches, Handler(Handler)) -import GHC.IO.Encoding (setForeignEncoding, char8) -- Azure import Network.Azure.ServiceManagement @@ -40,7 +38,6 @@ import qualified Network.SSH.Client.LibSSH2 as SSH , execCommands , scpSendFile , withChannel - , readAllChannel , Session ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH @@ -55,6 +52,10 @@ import qualified Network.SSH.Client.LibSSH2.Errors as SSH , NULL_POINTER , getLastError ) +import qualified Network.SSH.Client.LibSSH2.ByteString.Lazy as SSHBS + ( writeChannel + , readAllChannel + ) -- CH import Control.Distributed.Process @@ -140,12 +141,9 @@ apiRunOnVM params vm port proc = putStrLn $ "Executing " ++ show exe r <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe - let enc = encode proc - putStrLn $ "Sending closure of size " ++ show (length (BSLC.unpack enc)) - setForeignEncoding char8 - SSH.writeChannel ch $ BSLC.unpack enc + SSHBS.writeChannel fd ch (encode proc) SSH.channelSendEOF ch - SSH.readAllChannel fd ch + SSHBS.readAllChannel fd ch print r -- | Check the MD5 hash of the executable on the remote machine @@ -157,7 +155,7 @@ apiCheckMD5 params vm = do SSH.retryIfNeeded fd s $ SSH.channelExecute ch "md5sum -c --status" SSH.writeChannel ch $ show hash ++ " " ++ azureSshRemotePath params SSH.channelSendEOF ch - SSH.readAllChannel fd ch + SSHBS.readAllChannel fd ch return (r == 0) withSSH2 :: AzureParameters -> VirtualMachine -> (Fd -> SSH.Session -> IO a) -> IO a From 5533de5f6fb76c2526399d3d4cb5a793c735f316 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 1 Aug 2012 16:14:28 +0100 Subject: [PATCH 0147/2357] Don't hardcode remote path --- src/Control/Distributed/Process/Backend/Azure.hs | 2 +- src/Control/Distributed/Process/Backend/Azure/CommandLine.hs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 0795451e..40235033 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -133,7 +133,7 @@ apiCopyToVM params vm = apiRunOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Process ()) -> IO () apiRunOnVM params vm port proc = void . withSSH2 params vm $ \fd s -> do - let exe = "/home/edsko/" ++ azureSshRemotePath params + let exe = "PATH=. " ++ azureSshRemotePath params ++ " onvm run " ++ " --host " ++ vmIpAddress vm ++ " --port " ++ port diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index f4b8f608..815bd1c4 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -139,15 +139,12 @@ onVmRun :: RemoteTable -> String -> String -> IO () onVmRun rtable host port = do hSetBinaryMode stdin True proc <- BSL.getContents :: IO BSL.ByteString - putStrLn $ "Got closure encoding of length " ++ show (BSL.length proc) mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> throwIO err Right transport -> do node <- newLocalNode transport rtable - runProcess node $ do - liftIO $ putStrLn "Starting remote node" - catch (join . unClosure . decode $ proc) (\e -> liftIO $ print (e :: SomeException)) + runProcess node $ join . unClosure . decode $ proc -------------------------------------------------------------------------------- -- Command line options -- From 9b84a099556832b7d5e81418c42137410fbf6529 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 1 Aug 2012 17:03:22 +0100 Subject: [PATCH 0148/2357] Split into a generic main and a specific driver --- distributed-process-azure.cabal | 16 +- .../Distributed/Process/Backend/Azure.hs | 9 +- .../Process/Backend/Azure/CommandLine.hs | 295 +---------------- .../Process/Backend/Azure/GenericMain.hs | 299 ++++++++++++++++++ 4 files changed, 316 insertions(+), 303 deletions(-) create mode 100644 src/Control/Distributed/Process/Backend/Azure/GenericMain.hs diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index be8a7ca9..5fc3bd0a 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -27,8 +27,11 @@ Library pureMD5 >= 2.1 && < 2.2, bytestring >= 0.9 && < 0.11, distributed-process >= 0.2 && < 0.3, - binary >= 0.5 && < 0.6 - Exposed-modules: Control.Distributed.Process.Backend.Azure + binary >= 0.5 && < 0.6, + network-transport-tcp >= 0.2 && < 0.3, + optparse-applicative >= 0.2 && < 0.4 + Exposed-modules: Control.Distributed.Process.Backend.Azure, + Control.Distributed.Process.Backend.Azure.GenericMain Extensions: ViewPatterns ghc-options: -Wall HS-Source-Dirs: src @@ -36,13 +39,6 @@ Library Executable cloud-haskell-azure Main-Is: src/Control/Distributed/Process/Backend/Azure/CommandLine.hs Build-Depends: base >= 4.4 && < 5, - utf8-string >= 0.3 && < 0.4, - libssh2 >= 0.2 && < 0.3, - filepath >= 1.3 && < 1.4, distributed-process-azure >= 0.1 && < 0.2, - optparse-applicative >= 0.2 && < 0.4, distributed-process >= 0.2 && < 0.3, - transformers >= 0.3 && < 0.4, - network-transport-tcp >= 0.2 && < 0.3, - binary >= 0.5 && < 0.6, - bytestring >= 0.9 && < 0.11 + transformers >= 0.3 && < 0.4 diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 40235033..b5705d4b 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -15,7 +15,8 @@ import System.Environment.Executable (getExecutablePath) import System.Posix.Types (Fd) import Data.Binary (encode) import Data.Digest.Pure.MD5 (md5, MD5Digest) -import qualified Data.ByteString.Lazy as BSL (readFile, length) +import qualified Data.ByteString.Lazy as BSL (readFile) +import qualified Data.ByteString.Lazy.Char8 as BSLC (putStr) import Control.Applicative ((<$>)) import Control.Monad (void) import Control.Exception (catches, Handler(Handler)) @@ -35,7 +36,6 @@ import qualified Network.Azure.ServiceManagement as Azure -- SSH import qualified Network.SSH.Client.LibSSH2 as SSH ( withSSH2 - , execCommands , scpSendFile , withChannel , Session @@ -138,13 +138,12 @@ apiRunOnVM params vm port proc = ++ " --host " ++ vmIpAddress vm ++ " --port " ++ port ++ " 2>&1" - putStrLn $ "Executing " ++ show exe - r <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do + (_, r) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe SSHBS.writeChannel fd ch (encode proc) SSH.channelSendEOF ch SSHBS.readAllChannel fd ch - print r + BSLC.putStr r -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 815bd1c4..cb427475 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,297 +1,16 @@ {-# LANGUAGE TemplateHaskell #-} -import Prelude hiding (catch) -import System.Environment (getArgs) -import System.Exit (exitSuccess, exitFailure) -import System.IO - ( hFlush - , stdout - , stdin - , hSetBinaryMode - ) -import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (ByteString, length, getContents) -import Control.Monad (unless, forM, forM_, join) + import Control.Monad.IO.Class (liftIO) -import Control.Arrow (returnA) -import Control.Exception (throwIO, SomeException) -import Control.Distributed.Process.Backend.Azure - ( AzureParameters(azureSshUserName) - , defaultAzureParameters - , initializeBackend - , cloudServices - , CloudService(cloudServiceName, cloudServiceVMs) - , VirtualMachine(vmName) - , Backend(copyToVM, checkMD5, runOnVM) - ) -import qualified Network.SSH.Client.LibSSH2.Foreign as SSH - ( initialize - , exit - ) -import Control.Applicative ((<$>), (<*>), (<|>), pure) -import Options.Applicative - ( Parser - , strOption - , long - , (&) - , metavar - , help - , subparser - , command - , info - , progDesc - , execParser - , helper - , fullDesc - , header - , switch - ) -import Options.Applicative.Arrows (runA, asA) -import Control.Distributed.Process - ( getSelfPid - , RemoteTable - , Closure - , Process - , unClosure - , catch - ) +import Control.Distributed.Process (Process) import Control.Distributed.Process.Closure (remotable, mkClosure) -import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) -import Network.Transport.TCP (createTransport, defaultTCPParameters) +import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) cprint :: String -> Process () cprint = liftIO . putStrLn remotable ['cprint] --------------------------------------------------------------------------------- --- Main -- --------------------------------------------------------------------------------- - -main :: IO () -main = do - _ <- SSH.initialize True - cmd <- execParser opts - case cmd of - List {} -> do - params <- azureParameters (azureOptions cmd) Nothing - backend <- initializeBackend params - css <- cloudServices backend - mapM_ print css - CopyTo {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - copyToVM backend vm - putStrLn "Done" - CheckMD5 {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - matches <- forM (findTarget (target cmd) css) $ \vm -> do - unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout - match <- checkMD5 backend vm - unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" - return match - if and matches - then exitSuccess - else exitFailure - RunOn {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - runOnVM backend vm (port cmd) ($(mkClosure 'cprint) "Hello world") - OnVmCommand (vmCmd@OnVmRun {}) -> do - let rtable = __remoteTable initRemoteTable - onVmRun rtable (onVmIP vmCmd) (onVmPort vmCmd) - SSH.exit - where - opts = info (helper <*> commandParser) - ( fullDesc - & header "Cloud Haskell backend for Azure" - ) - -findTarget :: Target -> [CloudService] -> [VirtualMachine] -findTarget (CloudService cs) css = - concatMap cloudServiceVMs . filter ((== cs) . cloudServiceName) $ css -findTarget (VirtualMachine virtualMachine) css = - [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == virtualMachine - ] - -azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters -azureParameters opts Nothing = - defaultAzureParameters (subscriptionId opts) - (pathToCert opts) - (pathToKey opts) -azureParameters opts (Just sshOpts) = do - params <- defaultAzureParameters (subscriptionId opts) - (pathToCert opts) - (pathToKey opts) - return params { - azureSshUserName = remoteUser sshOpts - } - -onVmRun :: RemoteTable -> String -> String -> IO () -onVmRun rtable host port = do - hSetBinaryMode stdin True - proc <- BSL.getContents :: IO BSL.ByteString - mTransport <- createTransport host port defaultTCPParameters - case mTransport of - Left err -> throwIO err - Right transport -> do - node <- newLocalNode transport rtable - runProcess node $ join . unClosure . decode $ proc - --------------------------------------------------------------------------------- --- Command line options -- --------------------------------------------------------------------------------- - -data AzureOptions = AzureOptions { - subscriptionId :: String - , pathToCert :: FilePath - , pathToKey :: FilePath - } - deriving Show - -data SshOptions = SshOptions { - remoteUser :: String - } - deriving Show - -data Target = - VirtualMachine String - | CloudService String - deriving Show - -data Command = - List { - azureOptions :: AzureOptions - } - | CopyTo { - azureOptions :: AzureOptions - , sshOptions :: SshOptions - , target :: Target - } - | CheckMD5 { - azureOptions :: AzureOptions - , sshOptions :: SshOptions - , target :: Target - , status :: Bool - } - | RunOn { - azureOptions :: AzureOptions - , sshOptions :: SshOptions - , target :: Target - , port :: String - } - | OnVmCommand { - onVmCommand :: OnVmCommand - } - deriving Show - -data OnVmCommand = - OnVmRun { - onVmIP :: String - , onVmPort :: String - } - deriving Show - -azureOptionsParser :: Parser AzureOptions -azureOptionsParser = AzureOptions - <$> strOption ( long "subscription-id" - & metavar "SID" - & help "Azure subscription ID" - ) - <*> strOption ( long "certificate" - & metavar "CERT" - & help "X509 certificate" - ) - <*> strOption ( long "private" - & metavar "PRI" - & help "Private key in PKCS#1 format" - ) - -sshOptionsParser :: Parser SshOptions -sshOptionsParser = SshOptions - <$> strOption ( long "user" - & metavar "USER" - & help "Remove SSH username" - ) - -listParser :: Parser Command -listParser = List <$> azureOptionsParser - -copyToParser :: Parser Command -copyToParser = CopyTo - <$> azureOptionsParser - <*> sshOptionsParser - <*> targetParser - -targetParser :: Parser Target -targetParser = - ( VirtualMachine <$> strOption ( long "virtual-machine" - & metavar "VM" - & help "Virtual machine name" - ) - ) - <|> - ( CloudService <$> strOption ( long "cloud-service" - & metavar "CS" - & help "Cloud service name" - ) - ) - -checkMD5Parser :: Parser Command -checkMD5Parser = CheckMD5 - <$> azureOptionsParser - <*> sshOptionsParser - <*> targetParser - <*> switch ( long "status" - & help "Don't output anything, status code shows success" - ) - -commandParser :: Parser Command -commandParser = subparser - ( command "list" (info listParser - (progDesc "List Azure cloud services")) - & command "install" (info copyToParser - (progDesc "Install the executable")) - & command "md5" (info checkMD5Parser - (progDesc "Check if the remote and local MD5 hash match")) - & command "run" (info runOnParser - (progDesc "Run the executable")) - & command "onvm" (info onVmCommandParser - (progDesc "Commands used when running ON the vm (usually used internally only)")) - ) - -runOnParser :: Parser Command -runOnParser = RunOn - <$> azureOptionsParser - <*> sshOptionsParser - <*> targetParser - <*> strOption ( long "port" - & metavar "PORT" - & help "Port number of the CH instance" - ) - -onVmRunParser :: Parser OnVmCommand -onVmRunParser = OnVmRun - <$> strOption ( long "host" - & metavar "IP" - & help "IP address" - ) - <*> strOption ( long "port" - & metavar "PORT" - & help "port number" - ) - -onVmCommandParser :: Parser Command -onVmCommandParser = OnVmCommand <$> subparser - ( command "run" (info onVmRunParser - (progDesc "Run the executable")) - ) - +main = genericMain __remoteTable $ \cmd -> + case cmd of + "hello" -> return $ $(mkClosure 'cprint) "Hi world!" + _ -> error "unknown command" diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs new file mode 100644 index 00000000..0d60146c --- /dev/null +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -0,0 +1,299 @@ +-- | Generic main +module Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) where + +import Prelude hiding (catch) +import System.Exit (exitSuccess, exitFailure) +import System.IO + ( hFlush + , stdout + , stdin + , hSetBinaryMode + ) +import Data.Binary (decode) +import qualified Data.ByteString.Lazy as BSL (ByteString, getContents) +import Control.Monad (unless, forM, forM_, join) +import Control.Exception (throwIO) +import Control.Distributed.Process.Backend.Azure + ( AzureParameters(azureSshUserName) + , defaultAzureParameters + , initializeBackend + , cloudServices + , CloudService(cloudServiceName, cloudServiceVMs) + , VirtualMachine(vmName) + , Backend(copyToVM, checkMD5, runOnVM) + ) +import qualified Network.SSH.Client.LibSSH2.Foreign as SSH + ( initialize + , exit + ) +import Control.Applicative ((<$>), (<*>), (<|>)) +import Options.Applicative + ( Parser + , strOption + , long + , (&) + , metavar + , help + , subparser + , command + , info + , progDesc + , execParser + , helper + , fullDesc + , header + , switch + ) +import Control.Distributed.Process + ( RemoteTable + , Closure + , Process + , unClosure + ) +import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) +import Network.Transport.TCP (createTransport, defaultTCPParameters) + +-------------------------------------------------------------------------------- +-- Main -- +-------------------------------------------------------------------------------- + +genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table + -> (String -> IO (Closure (Process ()))) -- ^ Closures to support in 'run' + -> IO () +genericMain remoteTable cmds = do + _ <- SSH.initialize True + cmd <- execParser opts + case cmd of + List {} -> do + params <- azureParameters (azureOptions cmd) Nothing + backend <- initializeBackend params + css <- cloudServices backend + mapM_ print css + CopyTo {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + copyToVM backend vm + putStrLn "Done" + CheckMD5 {} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + matches <- forM (findTarget (target cmd) css) $ \vm -> do + unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout + match <- checkMD5 backend vm + unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" + return match + if and matches + then exitSuccess + else exitFailure + RunOn {} -> do + closure <- cmds (closureId cmd) + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + runOnVM backend vm (remotePort cmd) closure + OnVmCommand (vmCmd@OnVmRun {}) -> do + onVmRun (remoteTable initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) + SSH.exit + where + opts = info (helper <*> commandParser) + ( fullDesc + & header "Cloud Haskell backend for Azure" + ) + +findTarget :: Target -> [CloudService] -> [VirtualMachine] +findTarget (CloudService cs) css = + concatMap cloudServiceVMs . filter ((== cs) . cloudServiceName) $ css +findTarget (VirtualMachine virtualMachine) css = + [ vm | vm <- concatMap cloudServiceVMs css + , vmName vm == virtualMachine + ] + +azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters +azureParameters opts Nothing = + defaultAzureParameters (subscriptionId opts) + (pathToCert opts) + (pathToKey opts) +azureParameters opts (Just sshOpts) = do + params <- defaultAzureParameters (subscriptionId opts) + (pathToCert opts) + (pathToKey opts) + return params { + azureSshUserName = remoteUser sshOpts + } + +onVmRun :: RemoteTable -> String -> String -> IO () +onVmRun rtable host port = do + hSetBinaryMode stdin True + proc <- BSL.getContents :: IO BSL.ByteString + mTransport <- createTransport host port defaultTCPParameters + case mTransport of + Left err -> throwIO err + Right transport -> do + node <- newLocalNode transport rtable + runProcess node $ join . unClosure . decode $ proc + +-------------------------------------------------------------------------------- +-- Command line options -- +-------------------------------------------------------------------------------- + +data AzureOptions = AzureOptions { + subscriptionId :: String + , pathToCert :: FilePath + , pathToKey :: FilePath + } + deriving Show + +data SshOptions = SshOptions { + remoteUser :: String + } + deriving Show + +data Target = + VirtualMachine String + | CloudService String + deriving Show + +data Command = + List { + azureOptions :: AzureOptions + } + | CopyTo { + azureOptions :: AzureOptions + , sshOptions :: SshOptions + , target :: Target + } + | CheckMD5 { + azureOptions :: AzureOptions + , sshOptions :: SshOptions + , target :: Target + , status :: Bool + } + | RunOn { + azureOptions :: AzureOptions + , sshOptions :: SshOptions + , target :: Target + , remotePort :: String + , closureId :: String + , background :: Bool + } + | OnVmCommand { + onVmCommand :: OnVmCommand + } + deriving Show + +data OnVmCommand = + OnVmRun { + onVmIP :: String + , onVmPort :: String + } + deriving Show + +azureOptionsParser :: Parser AzureOptions +azureOptionsParser = AzureOptions + <$> strOption ( long "subscription-id" + & metavar "SID" + & help "Azure subscription ID" + ) + <*> strOption ( long "certificate" + & metavar "CERT" + & help "X509 certificate" + ) + <*> strOption ( long "private" + & metavar "PRI" + & help "Private key in PKCS#1 format" + ) + +sshOptionsParser :: Parser SshOptions +sshOptionsParser = SshOptions + <$> strOption ( long "user" + & metavar "USER" + & help "Remove SSH username" + ) + +listParser :: Parser Command +listParser = List <$> azureOptionsParser + +copyToParser :: Parser Command +copyToParser = CopyTo + <$> azureOptionsParser + <*> sshOptionsParser + <*> targetParser + +targetParser :: Parser Target +targetParser = + ( VirtualMachine <$> strOption ( long "virtual-machine" + & metavar "VM" + & help "Virtual machine name" + ) + ) + <|> + ( CloudService <$> strOption ( long "cloud-service" + & metavar "CS" + & help "Cloud service name" + ) + ) + +checkMD5Parser :: Parser Command +checkMD5Parser = CheckMD5 + <$> azureOptionsParser + <*> sshOptionsParser + <*> targetParser + <*> switch ( long "status" + & help "Don't output anything, status code shows success" + ) + +commandParser :: Parser Command +commandParser = subparser + ( command "list" (info listParser + (progDesc "List Azure cloud services")) + & command "install" (info copyToParser + (progDesc "Install the executable")) + & command "md5" (info checkMD5Parser + (progDesc "Check if the remote and local MD5 hash match")) + & command "run" (info runOnParser + (progDesc "Run the executable")) + & command "onvm" (info onVmCommandParser + (progDesc "Commands used when running ON the vm (usually used internally only)")) + ) + +runOnParser :: Parser Command +runOnParser = RunOn + <$> azureOptionsParser + <*> sshOptionsParser + <*> targetParser + <*> strOption ( long "port" + & metavar "PORT" + & help "Port number of the CH instance" + ) + <*> strOption ( long "closure" + & metavar "PROC" + & help "Process to run on the CH instance" + ) + <*> switch ( long "background" + & help "Run the process in the background" + ) + +onVmRunParser :: Parser OnVmCommand +onVmRunParser = OnVmRun + <$> strOption ( long "host" + & metavar "IP" + & help "IP address" + ) + <*> strOption ( long "port" + & metavar "PORT" + & help "port number" + ) + +onVmCommandParser :: Parser Command +onVmCommandParser = OnVmCommand <$> subparser + ( command "run" (info onVmRunParser + (progDesc "Run the executable")) + ) + + From e387574142fdd6dcf801734e19e5ad70eaa32847 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 2 Aug 2012 12:28:36 +0100 Subject: [PATCH 0149/2357] Actually, Static constructor should not be exposed --- distributed-process-azure.cabal | 8 ++- .../Distributed/Process/Backend/Azure.hs | 63 ++++++++++++------- .../Process/Backend/Azure/CommandLine.hs | 17 ++--- .../Process/Backend/Azure/GenericMain.hs | 32 +++++++--- 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 5fc3bd0a..a8273f80 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -29,10 +29,14 @@ Library distributed-process >= 0.2 && < 0.3, binary >= 0.5 && < 0.6, network-transport-tcp >= 0.2 && < 0.3, - optparse-applicative >= 0.2 && < 0.4 + optparse-applicative >= 0.2 && < 0.4, + transformers >= 0.3 && < 0.4 Exposed-modules: Control.Distributed.Process.Backend.Azure, Control.Distributed.Process.Backend.Azure.GenericMain - Extensions: ViewPatterns + Extensions: ViewPatterns, + RankNTypes, + ExistentialQuantification, + ScopedTypeVariables ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index b5705d4b..a4b9fa01 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE TemplateHaskell #-} module Control.Distributed.Process.Backend.Azure ( -- * Initialization Backend(..) @@ -13,13 +14,14 @@ import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) import System.Posix.Types (Fd) -import Data.Binary (encode) +import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) -import qualified Data.ByteString.Lazy as BSL (readFile) -import qualified Data.ByteString.Lazy.Char8 as BSLC (putStr) +import qualified Data.ByteString.Lazy as BSL (readFile, putStr) +import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void) import Control.Exception (catches, Handler(Handler)) +import Control.Monad.IO.Class (liftIO) -- Azure import Network.Azure.ServiceManagement @@ -61,7 +63,23 @@ import qualified Network.SSH.Client.LibSSH2.ByteString.Lazy as SSHBS import Control.Distributed.Process ( Closure , Process + , Static ) +import Control.Distributed.Process.Closure + ( remotable + , mkClosure + , cpBind + , SerializableDict(SerializableDict) + ) +import Control.Distributed.Process.Serializable (Serializable) + +encodeToStdout :: Serializable a => a -> Process () +encodeToStdout = liftIO . BSL.putStr . encode + +encodeToStdoutDict :: SerializableDict a -> a -> Process () +encodeToStdoutDict SerializableDict = encodeToStdout + +remotable ['encodeToStdoutDict] -- | Azure backend data Backend = Backend { @@ -71,8 +89,8 @@ data Backend = Backend { , copyToVM :: VirtualMachine -> IO () -- | Check the MD5 hash of the remote executable , checkMD5 :: VirtualMachine -> IO Bool - -- | @runOnVM vm port p@ starts a CH node on port 'port' and runs 'p' - , runOnVM :: VirtualMachine -> String -> Closure (Process ()) -> IO () + -- | @runOnVM vm port p bg@ starts a CH node on port 'port' and runs 'p' + , callOnVM :: forall a. Serializable a => Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a } data AzureParameters = AzureParameters { @@ -120,7 +138,7 @@ initializeBackend params = do cloudServices = Azure.cloudServices setup , copyToVM = apiCopyToVM params , checkMD5 = apiCheckMD5 params - , runOnVM = apiRunOnVM params + , callOnVM = apiCallOnVM params } -- | Start a CH node on the given virtual machine @@ -129,21 +147,24 @@ apiCopyToVM params vm = void . withSSH2 params vm $ \fd s -> catchSshError s $ SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) --- | Start the executable on the remote machine -apiRunOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Process ()) -> IO () -apiRunOnVM params vm port proc = - void . withSSH2 params vm $ \fd s -> do - let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm run " - ++ " --host " ++ vmIpAddress vm - ++ " --port " ++ port - ++ " 2>&1" - (_, r) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do - SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe - SSHBS.writeChannel fd ch (encode proc) - SSH.channelSendEOF ch - SSHBS.readAllChannel fd ch - BSLC.putStr r +-- | Call a process on a VM +apiCallOnVM :: Serializable a => AzureParameters -> Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a +apiCallOnVM params dict vm port proc = + withSSH2 params vm $ \fd s -> do + let exe = "PATH=. " ++ azureSshRemotePath params + ++ " onvm run " + ++ " --host " ++ vmIpAddress vm + ++ " --port " ++ port + ++ " 2>&1" + (_, r) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do + SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe + SSHBS.writeChannel fd ch (encode proc) + SSH.channelSendEOF ch + SSHBS.readAllChannel fd ch + return (decode r) + where + proc' :: Closure (Process ()) + proc' = proc `cpBind` undefined -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index cb427475..8be6f475 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,16 +1,19 @@ {-# LANGUAGE TemplateHaskell #-} import Control.Monad.IO.Class (liftIO) -import Control.Distributed.Process (Process) -import Control.Distributed.Process.Closure (remotable, mkClosure) -import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) +import Control.Distributed.Process (Process, ProcessId, getSelfPid) +import Control.Distributed.Process.Closure (remotable, mkClosure, sdictProcessId) +import Control.Distributed.Process.Backend.Azure.GenericMain + ( genericMain + , ProcessPair(..) + ) -cprint :: String -> Process () -cprint = liftIO . putStrLn +getPid :: () -> Process ProcessId +getPid () = getSelfPid -remotable ['cprint] +remotable ['getPid] main = genericMain __remoteTable $ \cmd -> case cmd of - "hello" -> return $ $(mkClosure 'cprint) "Hi world!" + "hello" -> return $ ProcessPair ($(mkClosure 'getPid) ()) print sdictProcessId _ -> error "unknown command" diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index 0d60146c..fc0b8e42 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -1,5 +1,8 @@ -- | Generic main -module Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) where +module Control.Distributed.Process.Backend.Azure.GenericMain + ( genericMain + , ProcessPair(..) + ) where import Prelude hiding (catch) import System.Exit (exitSuccess, exitFailure) @@ -20,7 +23,7 @@ import Control.Distributed.Process.Backend.Azure , cloudServices , CloudService(cloudServiceName, cloudServiceVMs) , VirtualMachine(vmName) - , Backend(copyToVM, checkMD5, runOnVM) + , Backend(copyToVM, checkMD5, callOnVM) ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize @@ -49,16 +52,25 @@ import Control.Distributed.Process , Closure , Process , unClosure + , Static ) import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Closure (SerializableDict) import Network.Transport.TCP (createTransport, defaultTCPParameters) -------------------------------------------------------------------------------- -- Main -- -------------------------------------------------------------------------------- -genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table - -> (String -> IO (Closure (Process ()))) -- ^ Closures to support in 'run' +data ProcessPair b = forall a. Serializable a => ProcessPair { + ppairRemote :: Closure (Process a) + , ppairLocal :: a -> IO b + , ppairDict :: Static (SerializableDict a) + } + +genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table + -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' -> IO () genericMain remoteTable cmds = do _ <- SSH.initialize True @@ -90,13 +102,15 @@ genericMain remoteTable cmds = do then exitSuccess else exitFailure RunOn {} -> do - closure <- cmds (closureId cmd) - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend + procPair <- cmds (closureId cmd) + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend forM_ (findTarget (target cmd) css) $ \vm -> do putStr (vmName vm ++ ": ") >> hFlush stdout - runOnVM backend vm (remotePort cmd) closure + case procPair of + ProcessPair rProc lProc dict -> + callOnVM backend dict vm (remotePort cmd) rProc >>= lProc OnVmCommand (vmCmd@OnVmRun {}) -> do onVmRun (remoteTable initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) SSH.exit From ffa23f1e429fa95997aa24ccd637e168ef96f382 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 2 Aug 2012 13:20:33 +0100 Subject: [PATCH 0150/2357] Bumb version 0.2.3/Serializable Static --- src/Control/Distributed/Process/Backend/Azure.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index a4b9fa01..dbaea918 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -79,7 +79,7 @@ encodeToStdout = liftIO . BSL.putStr . encode encodeToStdoutDict :: SerializableDict a -> a -> Process () encodeToStdoutDict SerializableDict = encodeToStdout -remotable ['encodeToStdoutDict] +remotable ['encodeToStdout] -- | Azure backend data Backend = Backend { From ad6a67eac5ce7815496caa12cd5885420c112ee4 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 3 Aug 2012 08:43:33 +0100 Subject: [PATCH 0151/2357] Run an arbitrary process closure on a VM --- distributed-process-azure.cabal | 5 ++- .../Distributed/Process/Backend/Azure.hs | 28 +++++++++---- .../Process/Backend/Azure/CommandLine.hs | 4 +- .../Process/Backend/Azure/GenericMain.hs | 42 ++++++++++++------- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index a8273f80..d10f3247 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -26,11 +26,12 @@ Library libssh2 >= 0.2 && < 0.3, pureMD5 >= 2.1 && < 2.2, bytestring >= 0.9 && < 0.11, - distributed-process >= 0.2 && < 0.3, + distributed-process >= 0.2.3 && < 0.3, binary >= 0.5 && < 0.6, network-transport-tcp >= 0.2 && < 0.3, optparse-applicative >= 0.2 && < 0.4, - transformers >= 0.3 && < 0.4 + transformers >= 0.3 && < 0.4, + certificate == 1.2.3 Exposed-modules: Control.Distributed.Process.Backend.Azure, Control.Distributed.Process.Backend.Azure.GenericMain Extensions: ViewPatterns, diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index dbaea918..7e0744e4 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -5,6 +5,7 @@ module Control.Distributed.Process.Backend.Azure , AzureParameters(..) , defaultAzureParameters , initializeBackend + , remoteTable -- * Re-exports from Azure Service Management , CloudService(..) , VirtualMachine(..) @@ -16,7 +17,7 @@ import System.Environment.Executable (getExecutablePath) import System.Posix.Types (Fd) import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) -import qualified Data.ByteString.Lazy as BSL (readFile, putStr) +import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr, length, writeFile) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void) @@ -61,15 +62,18 @@ import qualified Network.SSH.Client.LibSSH2.ByteString.Lazy as SSHBS -- CH import Control.Distributed.Process - ( Closure + ( Closure(Closure) , Process , Static + , RemoteTable ) import Control.Distributed.Process.Closure ( remotable - , mkClosure , cpBind , SerializableDict(SerializableDict) + , staticConst + , staticApply + , mkStatic ) import Control.Distributed.Process.Serializable (Serializable) @@ -79,7 +83,17 @@ encodeToStdout = liftIO . BSL.putStr . encode encodeToStdoutDict :: SerializableDict a -> a -> Process () encodeToStdoutDict SerializableDict = encodeToStdout -remotable ['encodeToStdout] +remotable ['encodeToStdoutDict] + +-- | Remote table necessary for the Azure backend +remoteTable :: RemoteTable -> RemoteTable +remoteTable = __remoteTable + +cpEncodeToStdout :: forall a. Typeable a => Static (SerializableDict a) -> Closure (a -> Process ()) +cpEncodeToStdout dict = Closure decoder (encode ()) + where + decoder :: Static (BSL.ByteString -> a -> Process ()) + decoder = staticConst `staticApply` ($(mkStatic 'encodeToStdoutDict) `staticApply` dict) -- | Azure backend data Backend = Backend { @@ -89,7 +103,7 @@ data Backend = Backend { , copyToVM :: VirtualMachine -> IO () -- | Check the MD5 hash of the remote executable , checkMD5 :: VirtualMachine -> IO Bool - -- | @runOnVM vm port p bg@ starts a CH node on port 'port' and runs 'p' + -- | @runOnVM dict vm port p@ starts a CH node on port 'port' and runs 'p' , callOnVM :: forall a. Serializable a => Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a } @@ -158,13 +172,13 @@ apiCallOnVM params dict vm port proc = ++ " 2>&1" (_, r) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe - SSHBS.writeChannel fd ch (encode proc) + cnt <- SSHBS.writeChannel fd ch (encode proc') SSH.channelSendEOF ch SSHBS.readAllChannel fd ch return (decode r) where proc' :: Closure (Process ()) - proc' = proc `cpBind` undefined + proc' = proc `cpBind` cpEncodeToStdout dict -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 8be6f475..aaabcbca 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -9,7 +9,9 @@ import Control.Distributed.Process.Backend.Azure.GenericMain ) getPid :: () -> Process ProcessId -getPid () = getSelfPid +getPid () = do + liftIO $ appendFile "Log" "getPid did run" + getSelfPid remotable ['getPid] diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index fc0b8e42..ec985c9f 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -13,23 +13,19 @@ import System.IO , hSetBinaryMode ) import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (ByteString, getContents) +import qualified Data.ByteString.Lazy as BSL (ByteString, getContents, length) import Control.Monad (unless, forM, forM_, join) -import Control.Exception (throwIO) -import Control.Distributed.Process.Backend.Azure - ( AzureParameters(azureSshUserName) - , defaultAzureParameters - , initializeBackend - , cloudServices - , CloudService(cloudServiceName, cloudServiceVMs) - , VirtualMachine(vmName) - , Backend(copyToVM, checkMD5, callOnVM) - ) +import Control.Exception (throwIO, SomeException) +import Control.Applicative ((<$>), (<*>), (<|>)) +import Control.Monad.IO.Class (liftIO) + +-- SSH import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize , exit ) -import Control.Applicative ((<$>), (<*>), (<|>)) + +-- Command line options import Options.Applicative ( Parser , strOption @@ -47,17 +43,30 @@ import Options.Applicative , header , switch ) + +-- CH import Control.Distributed.Process ( RemoteTable , Closure , Process , unClosure , Static + , catch ) import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Closure (SerializableDict) import Network.Transport.TCP (createTransport, defaultTCPParameters) +import Control.Distributed.Process.Backend.Azure + ( AzureParameters(azureSshUserName) + , defaultAzureParameters + , initializeBackend + , cloudServices + , CloudService(cloudServiceName, cloudServiceVMs) + , VirtualMachine(vmName) + , Backend(copyToVM, checkMD5, callOnVM) + ) +import qualified Control.Distributed.Process.Backend.Azure as Azure (remoteTable) -------------------------------------------------------------------------------- -- Main -- @@ -112,7 +121,9 @@ genericMain remoteTable cmds = do ProcessPair rProc lProc dict -> callOnVM backend dict vm (remotePort cmd) rProc >>= lProc OnVmCommand (vmCmd@OnVmRun {}) -> do - onVmRun (remoteTable initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) + onVmRun (remoteTable . Azure.remoteTable $ initRemoteTable) + (onVmIP vmCmd) + (onVmPort vmCmd) SSH.exit where opts = info (helper <*> commandParser) @@ -144,13 +155,16 @@ azureParameters opts (Just sshOpts) = do onVmRun :: RemoteTable -> String -> String -> IO () onVmRun rtable host port = do hSetBinaryMode stdin True + hSetBinaryMode stdout True proc <- BSL.getContents :: IO BSL.ByteString mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> throwIO err Right transport -> do node <- newLocalNode transport rtable - runProcess node $ join . unClosure . decode $ proc + runProcess node $ + catch (join . unClosure . decode $ proc) + (\e -> liftIO (print (e :: SomeException) >> throwIO e)) -------------------------------------------------------------------------------- -- Command line options -- From aa4517adfb88fb0c4e0921f76b309e9da8d47b22 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 3 Aug 2012 16:12:05 +0100 Subject: [PATCH 0152/2357] Work with the revised version of libssh2(-hs) --- .../Distributed/Process/Backend/Azure.hs | 39 +++++++++---------- .../Process/Backend/Azure/GenericMain.hs | 4 +- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 7e0744e4..e6c31085 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -14,10 +14,10 @@ module Control.Distributed.Process.Backend.Azure import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) -import System.Posix.Types (Fd) import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) -import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr, length, writeFile) +import qualified Data.ByteString.Char8 as BSSC (pack) +import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void) @@ -40,12 +40,13 @@ import qualified Network.Azure.ServiceManagement as Azure import qualified Network.SSH.Client.LibSSH2 as SSH ( withSSH2 , scpSendFile - , withChannel + , withChannelBy , Session + , readAllChannel + , writeAllChannel ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( openChannelSession - , retryIfNeeded , channelExecute , writeChannel , channelSendEOF @@ -55,10 +56,6 @@ import qualified Network.SSH.Client.LibSSH2.Errors as SSH , NULL_POINTER , getLastError ) -import qualified Network.SSH.Client.LibSSH2.ByteString.Lazy as SSHBS - ( writeChannel - , readAllChannel - ) -- CH import Control.Distributed.Process @@ -158,23 +155,23 @@ initializeBackend params = do -- | Start a CH node on the given virtual machine apiCopyToVM :: AzureParameters -> VirtualMachine -> IO () apiCopyToVM params vm = - void . withSSH2 params vm $ \fd s -> catchSshError s $ - SSH.scpSendFile fd s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) + void . withSSH2 params vm $ \s -> catchSshError s $ + SSH.scpSendFile s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) -- | Call a process on a VM apiCallOnVM :: Serializable a => AzureParameters -> Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a apiCallOnVM params dict vm port proc = - withSSH2 params vm $ \fd s -> do + withSSH2 params vm $ \s -> do let exe = "PATH=. " ++ azureSshRemotePath params ++ " onvm run " ++ " --host " ++ vmIpAddress vm ++ " --port " ++ port ++ " 2>&1" - (_, r) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do - SSH.retryIfNeeded fd s $ SSH.channelExecute ch exe - cnt <- SSHBS.writeChannel fd ch (encode proc') + (_, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch exe + _cnt <- SSH.writeAllChannel ch (encode proc') SSH.channelSendEOF ch - SSHBS.readAllChannel fd ch + SSH.readAllChannel ch return (decode r) where proc' :: Closure (Process ()) @@ -184,15 +181,15 @@ apiCallOnVM params dict vm port proc = apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool apiCheckMD5 params vm = do hash <- localHash params - withSSH2 params vm $ \fd s -> do - (r, _) <- SSH.withChannel (SSH.openChannelSession s) id fd s $ \ch -> do - SSH.retryIfNeeded fd s $ SSH.channelExecute ch "md5sum -c --status" - SSH.writeChannel ch $ show hash ++ " " ++ azureSshRemotePath params + withSSH2 params vm $ \s -> do + (r, _) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch "md5sum -c --status" + SSH.writeChannel ch . BSSC.pack $ show hash ++ " " ++ azureSshRemotePath params SSH.channelSendEOF ch - SSHBS.readAllChannel fd ch + SSH.readAllChannel ch return (r == 0) -withSSH2 :: AzureParameters -> VirtualMachine -> (Fd -> SSH.Session -> IO a) -> IO a +withSSH2 :: AzureParameters -> VirtualMachine -> (SSH.Session -> IO a) -> IO a withSSH2 params (Azure.vmSshEndpoint -> Just ep) = SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index ec985c9f..b3ced696 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -13,7 +13,7 @@ import System.IO , hSetBinaryMode ) import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (ByteString, getContents, length) +import qualified Data.ByteString.Lazy as BSL (ByteString, getContents) import Control.Monad (unless, forM, forM_, join) import Control.Exception (throwIO, SomeException) import Control.Applicative ((<$>), (<*>), (<|>)) @@ -120,7 +120,7 @@ genericMain remoteTable cmds = do case procPair of ProcessPair rProc lProc dict -> callOnVM backend dict vm (remotePort cmd) rProc >>= lProc - OnVmCommand (vmCmd@OnVmRun {}) -> do + OnVmCommand (vmCmd@OnVmRun {}) -> onVmRun (remoteTable . Azure.remoteTable $ initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) From 4031c9a870ddc392266d8b5363f7d4022ece1683 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 6 Aug 2012 11:56:47 +0100 Subject: [PATCH 0153/2357] Support spawn (as well as call) --- distributed-process-azure.cabal | 3 +- .../Distributed/Process/Backend/Azure.hs | 50 +++++++---- .../Process/Backend/Azure/CommandLine.hs | 28 ++++-- .../Process/Backend/Azure/GenericMain.hs | 89 +++++++++++++------ 4 files changed, 121 insertions(+), 49 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index d10f3247..6bf3574b 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -31,7 +31,8 @@ Library network-transport-tcp >= 0.2 && < 0.3, optparse-applicative >= 0.2 && < 0.4, transformers >= 0.3 && < 0.4, - certificate == 1.2.3 + certificate == 1.2.3, + unix >= 2.5 && < 2.6 Exposed-modules: Control.Distributed.Process.Backend.Azure, Control.Distributed.Process.Backend.Azure.GenericMain Extensions: ViewPatterns, diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index e6c31085..3c3a568e 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -17,7 +17,7 @@ import System.Environment.Executable (getExecutablePath) import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString.Char8 as BSSC (pack) -import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr) +import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr, writeFile) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void) @@ -102,6 +102,9 @@ data Backend = Backend { , checkMD5 :: VirtualMachine -> IO Bool -- | @runOnVM dict vm port p@ starts a CH node on port 'port' and runs 'p' , callOnVM :: forall a. Serializable a => Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a + -- | Create a new CH node and run the specified process in the background. + -- The CH node will exit when the process exists. + , spawnOnVM :: VirtualMachine -> String -> Closure (Process ()) -> IO () } data AzureParameters = AzureParameters { @@ -150,6 +153,7 @@ initializeBackend params = do , copyToVM = apiCopyToVM params , checkMD5 = apiCheckMD5 params , callOnVM = apiCallOnVM params + , spawnOnVM = apiSpawnOnVM params } -- | Start a CH node on the given virtual machine @@ -161,21 +165,35 @@ apiCopyToVM params vm = -- | Call a process on a VM apiCallOnVM :: Serializable a => AzureParameters -> Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a apiCallOnVM params dict vm port proc = - withSSH2 params vm $ \s -> do - let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm run " - ++ " --host " ++ vmIpAddress vm - ++ " --port " ++ port - ++ " 2>&1" - (_, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do - SSH.channelExecute ch exe - _cnt <- SSH.writeAllChannel ch (encode proc') - SSH.channelSendEOF ch - SSH.readAllChannel ch - return (decode r) - where - proc' :: Closure (Process ()) - proc' = proc `cpBind` cpEncodeToStdout dict + withSSH2 params vm $ \s -> do + let exe = "PATH=. " ++ azureSshRemotePath params + ++ " onvm run " + ++ " --host " ++ vmIpAddress vm + ++ " --port " ++ port + ++ " 2>&1" + (_, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch exe + _cnt <- SSH.writeAllChannel ch (encode $ proc `cpBind` cpEncodeToStdout dict) + SSH.channelSendEOF ch + SSH.readAllChannel ch + return (decode r) + +apiSpawnOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Process ()) -> IO () +apiSpawnOnVM params vm port proc = + withSSH2 params vm $ \s -> do + let exe = "PATH=. " ++ azureSshRemotePath params + ++ " onvm run " + ++ " --host " ++ vmIpAddress vm + ++ " --port " ++ port + ++ " --background " + ++ " 2>&1" + BSL.writeFile "closure" (encode proc) + r <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch exe + _cnt <- SSH.writeAllChannel ch (encode proc) + SSH.channelSendEOF ch + SSH.readAllChannel ch + print r -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index aaabcbca..7a12bf35 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,7 +1,8 @@ {-# LANGUAGE TemplateHaskell #-} import Control.Monad.IO.Class (liftIO) -import Control.Distributed.Process (Process, ProcessId, getSelfPid) +import Control.Concurrent (threadDelay) +import Control.Distributed.Process (Process, ProcessId, getSelfPid, Closure) import Control.Distributed.Process.Closure (remotable, mkClosure, sdictProcessId) import Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain @@ -13,9 +14,24 @@ getPid () = do liftIO $ appendFile "Log" "getPid did run" getSelfPid -remotable ['getPid] +logN :: Int -> Process () +logN 0 = + liftIO $ appendFile "Log" "logN done\n" +logN n = do + liftIO $ do + appendFile "Log" $ "logN " ++ show n ++ "\n" + threadDelay 1000000 + logN (n - 1) -main = genericMain __remoteTable $ \cmd -> - case cmd of - "hello" -> return $ ProcessPair ($(mkClosure 'getPid) ()) print sdictProcessId - _ -> error "unknown command" +remotable ['getPid, 'logN] + +main :: IO () +main = genericMain __remoteTable callable spawnable + where + callable :: String -> IO (ProcessPair ()) + callable "getPid" = return $ ProcessPair ($(mkClosure 'getPid) ()) print sdictProcessId + callable _ = error "spawnable: unknown" + + spawnable :: String -> IO (Closure (Process ())) + spawnable "logN" = return $ $(mkClosure 'logN) (10 :: Int) + spawnable _ = error "callable: unknown" diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index b3ced696..85b09075 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -10,15 +10,20 @@ import System.IO ( hFlush , stdout , stdin + , stderr , hSetBinaryMode + , hClose ) import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (ByteString, getContents) -import Control.Monad (unless, forM, forM_, join) -import Control.Exception (throwIO, SomeException) +import qualified Data.ByteString.Lazy as BSL (getContents, length) +import Control.Monad (unless, forM, forM_, join, void) +import Control.Exception (throwIO, SomeException, evaluate) import Control.Applicative ((<$>), (<*>), (<|>)) import Control.Monad.IO.Class (liftIO) +-- Posix +import System.Posix.Process (forkProcess, createSession) + -- SSH import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( initialize @@ -64,7 +69,7 @@ import Control.Distributed.Process.Backend.Azure , cloudServices , CloudService(cloudServiceName, cloudServiceVMs) , VirtualMachine(vmName) - , Backend(copyToVM, checkMD5, callOnVM) + , Backend(copyToVM, checkMD5, callOnVM, spawnOnVM) ) import qualified Control.Distributed.Process.Backend.Azure as Azure (remoteTable) @@ -78,10 +83,11 @@ data ProcessPair b = forall a. Serializable a => ProcessPair { , ppairDict :: Static (SerializableDict a) } -genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table - -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' +genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table + -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' + -> (String -> IO (Closure (Process ()))) -- ^ Closures to support in @run --background@ -> IO () -genericMain remoteTable cmds = do +genericMain remoteTable callable spawnable = do _ <- SSH.initialize True cmd <- execParser opts case cmd of @@ -110,20 +116,31 @@ genericMain remoteTable cmds = do if and matches then exitSuccess else exitFailure - RunOn {} -> do - procPair <- cmds (closureId cmd) + RunOn {} | background cmd -> do + rProc <- spawnable (closureId cmd) + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params + css <- cloudServices backend + forM_ (findTarget (target cmd) css) $ \vm -> do + putStr (vmName vm ++ ": ") >> hFlush stdout + spawnOnVM backend vm (remotePort cmd) rProc + putStrLn "OK" + RunOn {} {- not (background cmd) -} -> do + procPair <- callable (closureId cmd) params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) backend <- initializeBackend params css <- cloudServices backend forM_ (findTarget (target cmd) css) $ \vm -> do putStr (vmName vm ++ ": ") >> hFlush stdout case procPair of - ProcessPair rProc lProc dict -> - callOnVM backend dict vm (remotePort cmd) rProc >>= lProc + ProcessPair rProc lProc dict -> do + result <- callOnVM backend dict vm (remotePort cmd) rProc + lProc result OnVmCommand (vmCmd@OnVmRun {}) -> onVmRun (remoteTable . Azure.remoteTable $ initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) + (onVmBackground vmCmd) SSH.exit where opts = info (helper <*> commandParser) @@ -152,19 +169,35 @@ azureParameters opts (Just sshOpts) = do azureSshUserName = remoteUser sshOpts } -onVmRun :: RemoteTable -> String -> String -> IO () -onVmRun rtable host port = do - hSetBinaryMode stdin True - hSetBinaryMode stdout True - proc <- BSL.getContents :: IO BSL.ByteString - mTransport <- createTransport host port defaultTCPParameters - case mTransport of - Left err -> throwIO err - Right transport -> do - node <- newLocalNode transport rtable - runProcess node $ - catch (join . unClosure . decode $ proc) - (\e -> liftIO (print (e :: SomeException) >> throwIO e)) +onVmRun :: RemoteTable -> String -> String -> Bool -> IO () +onVmRun rtable host port bg = do + hSetBinaryMode stdin True + hSetBinaryMode stdout True + procEnc <- BSL.getContents + -- Force evaluation (so that we can safely close stdin) + _length <- evaluate (BSL.length procEnc) + let proc = decode procEnc + if bg + then do + hClose stdin + hClose stdout + hClose stderr + void . forkProcess $ do + void createSession + startCH proc + else + startCH proc + where + startCH :: Closure (Process ()) -> IO () + startCH proc = do + mTransport <- createTransport host port defaultTCPParameters + case mTransport of + Left err -> throwIO err + Right transport -> do + node <- newLocalNode transport rtable + runProcess node $ + catch (join . unClosure $ proc) + (\e -> liftIO (print (e :: SomeException) >> throwIO e)) -------------------------------------------------------------------------------- -- Command line options -- @@ -217,8 +250,9 @@ data Command = data OnVmCommand = OnVmRun { - onVmIP :: String - , onVmPort :: String + onVmIP :: String + , onVmPort :: String + , onVmBackground :: Bool } deriving Show @@ -317,6 +351,9 @@ onVmRunParser = OnVmRun & metavar "PORT" & help "port number" ) + <*> switch ( long "background" + & help "Run the process in the background" + ) onVmCommandParser :: Parser Command onVmCommandParser = OnVmCommand <$> subparser From 252a7a8b65c75e5c56093d72eed0f50510bdb860 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 6 Aug 2012 12:33:45 +0100 Subject: [PATCH 0154/2357] Deal with exit status/Simple ping server+client --- distributed-process-azure.cabal | 4 +- .../Distributed/Process/Backend/Azure.hs | 13 +++- .../Process/Backend/Azure/CommandLine.hs | 75 ++++++++++++++----- 3 files changed, 68 insertions(+), 24 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 6bf3574b..308b1cb2 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -47,4 +47,6 @@ Executable cloud-haskell-azure Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, distributed-process >= 0.2 && < 0.3, - transformers >= 0.3 && < 0.4 + transformers >= 0.3 && < 0.4, + bytestring >= 0.9 && < 0.11, + binary >= 0.5 && < 0.6 diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 3c3a568e..b2792437 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -18,6 +18,7 @@ import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString.Char8 as BSSC (pack) import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr, writeFile) +import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void) @@ -171,12 +172,14 @@ apiCallOnVM params dict vm port proc = ++ " --host " ++ vmIpAddress vm ++ " --port " ++ port ++ " 2>&1" - (_, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe _cnt <- SSH.writeAllChannel ch (encode $ proc `cpBind` cpEncodeToStdout dict) SSH.channelSendEOF ch SSH.readAllChannel ch - return (decode r) + if status == 0 + then return $ decode r + else error (BSLC.unpack r) apiSpawnOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Process ()) -> IO () apiSpawnOnVM params vm port proc = @@ -188,12 +191,14 @@ apiSpawnOnVM params vm port proc = ++ " --background " ++ " 2>&1" BSL.writeFile "closure" (encode proc) - r <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe _cnt <- SSH.writeAllChannel ch (encode proc) SSH.channelSendEOF ch SSH.readAllChannel ch - print r + if status == 0 + then return () + else error (BSLC.unpack r) -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs index 7a12bf35..589fbb45 100644 --- a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs +++ b/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs @@ -1,37 +1,74 @@ {-# LANGUAGE TemplateHaskell #-} +import Data.Binary (encode, decode) +import Control.Applicative ((<$>)) +import Control.Monad (forever) import Control.Monad.IO.Class (liftIO) import Control.Concurrent (threadDelay) -import Control.Distributed.Process (Process, ProcessId, getSelfPid, Closure) -import Control.Distributed.Process.Closure (remotable, mkClosure, sdictProcessId) +import Control.Exception (try, IOException) +import Control.Distributed.Process + ( Process + , ProcessId + , getSelfPid + , Closure + , expect + , send + , monitor + , receiveWait + , match + , ProcessMonitorNotification(..) + ) +import Control.Distributed.Process.Closure + ( remotable + , mkClosure + , SerializableDict(..) + , mkStatic + ) import Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain , ProcessPair(..) ) +import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) + +sdictString :: SerializableDict String +sdictString = SerializableDict -getPid :: () -> Process ProcessId -getPid () = do - liftIO $ appendFile "Log" "getPid did run" - getSelfPid +ping :: () -> Process String +ping () = do + mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") + case mPingServerEnc of + Left err -> + return $ "Ping server not found: " ++ show (err :: IOException) + Right pingServerEnc -> do + let pingServer = decode pingServerEnc + pid <- getSelfPid + monitor pingServer + send pingServer pid + gotReply <- receiveWait + [ match (\() -> return True) + , match (\(ProcessMonitorNotification _ _ _) -> return False) + ] + if gotReply + then return $ "Ping server at " ++ show pingServer ++ " ok" + else return $ "Ping server at " ++ show pingServer ++ " failure" -logN :: Int -> Process () -logN 0 = - liftIO $ appendFile "Log" "logN done\n" -logN n = do - liftIO $ do - appendFile "Log" $ "logN " ++ show n ++ "\n" - threadDelay 1000000 - logN (n - 1) +pingServer :: () -> Process () +pingServer () = do + pid <- getSelfPid + liftIO $ BSL.writeFile "pingServer.pid" (encode pid) + forever $ do + pid <- expect + send pid () -remotable ['getPid, 'logN] +remotable ['ping, 'pingServer, 'sdictString] main :: IO () main = genericMain __remoteTable callable spawnable where callable :: String -> IO (ProcessPair ()) - callable "getPid" = return $ ProcessPair ($(mkClosure 'getPid) ()) print sdictProcessId - callable _ = error "spawnable: unknown" + callable "ping" = return $ ProcessPair ($(mkClosure 'ping) ()) putStrLn $(mkStatic 'sdictString) + callable _ = error "spawnable: unknown" spawnable :: String -> IO (Closure (Process ())) - spawnable "logN" = return $ $(mkClosure 'logN) (10 :: Int) - spawnable _ = error "callable: unknown" + spawnable "pingServer" = return $ $(mkClosure 'pingServer) () + spawnable _ = error "callable: unknown" From a710837c98fcd9f8493bcb2b87e2ca3db66beb39 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 6 Aug 2012 13:21:58 +0100 Subject: [PATCH 0155/2357] Skeleton demo --- demos/Conway.hs | 22 ++++++++++++++++ .../Azure/CommandLine.hs => demos/Ping.hs | 0 distributed-process-azure.cabal | 25 ++++++++++++++++--- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 demos/Conway.hs rename src/Control/Distributed/Process/Backend/Azure/CommandLine.hs => demos/Ping.hs (100%) diff --git a/demos/Conway.hs b/demos/Conway.hs new file mode 100644 index 00000000..b5b41655 --- /dev/null +++ b/demos/Conway.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE TemplateHaskell #-} +import Control.Distributed.Process + ( Process + , Closure + ) +import Control.Distributed.Process.Closure + ( remotable ) +import Control.Distributed.Process.Backend.Azure.GenericMain + ( genericMain + , ProcessPair(..) + ) + +remotable [] + +main :: IO () +main = genericMain __remoteTable callable spawnable + where + callable :: String -> IO (ProcessPair ()) + callable _ = error "spawnable: unknown" + + spawnable :: String -> IO (Closure (Process ())) + spawnable _ = error "callable: unknown" diff --git a/src/Control/Distributed/Process/Backend/Azure/CommandLine.hs b/demos/Ping.hs similarity index 100% rename from src/Control/Distributed/Process/Backend/Azure/CommandLine.hs rename to demos/Ping.hs diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 308b1cb2..e2a9b2ef 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -18,6 +18,10 @@ Source-Repository head Location: https://github.com/haskell-distributed/distributed-process SubDir: distributed-process-azure +Flag build-demos + description: Build the demos + default: False + Library Build-Depends: base >= 4.4 && < 5, azure-service-api >= 0.1 && < 0.2, @@ -42,11 +46,26 @@ Library ghc-options: -Wall HS-Source-Dirs: src -Executable cloud-haskell-azure - Main-Is: src/Control/Distributed/Process/Backend/Azure/CommandLine.hs - Build-Depends: base >= 4.4 && < 5, +Executable cloud-haskell-azure-ping + if flag(build-demos) + Build-Depends: base >= 4.4 && < 5, + distributed-process-azure >= 0.1 && < 0.2, + distributed-process >= 0.2 && < 0.3, + transformers >= 0.3 && < 0.4, + bytestring >= 0.9 && < 0.11, + binary >= 0.5 && < 0.6 + else + buildable: False + Main-Is: demos/Ping.hs + +Executable cloud-haskell-azure-conway + if flag(build-demos) + Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, distributed-process >= 0.2 && < 0.3, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6 + else + buildable: False + Main-Is: demos/Conway.hs From f239622d555709739a1b06aac48cf96f53a9a509 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 6 Aug 2012 17:18:34 +0100 Subject: [PATCH 0156/2357] Make backend specific to one cloud service --- demos/Conway.hs | 30 ++++++- distributed-process-azure.cabal | 3 +- .../Distributed/Process/Backend/Azure.hs | 60 +++++++------ .../Process/Backend/Azure/GenericMain.hs | 84 +++++++++---------- 4 files changed, 100 insertions(+), 77 deletions(-) diff --git a/demos/Conway.hs b/demos/Conway.hs index b5b41655..65b385a4 100644 --- a/demos/Conway.hs +++ b/demos/Conway.hs @@ -1,16 +1,39 @@ -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-} +import Data.Data (Typeable, Data) +import Data.Binary (Binary(get, put)) +import Data.Binary.Generic (getGeneric, putGeneric) import Control.Distributed.Process ( Process , Closure + , expect ) import Control.Distributed.Process.Closure - ( remotable ) + ( remotable + , mkClosure + ) import Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain , ProcessPair(..) ) -remotable [] +data ControllerMsg = + ControllerExit + deriving (Typeable, Data) + +instance Binary ControllerMsg where + get = getGeneric + put = putGeneric + +conwayController :: () -> Process () +conwayController () = go + where + go = do + msg <- expect + case msg of + ControllerExit -> + return () + +remotable ['conwayController] main :: IO () main = genericMain __remoteTable callable spawnable @@ -19,4 +42,5 @@ main = genericMain __remoteTable callable spawnable callable _ = error "spawnable: unknown" spawnable :: String -> IO (Closure (Process ())) + spawnable "controller" = return $ $(mkClosure 'conwayController) () spawnable _ = error "callable: unknown" diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index e2a9b2ef..fe883410 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -65,7 +65,8 @@ Executable cloud-haskell-azure-conway distributed-process >= 0.2 && < 0.3, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, - binary >= 0.5 && < 0.6 + binary >= 0.5 && < 0.6, + binary-generic >= 0.2 && < 0.3 else buildable: False Main-Is: demos/Conway.hs diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index b2792437..3ac549d8 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -9,6 +9,7 @@ module Control.Distributed.Process.Backend.Azure -- * Re-exports from Azure Service Management , CloudService(..) , VirtualMachine(..) + , Azure.cloudServices ) where import System.Environment (getEnv) @@ -30,6 +31,7 @@ import Network.Azure.ServiceManagement ( CloudService(..) , VirtualMachine(..) , Endpoint(..) + , AzureSetup ) import qualified Network.Azure.ServiceManagement as Azure ( cloudServices @@ -96,7 +98,7 @@ cpEncodeToStdout dict = Closure decoder (encode ()) -- | Azure backend data Backend = Backend { -- | Find virtual machines - cloudServices :: IO [CloudService] + findVMs :: IO [VirtualMachine] -- | Copy the executable to a virtual machine , copyToVM :: VirtualMachine -> IO () -- | Check the MD5 hash of the remote executable @@ -109,9 +111,7 @@ data Backend = Backend { } data AzureParameters = AzureParameters { - azureSubscriptionId :: String - , azureAuthCertificate :: FilePath - , azureAuthPrivateKey :: FilePath + azureSetup :: AzureSetup , azureSshUserName :: FilePath , azureSshPublicKey :: FilePath , azureSshPrivateKey :: FilePath @@ -127,36 +127,42 @@ defaultAzureParameters :: String -- ^ Azure subscription ID -> FilePath -- ^ Path to private key -> IO AzureParameters defaultAzureParameters sid x509 pkey = do - home <- getEnv "HOME" - user <- getEnv "USER" - self <- getExecutablePath + home <- getEnv "HOME" + user <- getEnv "USER" + self <- getExecutablePath + setup <- Azure.azureSetup sid x509 pkey return AzureParameters - { azureSubscriptionId = sid - , azureAuthCertificate = x509 - , azureAuthPrivateKey = pkey - , azureSshUserName = user - , azureSshPublicKey = home ".ssh" "id_rsa.pub" - , azureSshPrivateKey = home ".ssh" "id_rsa" - , azureSshPassphrase = "" - , azureSshKnownHosts = home ".ssh" "known_hosts" - , azureSshRemotePath = takeFileName self - , azureSshLocalPath = self + { azureSetup = setup + , azureSshUserName = user + , azureSshPublicKey = home ".ssh" "id_rsa.pub" + , azureSshPrivateKey = home ".ssh" "id_rsa" + , azureSshPassphrase = "" + , azureSshKnownHosts = home ".ssh" "known_hosts" + , azureSshRemotePath = takeFileName self + , azureSshLocalPath = self } -- | Initialize the backend -initializeBackend :: AzureParameters -> IO Backend -initializeBackend params = do - setup <- Azure.azureSetup (azureSubscriptionId params) - (azureAuthCertificate params) - (azureAuthPrivateKey params) +initializeBackend :: AzureParameters -- ^ Connection parameters + -> String -- ^ Cloud service name + -> IO Backend +initializeBackend params cloudService = do return Backend { - cloudServices = Azure.cloudServices setup - , copyToVM = apiCopyToVM params - , checkMD5 = apiCheckMD5 params - , callOnVM = apiCallOnVM params - , spawnOnVM = apiSpawnOnVM params + findVMs = apiFindVMs params cloudService + , copyToVM = apiCopyToVM params + , checkMD5 = apiCheckMD5 params + , callOnVM = apiCallOnVM params + , spawnOnVM = apiSpawnOnVM params } +-- | Find virtual machines +apiFindVMs :: AzureParameters -> String -> IO [VirtualMachine] +apiFindVMs params cloudService = do + css <- Azure.cloudServices (azureSetup params) + case filter ((== cloudService) . cloudServiceName) css of + [cs] -> return $ cloudServiceVMs cs + _ -> return [] + -- | Start a CH node on the given virtual machine apiCopyToVM :: AzureParameters -> VirtualMachine -> IO () apiCopyToVM params vm = diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index 85b09075..db95fcfb 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -18,7 +18,7 @@ import Data.Binary (decode) import qualified Data.ByteString.Lazy as BSL (getContents, length) import Control.Monad (unless, forM, forM_, join, void) import Control.Exception (throwIO, SomeException, evaluate) -import Control.Applicative ((<$>), (<*>), (<|>)) +import Control.Applicative ((<$>), (<*>), optional) import Control.Monad.IO.Class (liftIO) -- Posix @@ -63,13 +63,12 @@ import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Closure (SerializableDict) import Network.Transport.TCP (createTransport, defaultTCPParameters) import Control.Distributed.Process.Backend.Azure - ( AzureParameters(azureSshUserName) + ( AzureParameters(azureSshUserName, azureSetup) , defaultAzureParameters , initializeBackend , cloudServices - , CloudService(cloudServiceName, cloudServiceVMs) , VirtualMachine(vmName) - , Backend(copyToVM, checkMD5, callOnVM, spawnOnVM) + , Backend(findVMs, copyToVM, checkMD5, callOnVM, spawnOnVM) ) import qualified Control.Distributed.Process.Backend.Azure as Azure (remoteTable) @@ -93,22 +92,21 @@ genericMain remoteTable callable spawnable = do case cmd of List {} -> do params <- azureParameters (azureOptions cmd) Nothing - backend <- initializeBackend params - css <- cloudServices backend + css <- cloudServices (azureSetup params) mapM_ print css CopyTo {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params (targetService (target cmd)) + vms <- findMatchingVMs backend (targetVM (target cmd)) + forM_ vms $ \vm -> do putStr (vmName vm ++ ": ") >> hFlush stdout copyToVM backend vm putStrLn "Done" CheckMD5 {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - matches <- forM (findTarget (target cmd) css) $ \vm -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params (targetService (target cmd)) + vms <- findMatchingVMs backend (targetVM (target cmd)) + matches <- forM vms $ \vm -> do unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout match <- checkMD5 backend vm unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" @@ -117,20 +115,20 @@ genericMain remoteTable callable spawnable = do then exitSuccess else exitFailure RunOn {} | background cmd -> do - rProc <- spawnable (closureId cmd) - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params (targetService (target cmd)) + vms <- findMatchingVMs backend (targetVM (target cmd)) + rProc <- spawnable (closureId cmd) + forM_ vms $ \vm -> do putStr (vmName vm ++ ": ") >> hFlush stdout spawnOnVM backend vm (remotePort cmd) rProc putStrLn "OK" RunOn {} {- not (background cmd) -} -> do + params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) + backend <- initializeBackend params (targetService (target cmd)) + vms <- findMatchingVMs backend (targetVM (target cmd)) procPair <- callable (closureId cmd) - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params - css <- cloudServices backend - forM_ (findTarget (target cmd) css) $ \vm -> do + forM_ vms $ \vm -> do putStr (vmName vm ++ ": ") >> hFlush stdout case procPair of ProcessPair rProc lProc dict -> do @@ -148,13 +146,9 @@ genericMain remoteTable callable spawnable = do & header "Cloud Haskell backend for Azure" ) -findTarget :: Target -> [CloudService] -> [VirtualMachine] -findTarget (CloudService cs) css = - concatMap cloudServiceVMs . filter ((== cs) . cloudServiceName) $ css -findTarget (VirtualMachine virtualMachine) css = - [ vm | vm <- concatMap cloudServiceVMs css - , vmName vm == virtualMachine - ] +findMatchingVMs :: Backend -> Maybe String -> IO [VirtualMachine] +findMatchingVMs backend Nothing = findVMs backend +findMatchingVMs backend (Just vm) = filter ((== vm) . vmName) `fmap` findVMs backend azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters azureParameters opts Nothing = @@ -215,9 +209,10 @@ data SshOptions = SshOptions { } deriving Show -data Target = - VirtualMachine String - | CloudService String +data Target = Target { + targetService :: String + , targetVM :: Maybe String + } deriving Show data Command = @@ -244,7 +239,7 @@ data Command = , background :: Bool } | OnVmCommand { - onVmCommand :: OnVmCommand + _onVmCommand :: OnVmCommand } deriving Show @@ -288,18 +283,15 @@ copyToParser = CopyTo <*> targetParser targetParser :: Parser Target -targetParser = - ( VirtualMachine <$> strOption ( long "virtual-machine" - & metavar "VM" - & help "Virtual machine name" - ) - ) - <|> - ( CloudService <$> strOption ( long "cloud-service" - & metavar "CS" - & help "Cloud service name" - ) - ) +targetParser = Target + <$> strOption ( long "cloud-service" + & metavar "CS" + & help "Cloud service name" + ) + <*> optional (strOption ( long "virtual-machine" + & metavar "VM" + & help "Virtual machine name" + )) checkMD5Parser :: Parser Command checkMD5Parser = CheckMD5 From 11c5cc0b73f8c56bef213a12ff019035baa30de8 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 6 Aug 2012 17:49:10 +0100 Subject: [PATCH 0157/2357] Pass (as yet uninit) backend to remote processes --- demos/Conway.hs | 8 ++- demos/Ping.hs | 12 ++-- distributed-process-azure.cabal | 3 +- .../Distributed/Process/Backend/Azure.hs | 64 ++++++++++++------- .../Process/Backend/Azure/GenericMain.hs | 31 +++++---- 5 files changed, 75 insertions(+), 43 deletions(-) diff --git a/demos/Conway.hs b/demos/Conway.hs index 65b385a4..1ab4bbdc 100644 --- a/demos/Conway.hs +++ b/demos/Conway.hs @@ -11,9 +11,11 @@ import Control.Distributed.Process.Closure ( remotable , mkClosure ) +import Control.Distributed.Process.Backend.Azure (Backend) import Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain , ProcessPair(..) + , RemoteProcess ) data ControllerMsg = @@ -24,8 +26,8 @@ instance Binary ControllerMsg where get = getGeneric put = putGeneric -conwayController :: () -> Process () -conwayController () = go +conwayController :: () -> Backend -> Process () +conwayController () _backend = go where go = do msg <- expect @@ -41,6 +43,6 @@ main = genericMain __remoteTable callable spawnable callable :: String -> IO (ProcessPair ()) callable _ = error "spawnable: unknown" - spawnable :: String -> IO (Closure (Process ())) + spawnable :: String -> IO (RemoteProcess ()) spawnable "controller" = return $ $(mkClosure 'conwayController) () spawnable _ = error "callable: unknown" diff --git a/demos/Ping.hs b/demos/Ping.hs index 589fbb45..95b5379b 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -24,17 +24,19 @@ import Control.Distributed.Process.Closure , SerializableDict(..) , mkStatic ) +import Control.Distributed.Process.Backend.Azure (Backend) import Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain , ProcessPair(..) + , RemoteProcess ) import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) sdictString :: SerializableDict String sdictString = SerializableDict -ping :: () -> Process String -ping () = do +ping :: () -> Backend -> Process String +ping () _backend = do mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") case mPingServerEnc of Left err -> @@ -52,8 +54,8 @@ ping () = do then return $ "Ping server at " ++ show pingServer ++ " ok" else return $ "Ping server at " ++ show pingServer ++ " failure" -pingServer :: () -> Process () -pingServer () = do +pingServer :: () -> Backend -> Process () +pingServer () _backend = do pid <- getSelfPid liftIO $ BSL.writeFile "pingServer.pid" (encode pid) forever $ do @@ -69,6 +71,6 @@ main = genericMain __remoteTable callable spawnable callable "ping" = return $ ProcessPair ($(mkClosure 'ping) ()) putStrLn $(mkStatic 'sdictString) callable _ = error "spawnable: unknown" - spawnable :: String -> IO (Closure (Process ())) + spawnable :: String -> IO (RemoteProcess ()) spawnable "pingServer" = return $ $(mkClosure 'pingServer) () spawnable _ = error "callable: unknown" diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index fe883410..a48d4215 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -42,7 +42,8 @@ Library Extensions: ViewPatterns, RankNTypes, ExistentialQuantification, - ScopedTypeVariables + ScopedTypeVariables, + DeriveDataTypeable ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 3ac549d8..126475aa 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -18,7 +18,7 @@ import System.Environment.Executable (getExecutablePath) import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString.Char8 as BSSC (pack) -import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr, writeFile) +import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr) import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) @@ -69,7 +69,7 @@ import Control.Distributed.Process ) import Control.Distributed.Process.Closure ( remotable - , cpBind + , cpComp , SerializableDict(SerializableDict) , staticConst , staticApply @@ -104,11 +104,19 @@ data Backend = Backend { -- | Check the MD5 hash of the remote executable , checkMD5 :: VirtualMachine -> IO Bool -- | @runOnVM dict vm port p@ starts a CH node on port 'port' and runs 'p' - , callOnVM :: forall a. Serializable a => Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a + , callOnVM :: forall a. Serializable a + => Static (SerializableDict a) + -> VirtualMachine + -> String + -> Closure (Backend -> Process a) + -> IO a -- | Create a new CH node and run the specified process in the background. -- The CH node will exit when the process exists. - , spawnOnVM :: VirtualMachine -> String -> Closure (Process ()) -> IO () - } + , spawnOnVM :: VirtualMachine + -> String + -> Closure (Backend -> Process ()) + -> IO () + } deriving (Typeable) data AzureParameters = AzureParameters { azureSetup :: AzureSetup @@ -170,24 +178,37 @@ apiCopyToVM params vm = SSH.scpSendFile s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) -- | Call a process on a VM -apiCallOnVM :: Serializable a => AzureParameters -> Static (SerializableDict a) -> VirtualMachine -> String -> Closure (Process a) -> IO a +apiCallOnVM :: Serializable a + => AzureParameters + -> Static (SerializableDict a) + -> VirtualMachine + -> String + -> Closure (Backend -> Process a) + -> IO a apiCallOnVM params dict vm port proc = - withSSH2 params vm $ \s -> do - let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm run " - ++ " --host " ++ vmIpAddress vm - ++ " --port " ++ port - ++ " 2>&1" - (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do - SSH.channelExecute ch exe - _cnt <- SSH.writeAllChannel ch (encode $ proc `cpBind` cpEncodeToStdout dict) - SSH.channelSendEOF ch - SSH.readAllChannel ch - if status == 0 - then return $ decode r - else error (BSLC.unpack r) + withSSH2 params vm $ \s -> do + let exe = "PATH=. " ++ azureSshRemotePath params + ++ " onvm run " + ++ " --host " ++ vmIpAddress vm + ++ " --port " ++ port + ++ " 2>&1" + (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch exe + _cnt <- SSH.writeAllChannel ch (encode proc') + SSH.channelSendEOF ch + SSH.readAllChannel ch + if status == 0 + then return $ decode r + else error (BSLC.unpack r) + where + proc' :: Closure (Backend -> Process ()) + proc' = proc `cpComp` cpEncodeToStdout dict -apiSpawnOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Process ()) -> IO () +apiSpawnOnVM :: AzureParameters + -> VirtualMachine + -> String + -> Closure (Backend -> Process ()) + -> IO () apiSpawnOnVM params vm port proc = withSSH2 params vm $ \s -> do let exe = "PATH=. " ++ azureSshRemotePath params @@ -196,7 +217,6 @@ apiSpawnOnVM params vm port proc = ++ " --port " ++ port ++ " --background " ++ " 2>&1" - BSL.writeFile "closure" (encode proc) (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe _cnt <- SSH.writeAllChannel ch (encode proc) diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index db95fcfb..264ef169 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -2,6 +2,8 @@ module Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain , ProcessPair(..) + , RemoteProcess + , LocalProcess ) where import Prelude hiding (catch) @@ -16,7 +18,7 @@ import System.IO ) import Data.Binary (decode) import qualified Data.ByteString.Lazy as BSL (getContents, length) -import Control.Monad (unless, forM, forM_, join, void) +import Control.Monad (unless, forM, forM_, void) import Control.Exception (throwIO, SomeException, evaluate) import Control.Applicative ((<$>), (<*>), optional) import Control.Monad.IO.Class (liftIO) @@ -76,15 +78,18 @@ import qualified Control.Distributed.Process.Backend.Azure as Azure (remoteTable -- Main -- -------------------------------------------------------------------------------- +type RemoteProcess a = Closure (Backend -> Process a) +type LocalProcess a = IO a + data ProcessPair b = forall a. Serializable a => ProcessPair { - ppairRemote :: Closure (Process a) - , ppairLocal :: a -> IO b + ppairRemote :: RemoteProcess a + , ppairLocal :: a -> LocalProcess b , ppairDict :: Static (SerializableDict a) } -genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table - -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' - -> (String -> IO (Closure (Process ()))) -- ^ Closures to support in @run --background@ +genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table + -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' + -> (String -> IO (RemoteProcess ())) -- ^ Closures to support in @run --background@ -> IO () genericMain remoteTable callable spawnable = do _ <- SSH.initialize True @@ -170,7 +175,7 @@ onVmRun rtable host port bg = do procEnc <- BSL.getContents -- Force evaluation (so that we can safely close stdin) _length <- evaluate (BSL.length procEnc) - let proc = decode procEnc + let proc = decode procEnc :: RemoteProcess () if bg then do hClose stdin @@ -182,15 +187,17 @@ onVmRun rtable host port bg = do else startCH proc where - startCH :: Closure (Process ()) -> IO () - startCH proc = do + startCH :: RemoteProcess () -> IO () + startCH rproc = do mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> throwIO err Right transport -> do node <- newLocalNode transport rtable - runProcess node $ - catch (join . unClosure $ proc) + runProcess node $ do + let backend = error "TODO: backend not initialized in onVmRun" + proc <- unClosure rproc :: Process (Backend -> Process ()) + catch (proc backend) (\e -> liftIO (print (e :: SomeException) >> throwIO e)) -------------------------------------------------------------------------------- @@ -290,7 +297,7 @@ targetParser = Target ) <*> optional (strOption ( long "virtual-machine" & metavar "VM" - & help "Virtual machine name" + & help "Virtual machine name (all VMs if unspecified)" )) checkMD5Parser :: Parser Command From 41c0330b75a40d2b47f59ac3a6c325ebbbb9baec Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 7 Aug 2012 12:55:06 +0100 Subject: [PATCH 0158/2357] Prepand length so that we don't read until EOF --- distributed-process-azure.cabal | 5 +- .../Distributed/Process/Backend/Azure.hs | 50 ++++++++++++------- .../Process/Backend/Azure/GenericMain.hs | 7 ++- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index a48d4215..7b89305c 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -35,8 +35,9 @@ Library network-transport-tcp >= 0.2 && < 0.3, optparse-applicative >= 0.2 && < 0.4, transformers >= 0.3 && < 0.4, - certificate == 1.2.3, - unix >= 2.5 && < 2.6 + certificate >= 1.2.4 && < 1.3, + unix >= 2.5 && < 2.6, + network-transport >= 0.2 && < 0.3 Exposed-modules: Control.Distributed.Process.Backend.Azure, Control.Distributed.Process.Backend.Azure.GenericMain Extensions: ViewPatterns, diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 126475aa..2abbf979 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -18,11 +18,16 @@ import System.Environment.Executable (getExecutablePath) import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString.Char8 as BSSC (pack) -import qualified Data.ByteString.Lazy as BSL (ByteString, readFile, putStr) +import qualified Data.ByteString.Lazy as BSL + ( ByteString + , readFile + , putStr + , length + ) import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) -import Control.Monad (void) +import Control.Monad (void, unless) import Control.Exception (catches, Handler(Handler)) import Control.Monad.IO.Class (liftIO) @@ -76,6 +81,7 @@ import Control.Distributed.Process.Closure , mkStatic ) import Control.Distributed.Process.Serializable (Serializable) +import Network.Transport.Internal (encodeInt32) encodeToStdout :: Serializable a => a -> Process () encodeToStdout = liftIO . BSL.putStr . encode @@ -154,7 +160,7 @@ defaultAzureParameters sid x509 pkey = do initializeBackend :: AzureParameters -- ^ Connection parameters -> String -- ^ Cloud service name -> IO Backend -initializeBackend params cloudService = do +initializeBackend params cloudService = return Backend { findVMs = apiFindVMs params cloudService , copyToVM = apiCopyToVM params @@ -194,7 +200,8 @@ apiCallOnVM params dict vm port proc = ++ " 2>&1" (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe - _cnt <- SSH.writeAllChannel ch (encode proc') + _ <- SSH.writeChannel ch (encodeInt32 (BSL.length procEnc)) + _ <- SSH.writeAllChannel ch procEnc SSH.channelSendEOF ch SSH.readAllChannel ch if status == 0 @@ -204,27 +211,32 @@ apiCallOnVM params dict vm port proc = proc' :: Closure (Backend -> Process ()) proc' = proc `cpComp` cpEncodeToStdout dict + procEnc :: BSL.ByteString + procEnc = encode proc' + apiSpawnOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Backend -> Process ()) -> IO () apiSpawnOnVM params vm port proc = - withSSH2 params vm $ \s -> do - let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm run " - ++ " --host " ++ vmIpAddress vm - ++ " --port " ++ port - ++ " --background " - ++ " 2>&1" - (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do - SSH.channelExecute ch exe - _cnt <- SSH.writeAllChannel ch (encode proc) - SSH.channelSendEOF ch - SSH.readAllChannel ch - if status == 0 - then return () - else error (BSLC.unpack r) + withSSH2 params vm $ \s -> do + let exe = "PATH=. " ++ azureSshRemotePath params + ++ " onvm run " + ++ " --host " ++ vmIpAddress vm + ++ " --port " ++ port + ++ " --background " + ++ " 2>&1" + (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch exe + _ <- SSH.writeChannel ch (encodeInt32 (BSL.length procEnc)) + _ <- SSH.writeAllChannel ch procEnc + SSH.channelSendEOF ch + SSH.readAllChannel ch + unless (status == 0) $ error (BSLC.unpack r) + where + procEnc :: BSL.ByteString + procEnc = encode proc -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index 264ef169..2af2db72 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -17,7 +17,8 @@ import System.IO , hClose ) import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (getContents, length) +import qualified Data.ByteString.Lazy as BSL (hGet, length) +import qualified Data.ByteString as BSS (hGet) import Control.Monad (unless, forM, forM_, void) import Control.Exception (throwIO, SomeException, evaluate) import Control.Applicative ((<$>), (<*>), optional) @@ -73,6 +74,7 @@ import Control.Distributed.Process.Backend.Azure , Backend(findVMs, copyToVM, checkMD5, callOnVM, spawnOnVM) ) import qualified Control.Distributed.Process.Backend.Azure as Azure (remoteTable) +import Network.Transport.Internal (decodeInt32) -------------------------------------------------------------------------------- -- Main -- @@ -172,7 +174,8 @@ onVmRun :: RemoteTable -> String -> String -> Bool -> IO () onVmRun rtable host port bg = do hSetBinaryMode stdin True hSetBinaryMode stdout True - procEnc <- BSL.getContents + procEncLength <- decodeInt32 <$> BSS.hGet stdin 4 + procEnc <- BSL.hGet stdin procEncLength -- Force evaluation (so that we can safely close stdin) _length <- evaluate (BSL.length procEnc) let proc = decode procEnc :: RemoteProcess () From 22ac3541ad14b6289ac197e8e5d2e433a9aacc60 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 8 Aug 2012 09:58:32 +0100 Subject: [PATCH 0159/2357] Modify call: local and remote proc can talk Previously the remote process could return a single value to the local process on completion, but that is not powerful enough. --- demos/Conway.hs | 7 +- demos/Echo.hs | 44 +++++ demos/Ping.hs | 62 ++++---- distributed-process-azure.cabal | 23 ++- .../Distributed/Process/Backend/Azure.hs | 150 +++++++++++------- .../Process/Backend/Azure/GenericMain.hs | 141 +++++++++------- 6 files changed, 276 insertions(+), 151 deletions(-) create mode 100644 demos/Echo.hs diff --git a/demos/Conway.hs b/demos/Conway.hs index 1ab4bbdc..31885638 100644 --- a/demos/Conway.hs +++ b/demos/Conway.hs @@ -4,19 +4,18 @@ import Data.Binary (Binary(get, put)) import Data.Binary.Generic (getGeneric, putGeneric) import Control.Distributed.Process ( Process - , Closure , expect ) import Control.Distributed.Process.Closure ( remotable , mkClosure ) -import Control.Distributed.Process.Backend.Azure (Backend) -import Control.Distributed.Process.Backend.Azure.GenericMain - ( genericMain +import Control.Distributed.Process.Backend.Azure + ( Backend , ProcessPair(..) , RemoteProcess ) +import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) data ControllerMsg = ControllerExit diff --git a/demos/Echo.hs b/demos/Echo.hs new file mode 100644 index 00000000..1239e392 --- /dev/null +++ b/demos/Echo.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE TemplateHaskell #-} + +import System.IO (hFlush, stdout) +import Control.Monad (unless, forever) +import Control.Monad.IO.Class (liftIO) +import Control.Distributed.Process (Process, expect) +import Control.Distributed.Process.Closure (remotable, mkClosure) +import Control.Distributed.Process.Backend.Azure + ( Backend + , ProcessPair(..) + , RemoteProcess + , LocalProcess + , localExpect + , remoteSend + , localSend + ) +import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) + +echoServer :: () -> Backend -> Process () +echoServer () _backend = forever $ do + str <- expect + remoteSend (str :: String) + +remotable ['echoServer] + +echoClient :: LocalProcess () +echoClient = do + str <- liftIO $ putStr "# " >> hFlush stdout >> getLine + unless (null str) $ do + localSend str + liftIO $ putStr "Echo: " >> hFlush stdout + echo <- localExpect + liftIO $ putStrLn echo + echoClient + +main :: IO () +main = genericMain __remoteTable callable spawnable + where + callable :: String -> IO (ProcessPair ()) + callable "echo" = return $ ProcessPair ($(mkClosure 'echoServer) ()) echoClient + callable _ = error "callable: unknown" + + spawnable :: String -> IO (RemoteProcess ()) + spawnable _ = error "spawnable: unknown" diff --git a/demos/Ping.hs b/demos/Ping.hs index 95b5379b..b4810692 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -1,16 +1,12 @@ {-# LANGUAGE TemplateHaskell #-} import Data.Binary (encode, decode) -import Control.Applicative ((<$>)) import Control.Monad (forever) import Control.Monad.IO.Class (liftIO) -import Control.Concurrent (threadDelay) import Control.Exception (try, IOException) import Control.Distributed.Process ( Process - , ProcessId , getSelfPid - , Closure , expect , send , monitor @@ -18,59 +14,57 @@ import Control.Distributed.Process , match , ProcessMonitorNotification(..) ) -import Control.Distributed.Process.Closure - ( remotable - , mkClosure - , SerializableDict(..) - , mkStatic - ) -import Control.Distributed.Process.Backend.Azure (Backend) -import Control.Distributed.Process.Backend.Azure.GenericMain - ( genericMain +import Control.Distributed.Process.Closure (remotable, mkClosure) +import Control.Distributed.Process.Backend.Azure + ( Backend , ProcessPair(..) , RemoteProcess + , LocalProcess + , localExpect + , remoteSend ) +import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) -sdictString :: SerializableDict String -sdictString = SerializableDict - -ping :: () -> Backend -> Process String -ping () _backend = do +pingClient :: () -> Backend -> Process () +pingClient () _backend = do mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") case mPingServerEnc of Left err -> - return $ "Ping server not found: " ++ show (err :: IOException) + remoteSend $ "Ping server not found: " ++ show (err :: IOException) Right pingServerEnc -> do - let pingServer = decode pingServerEnc + let pingServerPid = decode pingServerEnc pid <- getSelfPid - monitor pingServer - send pingServer pid + _ref <- monitor pingServerPid + send pingServerPid pid gotReply <- receiveWait [ match (\() -> return True) - , match (\(ProcessMonitorNotification _ _ _) -> return False) + , match (\(ProcessMonitorNotification {}) -> return False) ] if gotReply - then return $ "Ping server at " ++ show pingServer ++ " ok" - else return $ "Ping server at " ++ show pingServer ++ " failure" + then remoteSend $ "Ping server at " ++ show pingServerPid ++ " ok" + else remoteSend $ "Ping server at " ++ show pingServerPid ++ " failure" pingServer :: () -> Backend -> Process () pingServer () _backend = do - pid <- getSelfPid - liftIO $ BSL.writeFile "pingServer.pid" (encode pid) + us <- getSelfPid + liftIO $ BSL.writeFile "pingServer.pid" (encode us) forever $ do - pid <- expect - send pid () + them <- expect + send them () + +remotable ['pingClient, 'pingServer] -remotable ['ping, 'pingServer, 'sdictString] +receiveString :: LocalProcess () +receiveString = localExpect >>= liftIO . putStrLn main :: IO () main = genericMain __remoteTable callable spawnable where callable :: String -> IO (ProcessPair ()) - callable "ping" = return $ ProcessPair ($(mkClosure 'ping) ()) putStrLn $(mkStatic 'sdictString) - callable _ = error "spawnable: unknown" + callable "ping" = return $ ProcessPair ($(mkClosure 'pingClient) ()) receiveString + callable _ = error "callable: unknown" spawnable :: String -> IO (RemoteProcess ()) - spawnable "pingServer" = return $ $(mkClosure 'pingServer) () - spawnable _ = error "callable: unknown" + spawnable "pingServer" = return $ ($(mkClosure 'pingServer) ()) + spawnable _ = error "spawnable: unknown" diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 7b89305c..7544ad52 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -37,17 +37,30 @@ Library transformers >= 0.3 && < 0.4, certificate >= 1.2.4 && < 1.3, unix >= 2.5 && < 2.6, - network-transport >= 0.2 && < 0.3 + network-transport >= 0.2 && < 0.3, + mtl >= 2.1 && < 2.2 Exposed-modules: Control.Distributed.Process.Backend.Azure, Control.Distributed.Process.Backend.Azure.GenericMain Extensions: ViewPatterns, RankNTypes, ExistentialQuantification, ScopedTypeVariables, - DeriveDataTypeable + DeriveDataTypeable, + GeneralizedNewtypeDeriving ghc-options: -Wall HS-Source-Dirs: src +Executable cloud-haskell-azure-echo + if flag(build-demos) + Build-Depends: base >= 4.4 && < 5, + distributed-process-azure >= 0.1 && < 0.2, + distributed-process >= 0.2 && < 0.3, + transformers >= 0.3 && < 0.4 + else + buildable: False + Main-Is: demos/Echo.hs + ghc-options: -Wall + Executable cloud-haskell-azure-ping if flag(build-demos) Build-Depends: base >= 4.4 && < 5, @@ -55,10 +68,13 @@ Executable cloud-haskell-azure-ping distributed-process >= 0.2 && < 0.3, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, - binary >= 0.5 && < 0.6 + binary >= 0.5 && < 0.6, + mtl, + libssh2 else buildable: False Main-Is: demos/Ping.hs + ghc-options: -Wall Executable cloud-haskell-azure-conway if flag(build-demos) @@ -72,3 +88,4 @@ Executable cloud-haskell-azure-conway else buildable: False Main-Is: demos/Conway.hs + ghc-options: -Wall diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 2abbf979..61aa6f1a 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -1,35 +1,50 @@ -{-# LANGUAGE TemplateHaskell #-} module Control.Distributed.Process.Backend.Azure ( -- * Initialization Backend(..) , AzureParameters(..) , defaultAzureParameters , initializeBackend - , remoteTable -- * Re-exports from Azure Service Management , CloudService(..) , VirtualMachine(..) , Azure.cloudServices + -- * Remote and local processes + , ProcessPair(..) + , RemoteProcess + , LocalProcess + , localExpect + , remoteSend + , localSend ) where import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) +import System.IO (stdout, hFlush) import Data.Binary (encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) +import qualified Data.ByteString as BSS + ( ByteString + , length + , concat + , hPut + ) import qualified Data.ByteString.Char8 as BSSC (pack) import qualified Data.ByteString.Lazy as BSL ( ByteString , readFile - , putStr , length + , fromChunks + , toChunks + , hPut ) import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void, unless) +import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) import Control.Exception (catches, Handler(Handler)) -import Control.Monad.IO.Class (liftIO) +import Control.Monad.IO.Class (MonadIO, liftIO) -- Azure import Network.Azure.ServiceManagement @@ -52,11 +67,13 @@ import qualified Network.SSH.Client.LibSSH2 as SSH , Session , readAllChannel , writeAllChannel + , Channel ) import qualified Network.SSH.Client.LibSSH2.Foreign as SSH ( openChannelSession , channelExecute , writeChannel + , readChannel , channelSendEOF ) import qualified Network.SSH.Client.LibSSH2.Errors as SSH @@ -66,40 +83,13 @@ import qualified Network.SSH.Client.LibSSH2.Errors as SSH ) -- CH -import Control.Distributed.Process - ( Closure(Closure) - , Process - , Static - , RemoteTable - ) -import Control.Distributed.Process.Closure - ( remotable - , cpComp - , SerializableDict(SerializableDict) - , staticConst - , staticApply - , mkStatic - ) +import Control.Distributed.Process (Process, Closure) import Control.Distributed.Process.Serializable (Serializable) -import Network.Transport.Internal (encodeInt32) - -encodeToStdout :: Serializable a => a -> Process () -encodeToStdout = liftIO . BSL.putStr . encode - -encodeToStdoutDict :: SerializableDict a -> a -> Process () -encodeToStdoutDict SerializableDict = encodeToStdout - -remotable ['encodeToStdoutDict] - --- | Remote table necessary for the Azure backend -remoteTable :: RemoteTable -> RemoteTable -remoteTable = __remoteTable - -cpEncodeToStdout :: forall a. Typeable a => Static (SerializableDict a) -> Closure (a -> Process ()) -cpEncodeToStdout dict = Closure decoder (encode ()) - where - decoder :: Static (BSL.ByteString -> a -> Process ()) - decoder = staticConst `staticApply` ($(mkStatic 'encodeToStdoutDict) `staticApply` dict) +import Control.Distributed.Process.Internal.Types + ( messageToPayload + , createMessage + ) +import Network.Transport.Internal (encodeInt32, decodeInt32, prependLength) -- | Azure backend data Backend = Backend { @@ -110,11 +100,10 @@ data Backend = Backend { -- | Check the MD5 hash of the remote executable , checkMD5 :: VirtualMachine -> IO Bool -- | @runOnVM dict vm port p@ starts a CH node on port 'port' and runs 'p' - , callOnVM :: forall a. Serializable a - => Static (SerializableDict a) - -> VirtualMachine + , callOnVM :: forall a. + VirtualMachine -> String - -> Closure (Backend -> Process a) + -> ProcessPair a -> IO a -- | Create a new CH node and run the specified process in the background. -- The CH node will exit when the process exists. @@ -184,14 +173,12 @@ apiCopyToVM params vm = SSH.scpSendFile s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) -- | Call a process on a VM -apiCallOnVM :: Serializable a - => AzureParameters - -> Static (SerializableDict a) +apiCallOnVM :: AzureParameters -> VirtualMachine -> String - -> Closure (Backend -> Process a) + -> ProcessPair a -> IO a -apiCallOnVM params dict vm port proc = +apiCallOnVM params vm port ppair = withSSH2 params vm $ \s -> do let exe = "PATH=. " ++ azureSshRemotePath params ++ " onvm run " @@ -200,19 +187,15 @@ apiCallOnVM params dict vm port proc = ++ " 2>&1" (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe - _ <- SSH.writeChannel ch (encodeInt32 (BSL.length procEnc)) - _ <- SSH.writeAllChannel ch procEnc - SSH.channelSendEOF ch - SSH.readAllChannel ch + _ <- SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc)) + _ <- SSH.writeAllChannel ch rprocEnc + runLocalProcess (ppairLocal ppair) ch if status == 0 - then return $ decode r - else error (BSLC.unpack r) + then return r + else error "callOnVM: Non-zero exit status" where - proc' :: Closure (Backend -> Process ()) - proc' = proc `cpComp` cpEncodeToStdout dict - - procEnc :: BSL.ByteString - procEnc = encode proc' + rprocEnc :: BSL.ByteString + rprocEnc = encode (ppairRemote ppair) apiSpawnOnVM :: AzureParameters -> VirtualMachine @@ -280,3 +263,56 @@ catchSshError s io = localHash :: AzureParameters -> IO MD5Digest localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) + +-------------------------------------------------------------------------------- +-- Local and remote processes -- +-------------------------------------------------------------------------------- + +data ProcessPair a = ProcessPair { + ppairRemote :: RemoteProcess () + , ppairLocal :: LocalProcess a + } + +type RemoteProcess a = Closure (Backend -> Process a) + +newtype LocalProcess a = LocalProcess { unLocalProcess :: ReaderT SSH.Channel IO a } + deriving (Functor, Monad, MonadIO, MonadReader SSH.Channel) + +runLocalProcess :: LocalProcess a -> SSH.Channel -> IO a +runLocalProcess = runReaderT . unLocalProcess + +localExpect :: Serializable a => LocalProcess a +localExpect = LocalProcess $ do + ch <- ask + liftIO $ do + len <- decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4 + decode <$> readSizeChannel ch len + +localSend :: Serializable a => a -> LocalProcess () +localSend x = LocalProcess $ do + ch <- ask + liftIO $ mapM_ (SSH.writeChannel ch) + . prependLength + . messageToPayload + . createMessage + $ x + +remoteSend :: Serializable a => a -> Process () +remoteSend x = liftIO $ do + let enc = encode x + BSS.hPut stdout (encodeInt32 (BSL.length enc)) + BSL.hPut stdout enc + hFlush stdout + +-------------------------------------------------------------------------------- +-- SSH utilities -- +-------------------------------------------------------------------------------- + +readSizeChannel :: SSH.Channel -> Int -> IO BSL.ByteString +readSizeChannel ch = go [] + where + go :: [BSS.ByteString] -> Int -> IO BSL.ByteString + go acc 0 = return (BSL.fromChunks $ reverse acc) + go acc size = do + bs <- SSH.readChannel ch (fromIntegral (0x400 `min` size)) + go (bs : acc) (size - BSS.length bs) diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index 2af2db72..e8cdf80e 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -2,8 +2,6 @@ module Control.Distributed.Process.Backend.Azure.GenericMain ( genericMain , ProcessPair(..) - , RemoteProcess - , LocalProcess ) where import Prelude hiding (catch) @@ -15,17 +13,21 @@ import System.IO , stderr , hSetBinaryMode , hClose + , Handle ) +import Data.Foldable (forM_) import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (hGet, length) -import qualified Data.ByteString as BSS (hGet) -import Control.Monad (unless, forM, forM_, void) -import Control.Exception (throwIO, SomeException, evaluate) +import qualified Data.ByteString.Lazy as BSL (ByteString, hGet, toChunks, length) +import qualified Data.ByteString as BSS (hGet, length) +import Control.Monad (unless, forM, void) +import Control.Monad.Reader (ask) +import Control.Exception (throwIO, SomeException) import Control.Applicative ((<$>), (<*>), optional) import Control.Monad.IO.Class (liftIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, readMVar) -- Posix -import System.Posix.Process (forkProcess, createSession) +import qualified System.Posix.Process as Posix (forkProcess, createSession) -- SSH import qualified Network.SSH.Client.LibSSH2.Foreign as SSH @@ -55,16 +57,22 @@ import Options.Applicative -- CH import Control.Distributed.Process ( RemoteTable - , Closure , Process , unClosure - , Static , catch ) -import Control.Distributed.Process.Node (newLocalNode, runProcess, initRemoteTable) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Process.Closure (SerializableDict) -import Network.Transport.TCP (createTransport, defaultTCPParameters) +import Control.Distributed.Process.Node + ( newLocalNode + , runProcess + , forkProcess + , initRemoteTable + , LocalNode + ) +import Control.Distributed.Process.Internal.Types + ( LocalProcess(processQueue) + , payloadToMessage + , Message + ) import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName, azureSetup) , defaultAzureParameters @@ -72,23 +80,20 @@ import Control.Distributed.Process.Backend.Azure , cloudServices , VirtualMachine(vmName) , Backend(findVMs, copyToVM, checkMD5, callOnVM, spawnOnVM) + , ProcessPair(..) + , RemoteProcess ) -import qualified Control.Distributed.Process.Backend.Azure as Azure (remoteTable) +import Control.Distributed.Process.Internal.CQueue (CQueue, enqueue) + +-- Transport +import Network.Transport (Transport) import Network.Transport.Internal (decodeInt32) +import Network.Transport.TCP (createTransport, defaultTCPParameters) -------------------------------------------------------------------------------- -- Main -- -------------------------------------------------------------------------------- -type RemoteProcess a = Closure (Backend -> Process a) -type LocalProcess a = IO a - -data ProcessPair b = forall a. Serializable a => ProcessPair { - ppairRemote :: RemoteProcess a - , ppairLocal :: a -> LocalProcess b - , ppairDict :: Static (SerializableDict a) - } - genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' -> (String -> IO (RemoteProcess ())) -- ^ Closures to support in @run --background@ @@ -137,12 +142,9 @@ genericMain remoteTable callable spawnable = do procPair <- callable (closureId cmd) forM_ vms $ \vm -> do putStr (vmName vm ++ ": ") >> hFlush stdout - case procPair of - ProcessPair rProc lProc dict -> do - result <- callOnVM backend dict vm (remotePort cmd) rProc - lProc result + callOnVM backend vm (remotePort cmd) procPair OnVmCommand (vmCmd@OnVmRun {}) -> - onVmRun (remoteTable . Azure.remoteTable $ initRemoteTable) + onVmRun (remoteTable initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) (onVmBackground vmCmd) @@ -170,39 +172,72 @@ azureParameters opts (Just sshOpts) = do azureSshUserName = remoteUser sshOpts } +-------------------------------------------------------------------------------- +-- Executing a closure on the VM -- +-------------------------------------------------------------------------------- + onVmRun :: RemoteTable -> String -> String -> Bool -> IO () onVmRun rtable host port bg = do hSetBinaryMode stdin True hSetBinaryMode stdout True - procEncLength <- decodeInt32 <$> BSS.hGet stdin 4 - procEnc <- BSL.hGet stdin procEncLength - -- Force evaluation (so that we can safely close stdin) - _length <- evaluate (BSL.length procEnc) - let proc = decode procEnc :: RemoteProcess () - if bg - then do - hClose stdin - hClose stdout - hClose stderr - void . forkProcess $ do - void createSession - startCH proc - else - startCH proc + mProcEnc <- getWithLength stdin + forM_ mProcEnc $ \procEnc -> do + let proc = decode procEnc + lprocMVar <- newEmptyMVar :: IO (MVar LocalProcess) + if bg + then detach $ startCH proc lprocMVar runProcess + else do + _pid <- startCH proc lprocMVar forkProcess + lproc <- readMVar lprocMVar + queueFromHandle stdin (processQueue lproc) where - startCH :: RemoteProcess () -> IO () - startCH rproc = do + startCH :: RemoteProcess () -> MVar LocalProcess -> (LocalNode -> Process () -> IO a) -> IO a + startCH rproc lprocMVar go = do + transport <-newTransport + node <- newLocalNode transport rtable + go node $ do + ask >>= liftIO . putMVar lprocMVar + let backend = error "TODO: backend not initialized in onVmRun" + proc <- unClosure rproc :: Process (Backend -> Process ()) + catch (proc backend) exceptionHandler + + newTransport :: IO Transport + newTransport = do mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> throwIO err - Right transport -> do - node <- newLocalNode transport rtable - runProcess node $ do - let backend = error "TODO: backend not initialized in onVmRun" - proc <- unClosure rproc :: Process (Backend -> Process ()) - catch (proc backend) - (\e -> liftIO (print (e :: SomeException) >> throwIO e)) - + Right transport -> return transport + + exceptionHandler :: SomeException -> Process () + exceptionHandler e = liftIO $ appendFile "error.log" (show e) + +-- | Read a 4-byte length @l@ and then an @l@-byte payload +-- +-- Returns Nothing on EOF +getWithLength :: Handle -> IO (Maybe BSL.ByteString) +getWithLength h = do + lenEnc <- BSS.hGet h 4 + if BSS.length lenEnc < 4 + then return Nothing + else do + let len = decodeInt32 lenEnc + bs <- BSL.hGet h len + if BSL.length bs < fromIntegral len + then return Nothing + else return (Just bs) + +queueFromHandle :: Handle -> CQueue Message -> IO () +queueFromHandle h q = do + mPayload <- getWithLength stdin + forM_ mPayload $ \payload -> do + enqueue q $ payloadToMessage (BSL.toChunks payload) + queueFromHandle h q + +detach :: IO () -> IO () +detach io = do + mapM_ hClose [stdin, stdout, stderr] + void . Posix.forkProcess $ void Posix.createSession >> io + -------------------------------------------------------------------------------- -- Command line options -- -------------------------------------------------------------------------------- From c4152b141e229b4dd8f43115066050818f871e33 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 8 Aug 2012 10:22:04 +0100 Subject: [PATCH 0160/2357] Deal with remote exceptions in callOnVM --- .../Distributed/Process/Backend/Azure.hs | 25 +++++++++-- .../Process/Backend/Azure/GenericMain.hs | 44 +++++++++---------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 61aa6f1a..0a7eee84 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -14,6 +14,8 @@ module Control.Distributed.Process.Backend.Azure , LocalProcess , localExpect , remoteSend + , remoteThrow + , remoteSend' , localSend ) where @@ -43,7 +45,7 @@ import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void, unless) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) -import Control.Exception (catches, Handler(Handler)) +import Control.Exception (Exception, catches, Handler(Handler)) import Control.Monad.IO.Class (MonadIO, liftIO) -- Azure @@ -285,8 +287,12 @@ localExpect :: Serializable a => LocalProcess a localExpect = LocalProcess $ do ch <- ask liftIO $ do - len <- decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4 - decode <$> readSizeChannel ch len + isE <- readIntChannel ch + len <- readIntChannel ch + msg <- readSizeChannel ch len + if isE /= 0 + then error (decode msg) + else return (decode msg) localSend :: Serializable a => a -> LocalProcess () localSend x = LocalProcess $ do @@ -298,8 +304,15 @@ localSend x = LocalProcess $ do $ x remoteSend :: Serializable a => a -> Process () -remoteSend x = liftIO $ do +remoteSend = liftIO . remoteSend' 0 + +remoteThrow :: Exception e => e -> Process () +remoteThrow = liftIO . remoteSend' 1 . show + +remoteSend' :: Serializable a => Int -> a -> IO () +remoteSend' flags x = do let enc = encode x + BSS.hPut stdout (encodeInt32 flags) BSS.hPut stdout (encodeInt32 (BSL.length enc)) BSL.hPut stdout enc hFlush stdout @@ -316,3 +329,7 @@ readSizeChannel ch = go [] go acc size = do bs <- SSH.readChannel ch (fromIntegral (0x400 `min` size)) go (bs : acc) (size - BSS.length bs) + +readIntChannel :: SSH.Channel -> IO Int +readIntChannel ch = + decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4 diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index e8cdf80e..13f18fe6 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -21,7 +21,7 @@ import qualified Data.ByteString.Lazy as BSL (ByteString, hGet, toChunks, length import qualified Data.ByteString as BSS (hGet, length) import Control.Monad (unless, forM, void) import Control.Monad.Reader (ask) -import Control.Exception (throwIO, SomeException) +import Control.Exception (SomeException) import Control.Applicative ((<$>), (<*>), optional) import Control.Monad.IO.Class (liftIO) import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, readMVar) @@ -73,6 +73,9 @@ import Control.Distributed.Process.Internal.Types , payloadToMessage , Message ) +import Control.Distributed.Process.Internal.CQueue (CQueue, enqueue) + +-- Azure import Control.Distributed.Process.Backend.Azure ( AzureParameters(azureSshUserName, azureSetup) , defaultAzureParameters @@ -82,11 +85,11 @@ import Control.Distributed.Process.Backend.Azure , Backend(findVMs, copyToVM, checkMD5, callOnVM, spawnOnVM) , ProcessPair(..) , RemoteProcess + , remoteSend' + , remoteThrow ) -import Control.Distributed.Process.Internal.CQueue (CQueue, enqueue) -- Transport -import Network.Transport (Transport) import Network.Transport.Internal (decodeInt32) import Network.Transport.TCP (createTransport, defaultTCPParameters) @@ -185,31 +188,28 @@ onVmRun rtable host port bg = do let proc = decode procEnc lprocMVar <- newEmptyMVar :: IO (MVar LocalProcess) if bg - then detach $ startCH proc lprocMVar runProcess + then detach $ startCH proc lprocMVar runProcess (\_ -> return ()) else do - _pid <- startCH proc lprocMVar forkProcess + startCH proc lprocMVar forkProcess remoteThrow lproc <- readMVar lprocMVar queueFromHandle stdin (processQueue lproc) where - startCH :: RemoteProcess () -> MVar LocalProcess -> (LocalNode -> Process () -> IO a) -> IO a - startCH rproc lprocMVar go = do - transport <-newTransport - node <- newLocalNode transport rtable - go node $ do - ask >>= liftIO . putMVar lprocMVar - let backend = error "TODO: backend not initialized in onVmRun" - proc <- unClosure rproc :: Process (Backend -> Process ()) - catch (proc backend) exceptionHandler - - newTransport :: IO Transport - newTransport = do + startCH :: RemoteProcess () + -> MVar LocalProcess + -> (LocalNode -> Process () -> IO a) + -> (SomeException -> Process ()) + -> IO () + startCH rproc lprocMVar go exceptionHandler = do mTransport <- createTransport host port defaultTCPParameters case mTransport of - Left err -> throwIO err - Right transport -> return transport - - exceptionHandler :: SomeException -> Process () - exceptionHandler e = liftIO $ appendFile "error.log" (show e) + Left err -> remoteSend' 1 (show err) + Right transport -> do + node <- newLocalNode transport rtable + void . go node $ do + ask >>= liftIO . putMVar lprocMVar + let backend = error "TODO: backend not initialized in onVmRun" + proc <- unClosure rproc :: Process (Backend -> Process ()) + catch (proc backend) exceptionHandler -- | Read a 4-byte length @l@ and then an @l@-byte payload -- From e52ccf5d4fd6811a93fcb177070760110dd29c97 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 8 Aug 2012 10:54:54 +0100 Subject: [PATCH 0161/2357] Resurrect ping benchmarks --- benchmarks/Headers.gnuplot | 16 +++ benchmarks/Indirection.gnuplot | 16 +++ benchmarks/JustPingC.c | 150 ++++++++++++++++++++++++++ benchmarks/JustPingCacheHeader.hs | 134 ++++++++++++++++++++++++ benchmarks/JustPingHaskell.hs | 105 +++++++++++++++++++ benchmarks/JustPingOneRecv.hs | 131 +++++++++++++++++++++++ benchmarks/JustPingThroughChan.hs | 127 ++++++++++++++++++++++ benchmarks/JustPingThroughMVar.hs | 127 ++++++++++++++++++++++ benchmarks/JustPingTransport.hs | 95 +++++++++++++++++ benchmarks/JustPingTwoSocketPairs.hs | 151 +++++++++++++++++++++++++++ benchmarks/JustPingWithHeader.hs | 131 +++++++++++++++++++++++ benchmarks/Makefile | 55 ++++++++++ benchmarks/NewTransport.gnuplot | 8 ++ benchmarks/cabal_macros.h | 5 + 14 files changed, 1251 insertions(+) create mode 100644 benchmarks/Headers.gnuplot create mode 100644 benchmarks/Indirection.gnuplot create mode 100644 benchmarks/JustPingC.c create mode 100644 benchmarks/JustPingCacheHeader.hs create mode 100644 benchmarks/JustPingHaskell.hs create mode 100644 benchmarks/JustPingOneRecv.hs create mode 100644 benchmarks/JustPingThroughChan.hs create mode 100644 benchmarks/JustPingThroughMVar.hs create mode 100644 benchmarks/JustPingTransport.hs create mode 100644 benchmarks/JustPingTwoSocketPairs.hs create mode 100644 benchmarks/JustPingWithHeader.hs create mode 100644 benchmarks/Makefile create mode 100644 benchmarks/NewTransport.gnuplot create mode 100644 benchmarks/cabal_macros.h diff --git a/benchmarks/Headers.gnuplot b/benchmarks/Headers.gnuplot new file mode 100644 index 00000000..131a3569 --- /dev/null +++ b/benchmarks/Headers.gnuplot @@ -0,0 +1,16 @@ +set title "Roundtrip (us)" +set yrange [0:150] +plot "JustPingHaskellNT.data" smooth bezier with lines title "JustPingHaskell (NOT -threaded)", \ + "JustPingHaskell.data" smooth bezier with lines title "JustPingHaskell", \ + "JustPingWithHeader.data" smooth bezier with lines title "JustPingWithHeader", \ + "JustPingOneRecv.data" smooth bezier with lines title "JustPingOneRecv", \ + "JustPingCacheHeader.data" smooth bezier with lines title "JustPingCacheHeader", \ + "JustPingC.data" smooth bezier with lines title "JustPingC" +set terminal postscript color +set output "Headers.ps" +plot "JustPingHaskellNT.data" smooth bezier with lines title "JustPingHaskell (NOT -threaded)", \ + "JustPingHaskell.data" smooth bezier with lines title "JustPingHaskell", \ + "JustPingWithHeader.data" smooth bezier with lines title "JustPingWithHeader", \ + "JustPingOneRecv.data" smooth bezier with lines title "JustPingOneRecv", \ + "JustPingCacheHeader.data" smooth bezier with lines title "JustPingCacheHeader", \ + "JustPingC.data" smooth bezier with lines title "JustPingC" diff --git a/benchmarks/Indirection.gnuplot b/benchmarks/Indirection.gnuplot new file mode 100644 index 00000000..e7808707 --- /dev/null +++ b/benchmarks/Indirection.gnuplot @@ -0,0 +1,16 @@ +set title "Roundtrip (us)" +set yrange [0:150] +plot "JustPingWithHeader.data" smooth bezier with lines title "JustPingWithHeader", \ + "JustPingThroughChan.data" smooth bezier with lines title "JustPingThroughChan", \ + "JustPingThroughMVar.data" smooth bezier with lines title "JustPingThroughMVar", \ + "JustPingTwoSocketPairs.data" smooth bezier with lines title "JustPingTwoSocketPairs", \ + "JustPingTwoSocketPairsND.data" smooth bezier with lines title "JustPingTwoSocketPairs (--NoDelay)", \ + "JustPingTransport.data" smooth bezier with lines title "JustPingTransport" +set terminal postscript color +set output "Indirection.ps" +plot "JustPingWithHeader.data" smooth bezier with lines title "JustPingWithHeader", \ + "JustPingThroughChan.data" smooth bezier with lines title "JustPingThroughChan", \ + "JustPingThroughMVar.data" smooth bezier with lines title "JustPingThroughMVar", \ + "JustPingTwoSocketPairs.data" smooth bezier with lines title "JustPingTwoSocketPairs", \ + "JustPingTwoSocketPairsND.data" smooth bezier with lines title "JustPingTwoSocketPairs (--NoDelay)", \ + "JustPingTransport.data" smooth bezier with lines title "JustPingTransport" diff --git a/benchmarks/JustPingC.c b/benchmarks/JustPingC.c new file mode 100644 index 00000000..9391dc8d --- /dev/null +++ b/benchmarks/JustPingC.c @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Note: this is not consistent across CPUs (and hence across threads on multicore machines) +double timestamp() { + struct timeval tp; + gettimeofday(&tp, NULL); + return ((double) tp.tv_sec) * 1e6 + (double) tp.tv_usec; +} + +int server() { + printf("starting server\n"); + + struct addrinfo hints, *res; + int error, server_socket; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + error = getaddrinfo(NULL, "8080", &hints, &res); + if(error) { + printf("server error: %s\n", gai_strerror(error)); + return -1; + } + + server_socket = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if(server_socket < 0) { + printf("server error: could not create socket\n"); + return -1; + } + + int yes = 1; + if(setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) < 0) { + printf("server error: could not set socket options\n"); + return -1; + } + + if(bind(server_socket, res->ai_addr, res->ai_addrlen) < 0) { + printf("server error: could not bind to socket\n"); + return -1; + } + + listen(server_socket, 5); + + int client_socket; + struct sockaddr_storage client_addr; + socklen_t addr_size; + client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_size); + + for(;;) { + char* buf = malloc(8); + ssize_t read = recv(client_socket, buf, 8, 0); + if(read == 0) { + free(buf); + break; + } + // printf("server received '%s'\n", buf); + send(client_socket, buf, 8, 0); + free(buf); + } + + freeaddrinfo(res); + return 0; +} + +int client(int pings) { + printf("starting client\n"); + + struct addrinfo hints, *res; + int error, client_socket; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + hints.ai_socktype = SOCK_STREAM; + + error = getaddrinfo("127.0.0.1", "8080", &hints, &res); + if(error) { + printf("client error: %s\n", gai_strerror(error)); + return -1; + } + + client_socket = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if(client_socket < 0) { + printf("client error: could not create socket\n"); + return -1; + } + + if(connect(client_socket, res->ai_addr, res->ai_addrlen) < 0) { + printf("client error: could not connect: %s\n", strerror(errno)); + return -1; + } + + for(int i = 0; i < pings; i++) { + double timestamp_before = timestamp(); + + send(client_socket, "ping123", 8, 0); + + char *buf = malloc(8); + ssize_t read = recv(client_socket, buf, 8, 0); + + if(read == 0) { + printf("server exited prematurely\n"); + free(buf); + break; + } + + // printf("client received '%s'\n", buf); + free(buf); + + double timestamp_after = timestamp(); + fprintf(stderr, "%i %lf\n", i, timestamp_after - timestamp_before); + } + + printf("client did %d pings\n", pings); + + freeaddrinfo(res); + return 0; +} + +int usage(int argc, char** argv) { + printf("usage: %s \n", argv[0]); + return -1; +} + +int main(int argc, char** argv) { + if(argc != 2) { + return usage(argc, argv); + } + + if(fork() == 0) { + // TODO: we should wait until we know the server is ready + int pings = 0; + sscanf(argv[1], "%d", &pings); + return client(pings); + } else { + return server(); + } +} diff --git a/benchmarks/JustPingCacheHeader.hs b/benchmarks/JustPingCacheHeader.hs new file mode 100644 index 00000000..e9c29e88 --- /dev/null +++ b/benchmarks/JustPingCacheHeader.hs @@ -0,0 +1,134 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Data.ByteString.Internal as BSI +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + +main :: IO () +main = do + [pingsStr] <- getArgs + serverReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + putStrLn "server: creating TCP connection" + serverAddrs <- getAddrInfo + (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) + Nothing + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + + putStrLn "server: awaiting client connection" + putMVar serverReady () + listen sock 1 + (clientSock, clientAddr) <- accept sock + + putStrLn "server: listening for pings" + pong clientSock + + -- Start the client + forkIO $ do + takeMVar serverReady + let pings = read pingsStr + serverAddrs <- getAddrInfo + Nothing + (Just "127.0.0.1") + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + + N.connect sock (addrAddress serverAddr) + + ping sock pings + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Int -> IO () +ping sock pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- recv sock 8 + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +pong :: Socket -> IO () +pong sock = do + bs <- recv sock 8 + -- putStrLn $ "server received " ++ unpack bs + when (BS.length bs > 0) $ do + send sock bs + pong sock + +-- | Wrapper around NBS.recv (for profiling) +recv :: Socket -> Int -> IO ByteString +recv sock i = do + (header, payload) <- BS.splitAt 4 `fmap` NBS.recv sock (4 + i) + -- Ignore header + return payload + +-- | Cached header +header :: ByteString +header = pack "fake" + +-- | Wrapper around NBS.send (for profiling) +send :: Socket -> ByteString -> IO () +send sock bs = do + NBS.sendMany sock [header, bs] + +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingHaskell.hs b/benchmarks/JustPingHaskell.hs new file mode 100644 index 00000000..8aaf157a --- /dev/null +++ b/benchmarks/JustPingHaskell.hs @@ -0,0 +1,105 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) + +main :: IO () +main = do + [pingsStr] <- getArgs + serverReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + putStrLn "server: creating TCP connection" + serverAddrs <- getAddrInfo + (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) + Nothing + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + + putStrLn "server: awaiting client connection" + putMVar serverReady () + listen sock 1 + (clientSock, clientAddr) <- accept sock + + putStrLn "server: listening for pings" + pong clientSock + + -- Start the client + forkIO $ do + takeMVar serverReady + let pings = read pingsStr + serverAddrs <- getAddrInfo + Nothing + (Just "127.0.0.1") + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + + N.connect sock (addrAddress serverAddr) + + ping sock pings + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Int -> IO () +ping sock pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- recv sock 8 + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +pong :: Socket -> IO () +pong sock = do + bs <- recv sock 8 + -- putStrLn $ "server received " ++ unpack bs + when (BS.length bs > 0) $ do + send sock bs + pong sock + +-- | Wrapper around NBS.recv (for profiling) +recv :: Socket -> Int -> IO ByteString +recv = NBS.recv + +-- | Wrapper around NBS.send (for profiling) +send :: Socket -> ByteString -> IO Int +send = NBS.send diff --git a/benchmarks/JustPingOneRecv.hs b/benchmarks/JustPingOneRecv.hs new file mode 100644 index 00000000..aae81cf4 --- /dev/null +++ b/benchmarks/JustPingOneRecv.hs @@ -0,0 +1,131 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Data.ByteString.Internal as BSI +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + +main :: IO () +main = do + [pingsStr] <- getArgs + serverReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + putStrLn "server: creating TCP connection" + serverAddrs <- getAddrInfo + (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) + Nothing + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + + putStrLn "server: awaiting client connection" + putMVar serverReady () + listen sock 1 + (clientSock, clientAddr) <- accept sock + + putStrLn "server: listening for pings" + pong clientSock + + -- Start the client + forkIO $ do + takeMVar serverReady + let pings = read pingsStr + serverAddrs <- getAddrInfo + Nothing + (Just "127.0.0.1") + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + + N.connect sock (addrAddress serverAddr) + + ping sock pings + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Int -> IO () +ping sock pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- recv sock 8 + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +pong :: Socket -> IO () +pong sock = do + bs <- recv sock 8 + -- putStrLn $ "server received " ++ unpack bs + when (BS.length bs > 0) $ do + send sock bs + pong sock + +-- | Wrapper around NBS.recv (for profiling) +recv :: Socket -> Int -> IO ByteString +recv sock i = do + (header, payload) <- BS.splitAt 4 `fmap` NBS.recv sock (4 + i) + length <- decodeLength header -- Ignored + return payload + +-- | Wrapper around NBS.send (for profiling) +send :: Socket -> ByteString -> IO () +send sock bs = do + length <- encodeLength (fromIntegral (BS.length bs)) + NBS.sendMany sock [length, bs] + +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingThroughChan.hs b/benchmarks/JustPingThroughChan.hs new file mode 100644 index 00000000..ad4538aa --- /dev/null +++ b/benchmarks/JustPingThroughChan.hs @@ -0,0 +1,127 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfo, AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Data.ByteString.Internal as BSI +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + +passive :: Maybe AddrInfo +passive = Just (defaultHints { addrFlags = [AI_PASSIVE] }) + +main = do + [pingsStr] <- getArgs + serverReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + -- Initialize the server + serverAddr:_ <- getAddrInfo passive Nothing (Just "8080") + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + listen sock 1 + + -- Set up multiplexing channel + multiplexChannel <- newChan + + -- Wait for incoming connections (pings from the client) + putMVar serverReady () + (clientSock, pingAddr) <- accept sock + forkIO $ socketToChan clientSock multiplexChannel + + -- Reply to the client + forever $ readChan multiplexChannel >>= send clientSock + + -- Start the client + forkIO $ do + takeMVar serverReady + serverAddr:_ <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") + clientSock <- socket (addrFamily serverAddr) Stream defaultProtocol + N.connect clientSock (addrAddress serverAddr) + ping clientSock (read pingsStr) + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +socketToChan :: Socket -> Chan ByteString -> IO () +socketToChan sock chan = go + where + go = do bs <- recv sock + when (BS.length bs > 0) $ do + writeChan chan bs + go + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Int -> IO () +ping sock pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- recv sock + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +-- | Receive a package +recv :: Socket -> IO ByteString +recv sock = do + header <- NBS.recv sock 4 + length <- decodeLength header + NBS.recv sock (fromIntegral (length :: Int32)) + +-- | Send a package +send :: Socket -> ByteString -> IO () +send sock bs = do + length <- encodeLength (fromIntegral (BS.length bs)) + NBS.sendMany sock [length, bs] + +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingThroughMVar.hs b/benchmarks/JustPingThroughMVar.hs new file mode 100644 index 00000000..5b9e75c1 --- /dev/null +++ b/benchmarks/JustPingThroughMVar.hs @@ -0,0 +1,127 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfo, AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Data.ByteString.Internal as BSI +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + +passive :: Maybe AddrInfo +passive = Just (defaultHints { addrFlags = [AI_PASSIVE] }) + +main = do + [pingsStr] <- getArgs + serverReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + -- Initialize the server + serverAddr:_ <- getAddrInfo passive Nothing (Just "8080") + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + listen sock 1 + + -- Set up multiplexing channel + multiplexMVar <- newEmptyMVar + + -- Wait for incoming connections (pings from the client) + putMVar serverReady () + (clientSock, pingAddr) <- accept sock + forkIO $ socketToMVar clientSock multiplexMVar + + -- Reply to the client + forever $ takeMVar multiplexMVar >>= send clientSock + + -- Start the client + forkIO $ do + takeMVar serverReady + serverAddr:_ <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") + clientSock <- socket (addrFamily serverAddr) Stream defaultProtocol + N.connect clientSock (addrAddress serverAddr) + ping clientSock (read pingsStr) + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +socketToMVar :: Socket -> MVar ByteString -> IO () +socketToMVar sock mvar = go + where + go = do bs <- recv sock + when (BS.length bs > 0) $ do + putMVar mvar bs + go + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Int -> IO () +ping sock pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- recv sock + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +-- | Receive a package +recv :: Socket -> IO ByteString +recv sock = do + header <- NBS.recv sock 4 + length <- decodeLength header + NBS.recv sock (fromIntegral (length :: Int32)) + +-- | Send a package +send :: Socket -> ByteString -> IO () +send sock bs = do + length <- encodeLength (fromIntegral (BS.length bs)) + NBS.sendMany sock [length, bs] + +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingTransport.hs b/benchmarks/JustPingTransport.hs new file mode 100644 index 00000000..29b745fe --- /dev/null +++ b/benchmarks/JustPingTransport.hs @@ -0,0 +1,95 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Network.Transport +import Network.Transport.TCP + +main :: IO () +main = do + [pingsStr] <- getArgs + serverAddr <- newEmptyMVar + clientAddr <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + -- establish transport and endpoint + putStrLn "server: creating TCP connection" + Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Connect to the client so that we can reply + theirAddr <- takeMVar clientAddr + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- reply to pings with pongs + putStrLn "server: awaiting client connection" + ConnectionOpened _ _ _ <- receive endpoint + pong endpoint conn + + -- Start the client + forkIO $ do + let pings = read pingsStr + + -- establish transport and endpoint + Right transport <- createTransport "127.0.0.1" "8081" defaultTCPParameters + Right endpoint <- newEndPoint transport + putMVar clientAddr (address endpoint) + + -- Connect to the server to send pings + theirAddr <- takeMVar serverAddr + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + -- Send pings, waiting for a reply after every ping + ConnectionOpened _ _ _ <- receive endpoint + ping endpoint conn pings + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +pingMessage :: [ByteString] +pingMessage = [pack "ping123"] + +ping :: EndPoint -> Connection -> Int -> IO () +ping endpoint conn pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send conn pingMessage + Received _ _payload <- receive endpoint + after <- getCurrentTime + -- putStrLn $ "client received " ++ show _payload + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +pong :: EndPoint -> Connection -> IO () +pong endpoint conn = go + where + go = do + msg <- receive endpoint + case msg of + Received _ payload -> send conn payload >> go + ConnectionClosed _ -> return () + _ -> fail "Unexpected message" diff --git a/benchmarks/JustPingTwoSocketPairs.hs b/benchmarks/JustPingTwoSocketPairs.hs new file mode 100644 index 00000000..1eea7862 --- /dev/null +++ b/benchmarks/JustPingTwoSocketPairs.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfo, AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr, NoDelay) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Data.ByteString.Internal as BSI +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) +import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + +passive :: Maybe AddrInfo +passive = Just (defaultHints { addrFlags = [AI_PASSIVE] }) + +main = do + pingsStr:args <- getArgs + serverReady <- newEmptyMVar + clientReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + -- Initialize the server + serverAddr:_ <- getAddrInfo passive Nothing (Just "8080") + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + listen sock 1 + + -- Set up multiplexing channel + multiplexChannel <- newChan + + -- Connect to the client (to reply) + forkIO $ do + takeMVar clientReady + clientAddr:_ <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8081") + pongSock <- socket (addrFamily clientAddr) Stream defaultProtocol + N.connect pongSock (addrAddress clientAddr) + when ("--NoDelay" `elem` args) $ setSocketOption pongSock NoDelay 1 + forever $ readChan multiplexChannel >>= send pongSock + + -- Wait for incoming connections (pings from the client) + putMVar serverReady () + (pingSock, pingAddr) <- accept sock + socketToChan pingSock multiplexChannel + + -- Start the client + forkIO $ do + clientAddr:_ <- getAddrInfo passive Nothing (Just "8081") + sock <- socket (addrFamily clientAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress clientAddr) + listen sock 1 + + -- Set up multiplexing channel + multiplexChannel <- newChan + + -- Connect to the server (to send pings) + forkIO $ do + takeMVar serverReady + serverAddr:_ <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") + pingSock <- socket (addrFamily serverAddr) Stream defaultProtocol + N.connect pingSock (addrAddress serverAddr) + when ("--NoDelay" `elem` args) $ setSocketOption pingSock NoDelay 1 + ping pingSock multiplexChannel (read pingsStr) + putMVar clientDone () + + -- Wait for incoming connections (pongs from the server) + putMVar clientReady () + (pongSock, pongAddr) <- accept sock + socketToChan pongSock multiplexChannel + + -- Wait for the client to finish + takeMVar clientDone + +socketToChan :: Socket -> Chan ByteString -> IO () +socketToChan sock chan = go + where + go = do bs <- recv sock + when (BS.length bs > 0) $ do + writeChan chan bs + go + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Chan ByteString -> Int -> IO () +ping sock chan pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- readChan chan + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +-- | Receive a package +recv :: Socket -> IO ByteString +recv sock = do + header <- NBS.recv sock 4 + length <- decodeLength header + NBS.recv sock (fromIntegral (length :: Int32)) + +-- | Send a package +send :: Socket -> ByteString -> IO () +send sock bs = do + length <- encodeLength (fromIntegral (BS.length bs)) + NBS.sendMany sock [length, bs] + +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingWithHeader.hs b/benchmarks/JustPingWithHeader.hs new file mode 100644 index 00000000..37cae85a --- /dev/null +++ b/benchmarks/JustPingWithHeader.hs @@ -0,0 +1,131 @@ +{-# LANGUAGE CPP, BangPatterns #-} + +module Main where + +import Control.Monad + +import Data.Int +import Network.Socket + ( AddrInfoFlag (AI_PASSIVE), HostName, ServiceName, Socket + , SocketType (Stream), SocketOption (ReuseAddr) + , accept, addrAddress, addrFlags, addrFamily, bindSocket, defaultProtocol + , defaultHints + , getAddrInfo, listen, setSocketOption, socket, sClose, withSocketsDo ) +import System.Environment (getArgs, withArgs) +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import System.IO (withFile, IOMode(..), hPutStrLn, Handle, stderr) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar) +import qualified Network.Socket as N +import Debug.Trace +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack, unpack) +import qualified Data.ByteString as BS +import qualified Network.Socket.ByteString as NBS +import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime) +import Data.ByteString.Internal as BSI +import Foreign.Storable (pokeByteOff, peekByteOff) +import Foreign.C (CInt(..)) +import Foreign.ForeignPtr (withForeignPtr) + +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt +foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt + +main :: IO () +main = do + [pingsStr] <- getArgs + serverReady <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Start the server + forkIO $ do + putStrLn "server: creating TCP connection" + serverAddrs <- getAddrInfo + (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) + Nothing + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress serverAddr) + + putStrLn "server: awaiting client connection" + putMVar serverReady () + listen sock 1 + (clientSock, clientAddr) <- accept sock + + putStrLn "server: listening for pings" + pong clientSock + + -- Start the client + forkIO $ do + takeMVar serverReady + let pings = read pingsStr + serverAddrs <- getAddrInfo + Nothing + (Just "127.0.0.1") + (Just "8080") + let serverAddr = head serverAddrs + sock <- socket (addrFamily serverAddr) Stream defaultProtocol + + N.connect sock (addrAddress serverAddr) + + ping sock pings + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone + +pingMessage :: ByteString +pingMessage = pack "ping123" + +ping :: Socket -> Int -> IO () +ping sock pings = go pings + where + go :: Int -> IO () + go 0 = do + putStrLn $ "client did " ++ show pings ++ " pings" + go !i = do + before <- getCurrentTime + send sock pingMessage + bs <- recv sock 8 + after <- getCurrentTime + -- putStrLn $ "client received " ++ unpack bs + let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) + hPutStrLn stderr $ show i ++ " " ++ show latency + go (i - 1) + +pong :: Socket -> IO () +pong sock = do + bs <- recv sock 8 + -- putStrLn $ "server received " ++ unpack bs + when (BS.length bs > 0) $ do + send sock bs + pong sock + +-- | Wrapper around NBS.recv (for profiling) +recv :: Socket -> Int -> IO ByteString +recv sock _ = do + header <- NBS.recv sock 4 + length <- decodeLength header + NBS.recv sock (fromIntegral (length :: Int32)) + +-- | Wrapper around NBS.send (for profiling) +send :: Socket -> ByteString -> IO () +send sock bs = do + length <- encodeLength (fromIntegral (BS.length bs)) + NBS.sendMany sock [length, bs] + +-- | Encode length (manual for now) +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) + +-- | Decode length (manual for now) +decodeLength :: ByteString -> IO Int32 +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in + withForeignPtr fp $ \p -> do + w32 <- peekByteOff p 0 + return (fromIntegral (ntohl w32)) diff --git a/benchmarks/Makefile b/benchmarks/Makefile new file mode 100644 index 00000000..1eae30d0 --- /dev/null +++ b/benchmarks/Makefile @@ -0,0 +1,55 @@ +# The "Just Ping" tests measure ping latency with as little overhead as possible + +NUMPINGS=100000 +ROOT=../.. +INCLUDES=-i${ROOT}/network-transport/src -i${ROOT}/network-transport-tcp/src + +# Enable for profiling +# PROF_GHC=-prof -fprof-auto +# PROF_EXE=+RTS -pa -RTS + +GCC=gcc -std=c99 +GHC=ghc -rtsopts -XRankNTypes -XScopedTypeVariables -XDeriveDataTypeable -XCPP -XGeneralizedNewtypeDeriving -optP-include -optPcabal_macros.h + +all: NewTransport.ps Indirection.ps Headers.ps + +NewTransport.ps: JustPingHaskell.data \ + JustPingTransport.data + gnuplot ./NewTransport.gnuplot + +Indirection.ps: JustPingWithHeader.data \ + JustPingThroughChan.data \ + JustPingThroughMVar.data \ + JustPingTwoSocketPairs.data \ + JustPingTwoSocketPairsND.data \ + JustPingTransport.data + gnuplot ./Indirection.gnuplot + +Headers.ps: JustPingC.data \ + JustPingHaskellNT.data \ + JustPingHaskell.data \ + JustPingWithHeader.data \ + JustPingOneRecv.data \ + JustPingCacheHeader.data + gnuplot ./Headers.gnuplot + +JustPingC.exe: JustPingC.c + $(GCC) -O2 -o JustPingC.exe JustPingC.c + +JustPingHaskellNT.exe: JustPingHaskell.hs + $(GHC) -O2 $(PROF_GHC) -o JustPingHaskellNT.exe JustPingHaskell.hs + +JustPingTwoSocketPairsND.data: JustPingTwoSocketPairs.exe + time ./$< $(NUMPINGS) --NoDelay $(PROF_EXE) 2>$@ + +%.data : %.exe + time ./$< $(NUMPINGS) $(PROF_EXE) 2>$@ + +%.exe :: %.hs + $(GHC) -O2 $(PROF_GHC) -threaded -o $@ --make $< $(INCLUDES) + +.PHONY: clean +clean: + rm -f *.data *.ps *.pdf *.o *.hi *.exe + +# vi:set noexpandtab: diff --git a/benchmarks/NewTransport.gnuplot b/benchmarks/NewTransport.gnuplot new file mode 100644 index 00000000..cf69dbf3 --- /dev/null +++ b/benchmarks/NewTransport.gnuplot @@ -0,0 +1,8 @@ +set title "Roundtrip (us)" +set yrange [0:150] +plot "JustPingHaskell.data" smooth bezier with lines title "JustPingHaskell", \ + "JustPingTransport.data" smooth bezier with lines title "JustPingTransport" +set terminal postscript color +set output "NewTransport.ps" +plot "JustPingHaskell.data" smooth bezier with lines title "JustPingHaskell", \ + "JustPingTransport.data" smooth bezier with lines title "JustPingTransport" diff --git a/benchmarks/cabal_macros.h b/benchmarks/cabal_macros.h new file mode 100644 index 00000000..705542cc --- /dev/null +++ b/benchmarks/cabal_macros.h @@ -0,0 +1,5 @@ +#define VERSION_base "4.5.0.0" +#define MIN_VERSION_base(major1,major2,minor) (\ + (major1) < 4 || \ + (major1) == 4 && (major2) < 5 || \ + (major1) == 4 && (major2) == 5 && (minor) <= 0) From 4a8e349078d9d62c9d649d2ba22a971c4ebe9fdb Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 9 Aug 2012 10:01:22 +0100 Subject: [PATCH 0162/2357] First steps towards extracting static This makes 'static' a separate module. Lots of cleaning up still to do, but first I'd like to try and remove the unsafe stuff from Dynamic and properly support polymorphic types. --- LICENSE | 30 ++ Setup.hs | 2 + distributed-static.cabal | 29 ++ src/Control/Distributed/Static.hs | 352 ++++++++++++++++++ .../Distributed/Static/Internal/Dynamic.hs | 121 ++++++ .../Distributed/Static/Internal/TypeRep.hs | 33 ++ 6 files changed, 567 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 distributed-static.cabal create mode 100644 src/Control/Distributed/Static.hs create mode 100644 src/Control/Distributed/Static/Internal/Dynamic.hs create mode 100644 src/Control/Distributed/Static/Internal/TypeRep.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a956d0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Edsko de Vries + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Edsko de Vries nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/distributed-static.cabal b/distributed-static.cabal new file mode 100644 index 00000000..38fdecbe --- /dev/null +++ b/distributed-static.cabal @@ -0,0 +1,29 @@ +Name: distributed-static +Version: 0.1.0.0 +Synopsis: Workaround the absence of "static" in ghc +-- description: +Homepage: http://www.github.com/haskell-distributed/distributed-process +License: BSD3 +License-File: LICENSE +Author: Edsko de Vries +Maintainer: edsko@well-typed.com +Copyright: Well-Typed LLP +Category: Control +Build-Type: Simple +Cabal-Version: >=1.8 + +Library + Exposed-Modules: Control.Distributed.Static, + Control.Distributed.Static.Internal.Dynamic, + Control.Distributed.Static.Internal.TypeRep + Build-Depends: base ==4.5.*, + containers >= 0.4 && < 0.5, + bytestring >= 0.0 && < 0.11, + binary >= 0.5 && < 0.6, + ghc-prim >= 0.2 && < 0.3 + HS-Source-Dirs: src + Extensions: DeriveDataTypeable, + ScopedTypeVariables, + RankNTypes, + CPP + GHC-Options: -Wall diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs new file mode 100644 index 00000000..4d0a8da9 --- /dev/null +++ b/src/Control/Distributed/Static.hs @@ -0,0 +1,352 @@ +-- | /Towards Haskell in the Cloud/ introduces a new typing construct called +-- @static@. Unfortunately, ghc does not yet include support for @static@. In +-- this module we provide a workaround, which at the same time also extends +-- the functionality of @static@ as described in the paper and makes it more +-- expressive. +-- +-- The main idea (already discussed in /Towards Haskell in the Cloud/, section +-- /Faking It/) is to maintain a runtime mapping from labels (strings) to +-- values. A basic static value (introduced using 'staticLabel') then is an +-- index into this runtime mapping. It is the responsibility of the client code +-- to make sure that the corresponding value in the 'RemoteTable' matches the +-- type of the label, although we do runtime type checks where possible. For +-- this reason, we only work with 'Typeable' values in this module. +module Control.Distributed.Static + ( -- * Introducing static values + Static + , staticLabel + , staticApply + , staticDuplicate + , staticTypeOf + -- * Eliminating static values + , RemoteTable + , initRemoteTable + , registerStatic + , resolveStatic + -- * Closures + , Closure(Closure) + , staticClosure + , resolveClosure + -- * Static values + , idStatic + , composeStatic + , constStatic + , flipStatic + , fstStatic + , sndStatic + , firstStatic + , secondStatic + , splitStatic + , unitStatic + , appStatic + -- * Combinators on static values + , staticCompose + , staticSplit + , staticConst + -- * Combinators on closures + , closureApplyStatic + , closureApply + , closureCompose + , closureSplit + -- * Re-exports + , Dynamic + , toDyn + , unsafeToDyn + , fromDyn + , fromDynamic + , dynTypeRep + ) where + +import Prelude hiding (id, (.)) +import Data.Typeable + ( Typeable + , TypeRep + , typeOf + , funResultTy + ) +import Data.Maybe (fromJust) +import Data.Binary + ( Binary(get, put) + , Put + , Get + , putWord8 + , getWord8 + , encode + , decode + ) +import Data.ByteString.Lazy (ByteString, empty) +import Data.Map (Map) +import qualified Data.Map as Map (lookup, empty, insert) +import Control.Applicative ((<$>), (<*>)) +import Control.Category (Category(id, (.))) +import qualified Control.Arrow as Arrow (first, second, (***), app) +import Control.Distributed.Static.Internal.Dynamic + ( Dynamic + , dynApply + , toDyn + , unsafeToDyn + , fromDyn + , dynTypeRep + , fromDynamic + , unsafeCastDyn + ) +import Control.Distributed.Static.Internal.TypeRep (compareTypeRep) + +-------------------------------------------------------------------------------- +-- Introducing static values -- +-------------------------------------------------------------------------------- + +data StaticLabel = + StaticLabel String TypeRep + | StaticApply StaticLabel StaticLabel + | StaticDuplicate StaticLabel TypeRep + deriving (Typeable, Show) + +-- | A static value. Static is opaque; see 'staticLabel', 'staticApply', +-- 'staticDuplicate' or 'staticTypeOf'. +newtype Static a = Static StaticLabel + deriving (Typeable, Show) + +instance Typeable a => Binary (Static a) where + put (Static label) = putStaticLabel label + get = do + label <- getStaticLabel + if typeOfStaticLabel label `compareTypeRep` typeOf (undefined :: a) + then return $ Static label + else fail "Static.get: type error" + +-- We don't want StaticLabel to be its own Binary instance +putStaticLabel :: StaticLabel -> Put +putStaticLabel (StaticLabel string typ) = + putWord8 0 >> put string >> put typ +putStaticLabel (StaticApply label1 label2) = + putWord8 1 >> putStaticLabel label1 >> putStaticLabel label2 +putStaticLabel (StaticDuplicate label typ) = + putWord8 2 >> putStaticLabel label >> put typ + +getStaticLabel :: Get StaticLabel +getStaticLabel = do + header <- getWord8 + case header of + 0 -> StaticLabel <$> get <*> get + 1 -> StaticApply <$> getStaticLabel <*> getStaticLabel + 2 -> StaticDuplicate <$> getStaticLabel <*> get + _ -> fail "StaticLabel.get: invalid" + +-- | Create a primitive static value. +-- +-- It is the responsibility of the client code to make sure the corresponding +-- entry in the 'RemoteTable' has the appropriate type. +staticLabel :: forall a. Typeable a => String -> Static a +staticLabel label = Static (StaticLabel label (typeOf (undefined :: a))) + +-- | Apply two static values +staticApply :: Static (a -> b) -> Static a -> Static b +staticApply (Static f) (Static x) = Static (StaticApply f x) + +-- | Co-monadic 'duplicate' for static values +staticDuplicate :: forall a. Typeable a => Static a -> Static (Static a) +staticDuplicate (Static x) = + Static (StaticDuplicate x (typeOf (undefined :: Static a))) + +-- | @staticTypeOf (x :: a)@ mimicks @static (undefined :: a)@ -- even if +-- @x@ is not static, the type of @x@ always is. +staticTypeOf :: forall a. Typeable a => a -> Static a +staticTypeOf _ = Static (StaticLabel "$undefined" (typeOf (undefined :: a))) + +typeOfStaticLabel :: StaticLabel -> TypeRep +typeOfStaticLabel (StaticLabel _ typ) + = typ +typeOfStaticLabel (StaticApply f x) + = fromJust $ funResultTy (typeOfStaticLabel f) (typeOfStaticLabel x) +typeOfStaticLabel (StaticDuplicate _ typ) + = typ + +-------------------------------------------------------------------------------- +-- Eliminating static values -- +-------------------------------------------------------------------------------- + +-- | Runtime dictionary for 'unstatic' lookups +newtype RemoteTable = RemoteTable (Map String Dynamic) + +-- | Initial remote table +initRemoteTable :: RemoteTable +initRemoteTable = + registerStatic "$id" (unsafeToDyn identity) + . registerStatic "$compose" (unsafeToDyn compose) + . registerStatic "$const" (unsafeToDyn const) + . registerStatic "$flip" (unsafeToDyn flip) + . registerStatic "$fst" (unsafeToDyn fst) + . registerStatic "$snd" (unsafeToDyn snd) + . registerStatic "$first" (unsafeToDyn first) + . registerStatic "$second" (unsafeToDyn second) + . registerStatic "$split" (unsafeToDyn split) + . registerStatic "$unit" (toDyn ()) + . registerStatic "$app" (unsafeToDyn app) + . registerStatic "$decodeEnvPair" (toDyn decodeEnvPair) + $ RemoteTable Map.empty + where + identity :: a -> a + identity = id + + compose :: (b -> c) -> (a -> b) -> a -> c + compose = (.) + + first :: (a -> b) -> (a, c) -> (b, c) + first = Arrow.first + + second :: (a -> b) -> (c, a) -> (c, b) + second = Arrow.second + + split :: (a -> b) -> (a' -> b') -> (a, a') -> (b, b') + split = (Arrow.***) + + decodeEnvPair :: ByteString -> (ByteString, ByteString) + decodeEnvPair = decode + + app :: (a -> b, a) -> b + app = Arrow.app + +-- | Register a static label +registerStatic :: String -> Dynamic -> RemoteTable -> RemoteTable +registerStatic label dyn (RemoteTable rtable) + = RemoteTable (Map.insert label dyn rtable) + +-- | Resolve a Static value. +resolveStatic :: RemoteTable -> Static a -> Maybe Dynamic +resolveStatic (RemoteTable rtable) (Static (StaticLabel string typ)) = do + unsafeCastDyn (const typ) <$> Map.lookup string rtable +resolveStatic rtable (Static (StaticApply static1 static2)) = do + f <- resolveStatic rtable (Static static1) + x <- resolveStatic rtable (Static static2) + f `dynApply` x +resolveStatic _rtable (Static (StaticDuplicate static typ)) = + return . unsafeCastDyn (const typ) $ unsafeToDyn (Static static) + +-------------------------------------------------------------------------------- +-- Closures -- +-------------------------------------------------------------------------------- + +-- | A closure is a static value and an encoded environment +data Closure a = Closure (Static (ByteString -> a)) ByteString + deriving (Typeable, Show) + +instance Typeable a => Binary (Closure a) where + put (Closure static env) = put static >> put env + get = Closure <$> get <*> get + +-- | Convert a static value into a closure. +staticClosure :: forall a. Typeable a => Static a -> Closure a +staticClosure static = Closure (staticConst static) empty + +-- | Resolve a Closure +resolveClosure :: RemoteTable -> Static a -> ByteString -> Maybe Dynamic +resolveClosure rtable static env = do + decoder <- resolveStatic rtable static + decoder `dynApply` toDyn env + +-------------------------------------------------------------------------------- +-- Predefined static values -- +-------------------------------------------------------------------------------- + +-- | Static version of 'id' +idStatic :: (Typeable a) + => Static (a -> a) +idStatic = staticLabel "$id" + +-- | Static version of ('Prelude..') +composeStatic :: (Typeable a, Typeable b, Typeable c) + => Static ((b -> c) -> (a -> b) -> a -> c) +composeStatic = staticLabel "$compose" + +-- | Static version of 'const' +constStatic :: (Typeable a, Typeable b) + => Static (a -> b -> a) +constStatic = staticLabel "$const" + +-- | Static version of 'flip' +flipStatic :: (Typeable a, Typeable b, Typeable c) + => Static ((a -> b -> c) -> b -> a -> c) +flipStatic = staticLabel "$flip" + +-- | Static version of 'fst' +fstStatic :: (Typeable a, Typeable b) + => Static ((a, b) -> a) +fstStatic = staticLabel "$fst" + +-- | Static version of 'snd' +sndStatic :: (Typeable a, Typeable b) + => Static ((a, b) -> b) +sndStatic = staticLabel "$snd" + +-- | Static version of 'Arrow.first' +firstStatic :: (Typeable a, Typeable b, Typeable c) + => Static ((a -> b) -> (a, c) -> (b, c)) +firstStatic = staticLabel "$first" + +-- | Static version of 'Arrow.second' +secondStatic :: (Typeable a, Typeable b, Typeable c) + => Static ((a -> b) -> (c, a) -> (c, b)) +secondStatic = staticLabel "$second" + +-- | Static version of ('Arrow.***') +splitStatic :: (Typeable a, Typeable a', Typeable b, Typeable b') + => Static ((a -> b) -> (a' -> b') -> (a, a') -> (b, b')) +splitStatic = staticLabel "$split" + +-- | Static version of @()@ +unitStatic :: Static () +unitStatic = staticLabel "$unit" + +-- | Static version of 'Arrow.app' +appStatic :: (Typeable a, Typeable b) + => Static ((a -> b, a) -> b) +appStatic = staticLabel "$app" + +-------------------------------------------------------------------------------- +-- Combinators on static values -- +-------------------------------------------------------------------------------- + +staticCompose :: (Typeable a, Typeable b, Typeable c) + => Static (b -> c) -> Static (a -> b) -> Static (a -> c) +staticCompose g f = composeStatic `staticApply` g `staticApply` f + +staticSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') + => Static (a -> b) -> Static (a' -> b') -> Static ((a, a') -> (b, b')) +staticSplit f g = splitStatic `staticApply` f `staticApply` g + +staticConst :: (Typeable a, Typeable b) + => Static a -> Static (b -> a) +staticConst x = constStatic `staticApply` x + +-------------------------------------------------------------------------------- +-- Combinators on Closures -- +-------------------------------------------------------------------------------- + +closureApplyStatic :: forall a b. (Typeable a, Typeable b) + => Static (a -> b) -> Closure a -> Closure b +closureApplyStatic f (Closure decoder env) = + Closure (f `staticCompose` decoder) env + +decodeEnvPairStatic :: Static (ByteString -> (ByteString, ByteString)) +decodeEnvPairStatic = staticLabel "$decodeEnvPair" + +closureApply :: forall a b. (Typeable a, Typeable b) + => Closure (a -> b) -> Closure a -> Closure b +closureApply (Closure fdec fenv) (Closure xdec xenv) = + Closure decoder (encode (fenv, xenv)) + where + decoder :: Static (ByteString -> b) + decoder = appStatic + `staticCompose` + (fdec `staticSplit` xdec) + `staticCompose` + decodeEnvPairStatic + +closureCompose :: (Typeable a, Typeable b, Typeable c) + => Closure (b -> c) -> Closure (a -> b) -> Closure (a -> c) +closureCompose g f = composeStatic `closureApplyStatic` g `closureApply` f + +closureSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') + => Closure (a -> b) -> Closure (a' -> b') -> Closure ((a, a') -> (b, b')) +closureSplit f g = splitStatic `closureApplyStatic` f `closureApply` g diff --git a/src/Control/Distributed/Static/Internal/Dynamic.hs b/src/Control/Distributed/Static/Internal/Dynamic.hs new file mode 100644 index 00000000..14000956 --- /dev/null +++ b/src/Control/Distributed/Static/Internal/Dynamic.hs @@ -0,0 +1,121 @@ +module Control.Distributed.Static.Internal.Dynamic + ( -- * The @Dynamic@ type + Dynamic + -- * Converting to and from @Dynamic@ + , toDyn + , fromDyn + , fromDynamic + -- * Applying functions of dynamic type + , dynApply + , dynApp + , dynTypeRep + -- * Additions + , unsafeToDyn + , unsafeCastDyn + ) where + +import Control.Exception (Exception) +import qualified GHC.Prim as GHC (Any) +import Data.Typeable (Typeable, TypeRep, typeOf, funResultTy) +import Unsafe.Coerce (unsafeCoerce) + +------------------------------------------------------------- +-- +-- The type Dynamic +-- +------------------------------------------------------------- + +{-| + A value of type 'Dynamic' is an object encapsulated together with its type. + + A 'Dynamic' may only represent a monomorphic value; an attempt to + create a value of type 'Dynamic' from a polymorphically-typed + expression will result in an ambiguity error (see 'toDyn'). + + 'Show'ing a value of type 'Dynamic' returns a pretty-printed representation + of the object\'s type; useful for debugging. +-} +data Dynamic = Dynamic TypeRep Obj + deriving Typeable + +instance Show Dynamic where + -- the instance just prints the type representation. + showsPrec _ (Dynamic t _) = + showString "<<" . + showsPrec 0 t . + showString ">>" + +instance Exception Dynamic + +-- Use GHC's primitive 'Any' type to hold the dynamically typed value. +-- +-- In GHC's new eval/apply execution model this type must not look +-- like a data type. If it did, GHC would use the constructor convention +-- when evaluating it, and this will go wrong if the object is really a +-- function. Using Any forces GHC to use +-- a fallback convention for evaluating it that works for all types. +type Obj = GHC.Any + +-- | Converts an arbitrary value into an object of type 'Dynamic'. +-- +-- The type of the object must be an instance of 'Typeable', which +-- ensures that only monomorphically-typed objects may be converted to +-- 'Dynamic'. To convert a polymorphic object into 'Dynamic', give it +-- a monomorphic type signature. For example: +-- +-- > toDyn (id :: Int -> Int) +-- +toDyn :: Typeable a => a -> Dynamic +toDyn v = Dynamic (typeOf v) (unsafeCoerce v) + +-- | Converts a 'Dynamic' object back into an ordinary Haskell value of +-- the correct type. See also 'fromDynamic'. +fromDyn :: Typeable a + => Dynamic -- ^ the dynamically-typed object + -> a -- ^ a default value + -> a -- ^ returns: the value of the first argument, if + -- it has the correct type, otherwise the value of + -- the second argument. +fromDyn (Dynamic t v) def + | typeOf def == t = unsafeCoerce v + | otherwise = def + +-- | Converts a 'Dynamic' object back into an ordinary Haskell value of +-- the correct type. See also 'fromDyn'. +fromDynamic + :: Typeable a + => Dynamic -- ^ the dynamically-typed object + -> Maybe a -- ^ returns: @'Just' a@, if the dynamically-typed + -- object has the correct type (and @a@ is its value), + -- or 'Nothing' otherwise. +fromDynamic (Dynamic t v) = + case unsafeCoerce v of + r | t == typeOf r -> Just r + | otherwise -> Nothing + +-- (f::(a->b)) `dynApply` (x::a) = (f a)::b +dynApply :: Dynamic -> Dynamic -> Maybe Dynamic +dynApply (Dynamic t1 f) (Dynamic t2 x) = + case funResultTy t1 t2 of + Just t3 -> Just (Dynamic t3 ((unsafeCoerce f) x)) + Nothing -> Nothing + +dynApp :: Dynamic -> Dynamic -> Dynamic +dynApp f x = case dynApply f x of + Just r -> r + Nothing -> error ("Type error in dynamic application.\n" ++ + "Can't apply function " ++ show f ++ + " to argument " ++ show x) + +dynTypeRep :: Dynamic -> TypeRep +dynTypeRep (Dynamic tr _) = tr + +-------------------------------------------------------------------------------- +-- Additions -- +-------------------------------------------------------------------------------- + +unsafeToDyn :: a -> Dynamic +unsafeToDyn x = Dynamic (error "No type rep") (unsafeCoerce x) + +unsafeCastDyn :: (TypeRep -> TypeRep) -> Dynamic -> Dynamic +unsafeCastDyn f (Dynamic t x) = Dynamic (f t) x diff --git a/src/Control/Distributed/Static/Internal/TypeRep.hs b/src/Control/Distributed/Static/Internal/TypeRep.hs new file mode 100644 index 00000000..9d8da5a2 --- /dev/null +++ b/src/Control/Distributed/Static/Internal/TypeRep.hs @@ -0,0 +1,33 @@ +-- | 'Binary' instances for 'TypeRep', and 'TypeRep' equality (bug workaround) +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Control.Distributed.Static.Internal.TypeRep (compareTypeRep) where + +import Control.Applicative ((<$>), (<*>)) +import Data.Binary (Binary(get, put)) +import Data.Typeable.Internal (TypeRep(..), TyCon(..)) +import GHC.Fingerprint.Type (Fingerprint(..)) + +instance Binary Fingerprint where + put (Fingerprint hi lo) = put hi >> put lo + get = Fingerprint <$> get <*> get + +instance Binary TypeRep where + put (TypeRep fp tyCon ts) = put fp >> put tyCon >> put ts + get = TypeRep <$> get <*> get <*> get + +instance Binary TyCon where + put (TyCon hash package modul name) = put hash >> put package >> put modul >> put name + get = TyCon <$> get <*> get <*> get <*> get + +-- | Compare two type representations +-- +-- For base >= 4.6 this compares fingerprints, but older versions of base +-- have a bug in the fingerprint construction +-- () +compareTypeRep :: TypeRep -> TypeRep -> Bool +#if ! MIN_VERSION_base(4,6,0) +compareTypeRep (TypeRep _ con ts) (TypeRep _ con' ts') + = con == con' && all (uncurry compareTypeRep) (zip ts ts') +#else +compareTypeRep = (==) +#endif From fce87e7797ee8e8a424422e6d2e0664858483f40 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 9 Aug 2012 11:44:55 +0100 Subject: [PATCH 0163/2357] Start development of Rank1Typeable --- LICENSE | 30 +++++++++++ Setup.hs | 2 + rank1dynamic.cabal | 22 ++++++++ src/Data/Rank1Dynamic.hs | 3 ++ src/Data/Rank1Typeable.hs | 107 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 rank1dynamic.cabal create mode 100644 src/Data/Rank1Dynamic.hs create mode 100644 src/Data/Rank1Typeable.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a956d0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Edsko de Vries + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Edsko de Vries nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal new file mode 100644 index 00000000..b8f61a8e --- /dev/null +++ b/rank1dynamic.cabal @@ -0,0 +1,22 @@ +Name: rank1dynamic +Version: 0.1.0.0 +Synopsis: Version of Data.Dynamic with support for rank-1 types +-- description: +Homepage: http://github.com/haskell-distributed/distributed-process +License: BSD3 +License-File: LICENSE +Author: Edsko de Vries +Maintainer: edsko@well-typed.com +Copyright: Well-Typed LLP +Category: Data +Build-Type: Simple +Cabal-Version: >=1.8 + +Library + Exposed-Modules: Data.Rank1Dynamic, + Data.Rank1Typeable + Build-Depends: base ==4.5.* + HS-Source-Dirs: src + GHC-Options: -Wall + Extensions: EmptyDataDecls, + DeriveDataTypeable diff --git a/src/Data/Rank1Dynamic.hs b/src/Data/Rank1Dynamic.hs new file mode 100644 index 00000000..32971ba8 --- /dev/null +++ b/src/Data/Rank1Dynamic.hs @@ -0,0 +1,3 @@ +module Data.Rank1Dynamic () where + + diff --git a/src/Data/Rank1Typeable.hs b/src/Data/Rank1Typeable.hs new file mode 100644 index 00000000..1ef1675b --- /dev/null +++ b/src/Data/Rank1Typeable.hs @@ -0,0 +1,107 @@ +module Data.Rank1Typeable + ( Rank1TypeRep + , underlyingTypeRep + , typeOf + -- * Type variables + , ANY + , ANY1 + , ANY2 + , ANY3 + , ANY4 + , ANY5 + , ANY6 + , ANY7 + , ANY8 + , ANY9 + -- * Internal functions + , occurs + , subst + , unify + ) where + +import Data.Typeable (Typeable, TypeRep, TyCon) +import Control.Arrow ((***)) +import qualified Data.Typeable as Typeable (typeOf, splitTyConApp, mkTyConApp) + +newtype Rank1TypeRep = Rank1TypeRep { underlyingTypeRep :: TypeRep } + deriving Eq + +instance Show Rank1TypeRep where + show = show . underlyingTypeRep + +typeOf :: Typeable a => a -> Rank1TypeRep +typeOf = Rank1TypeRep . Typeable.typeOf + +data ANY deriving Typeable +data ANY1 deriving Typeable +data ANY2 deriving Typeable +data ANY3 deriving Typeable +data ANY4 deriving Typeable +data ANY5 deriving Typeable +data ANY6 deriving Typeable +data ANY7 deriving Typeable +data ANY8 deriving Typeable +data ANY9 deriving Typeable + +tvars :: [Rank1TypeRep] +tvars = + [ typeOf (undefined :: ANY) + , typeOf (undefined :: ANY1) + , typeOf (undefined :: ANY2) + , typeOf (undefined :: ANY3) + , typeOf (undefined :: ANY4) + , typeOf (undefined :: ANY5) + , typeOf (undefined :: ANY6) + , typeOf (undefined :: ANY7) + , typeOf (undefined :: ANY8) + , typeOf (undefined :: ANY9) + ] + +isTVar :: Rank1TypeRep -> Bool +isTVar = flip elem tvars + +splitTyConApp :: Rank1TypeRep -> (TyCon, [Rank1TypeRep]) +splitTyConApp t = + let (c, ts) = Typeable.splitTyConApp (underlyingTypeRep t) + in (c, map Rank1TypeRep ts) + +mkTyConApp :: TyCon -> [Rank1TypeRep] -> Rank1TypeRep +mkTyConApp c ts + = Rank1TypeRep (Typeable.mkTyConApp c (map underlyingTypeRep ts)) + +occurs :: Rank1TypeRep -> Rank1TypeRep -> Bool +occurs x t + | x == t = True + | otherwise = let (_, ts) = splitTyConApp t + in any (occurs x) ts + +subst :: Rank1TypeRep -> Rank1TypeRep -> Rank1TypeRep -> Rank1TypeRep +subst x t t' + | x == t' = t + | otherwise = let (c, ts) = splitTyConApp t' + in mkTyConApp c (map (subst x t) ts) + +unify :: Monad m => Rank1TypeRep -> Rank1TypeRep -> m [(Rank1TypeRep, Rank1TypeRep)] +unify t1 t2 = unify' [(t1, t2)] + +unify' :: Monad m + => [(Rank1TypeRep, Rank1TypeRep)] + -> m [(Rank1TypeRep, Rank1TypeRep)] +unify' [] = + return [] +unify' ((x, t) : eqs) | isTVar x && not (isTVar t) = + if x `occurs` t + then fail "Occurs check" + else do + s <- unify' (map (subst x t *** subst x t) eqs) + return ((x, t) : s) +unify' ((t, x) : eqs) | isTVar x && not (isTVar t) = + unify' ((x, t) : eqs) +unify' ((x, x') : eqs) | isTVar x && isTVar x' && x == x' = + unify' eqs +unify' ((t1, t2) : eqs) = do + let (c1, ts1) = splitTyConApp t1 + (c2, ts2) = splitTyConApp t2 + if c1 /= c2 + then fail $ "Cannot unify' " ++ show c1 ++ " and " ++ show c2 + else unify' (zip ts1 ts2 ++ eqs) From 4a51f854f28bea985375ac0da50805a036f4609b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 9 Aug 2012 16:35:25 +0100 Subject: [PATCH 0164/2357] Finish first implementation rank1dynamic --- rank1dynamic.cabal | 12 +- src/Data/Rank1Dynamic.hs | 119 +++++++++++- src/Data/Rank1Typeable.hs | 393 +++++++++++++++++++++++++++++--------- 3 files changed, 434 insertions(+), 90 deletions(-) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index b8f61a8e..f9f77927 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -1,7 +1,9 @@ Name: rank1dynamic Version: 0.1.0.0 -Synopsis: Version of Data.Dynamic with support for rank-1 types --- description: +Synopsis: Version of Data.Dynamic with support for rank-1 polymorphic types +Description: "Data.Typeable" and "Data.Dynamic" only support monomorphic types. + In this package we provide similar functionality but with + support for rank-1 polymorphic types. Homepage: http://github.com/haskell-distributed/distributed-process License: BSD3 License-File: LICENSE @@ -15,8 +17,10 @@ Cabal-Version: >=1.8 Library Exposed-Modules: Data.Rank1Dynamic, Data.Rank1Typeable - Build-Depends: base ==4.5.* + Build-Depends: base ==4.5.*, + ghc-prim >= 0.2 && < 0.3 HS-Source-Dirs: src GHC-Options: -Wall Extensions: EmptyDataDecls, - DeriveDataTypeable + DeriveDataTypeable, + ViewPatterns diff --git a/src/Data/Rank1Dynamic.hs b/src/Data/Rank1Dynamic.hs index 32971ba8..55b7464b 100644 --- a/src/Data/Rank1Dynamic.hs +++ b/src/Data/Rank1Dynamic.hs @@ -1,3 +1,120 @@ -module Data.Rank1Dynamic () where +-- | Dynamic values with support for rank-1 polymorphic types. +-- +-- [Examples of fromDynamic] +-- +-- These examples correspond to the 'Data.Rank1Typeable.isInstanceOf' examples +-- in "Data.Rank1Typeable". +-- +-- > > do f <- fromDynamic (toDynamic (even :: Int -> Bool)) ; return $ (f :: Int -> Int) 0 +-- > Left "Cannot unify Int and Bool" +-- > +-- > > do f <- fromDynamic (toDynamic (const 1 :: ANY -> Int)) ; return $ (f :: Int -> Int) 0 +-- > Right 1 +-- > +-- > > do f <- fromDynamic (toDynamic (unsafeCoerce :: ANY1 -> ANY2)) ; return $ (f :: Int -> Int) 0 +-- > Right 0 +-- > +-- > > do f <- fromDynamic (toDynamic (id :: ANY -> ANY)) ; return $ (f :: Int -> Bool) 0 +-- > Left "Cannot unify Bool and Int" +-- > +-- > > do f <- fromDynamic (toDynamic (undefined :: ANY)) ; return $ (f :: Int -> Int) 0 +-- > Right *** Exception: Prelude.undefined +-- > +-- > > do f <- fromDynamic (toDynamic (id :: ANY -> ANY)) ; return $ (f :: Int) +-- > Left "Cannot unify Int and ->" +-- +-- [Examples of dynApply] +-- +-- These examples correspond to the 'Data.Rank1Typeable.funResultTy' examples +-- in "Data.Rank1Typeable". +-- +-- > > do app <- toDynamic (id :: ANY -> ANY) `dynApply` toDynamic True ; f <- fromDynamic app ; return $ (f :: Bool) +-- > Right True +-- > +-- > > do app <- toDynamic (const :: ANY -> ANY1 -> ANY) `dynApply` toDynamic True ; f <- fromDynamic app ; return $ (f :: Int -> Bool) 0 +-- > Right True +-- > +-- > > do app <- toDynamic (($ True) :: (Bool -> ANY) -> ANY) `dynApply` toDynamic (id :: ANY -> ANY) ; f <- fromDynamic app ; return (f :: Bool) +-- > Right True +-- > +-- > > app <- toDynamic (const :: ANY -> ANY1 -> ANY) `dynApply` toDynamic (id :: ANY -> ANY) ; f <- fromDynamic app ; return $ (f :: Int -> Bool -> Bool) 0 True +-- > Right True +-- > +-- > > do app <- toDynamic ((\f -> f . f) :: (ANY -> ANY) -> ANY -> ANY) `dynApply` toDynamic (even :: Int -> Bool) ; f <- fromDynamic app ; return (f :: ()) +-- > Left "Cannot unify Int and Bool" +-- +-- [Using toDynamic] +-- +-- When using polymorphic values you need to give an explicit type annotation: +-- +-- > > toDynamic id +-- > +-- > :46:1: +-- > Ambiguous type variable `a0' in the constraint: +-- > (Typeable a0) arising from a use of `toDynamic' +-- > Probable fix: add a type signature that fixes these type variable(s) +-- > In the expression: toDynamic id +-- > In an equation for `it': it = toDynamic id +-- +-- versus +-- +-- > > toDynamic (id :: ANY -> ANY) +-- > < ANY>> +-- +-- Note that these type annotation are checked by ghc like any other: +-- +-- > > toDynamic (id :: ANY -> ANY1) +-- > +-- > :45:12: +-- > Couldn't match expected type `V1' with actual type `V0' +-- > Expected type: ANY -> ANY1 +-- > Actual type: ANY -> ANY +-- > In the first argument of `toDynamic', namely `(id :: ANY -> ANY1)' +-- > In the expression: toDynamic (id :: ANY -> ANY1) +module Data.Rank1Dynamic + ( Dynamic + , toDynamic + , fromDynamic + , TypeError + , dynTypeRep + , dynApply + ) where +import qualified GHC.Prim as GHC (Any) +import Data.Rank1Typeable + ( Typeable + , TypeRep + , typeOf + , isInstanceOf + , TypeError + , funResultTy + ) +import Unsafe.Coerce (unsafeCoerce) +-- | Encapsulate an object and its type +data Dynamic = Dynamic TypeRep GHC.Any + +instance Show Dynamic where + showsPrec _ (Dynamic t _) = showString "<<" . shows t . showString ">>" + +-- | Introduce a dynamic value +toDynamic :: Typeable a => a -> Dynamic +toDynamic x = Dynamic (typeOf x) (unsafeCoerce x) + +-- | Eliminate a dynamic value +fromDynamic :: Typeable a => Dynamic -> Either TypeError a +fromDynamic (Dynamic t v) = + case unsafeCoerce v of + r -> case typeOf r `isInstanceOf` t of + Left err -> Left err + Right () -> Right r + +-- | Apply one dynamic value to another +dynApply :: Dynamic -> Dynamic -> Either TypeError Dynamic +dynApply (Dynamic t1 f) (Dynamic t2 x) = do + t3 <- funResultTy t1 t2 + return $ Dynamic t3 (unsafeCoerce f x) + +-- | The type representation of a dynamic value +dynTypeRep :: Dynamic -> TypeRep +dynTypeRep (Dynamic t _) = t diff --git a/src/Data/Rank1Typeable.hs b/src/Data/Rank1Typeable.hs index 1ef1675b..6b5a096c 100644 --- a/src/Data/Rank1Typeable.hs +++ b/src/Data/Rank1Typeable.hs @@ -1,8 +1,86 @@ +-- | Runtime type representation of terms with support for rank-1 polymorphic +-- types with type variables of kind *. +-- +-- The essence of this module is that we use the standard 'Typeable' +-- representation of "Data.Typeable" but we introduce a special (empty) data +-- type 'TypVar' which represents type variables. 'TypVar' is indexed by an +-- arbitrary other data type, giving you an unbounded number of type variables; +-- for convenience, we define 'ANY', 'ANY1', .., 'ANY9'. +-- +-- [Examples of isInstanceOf] +-- +-- > -- We CANNOT use a term of type 'Int -> Bool' as 'Int -> Int' +-- > > typeOf (undefined :: Int -> Int) `isInstanceOf` typeOf (undefined :: Int -> Bool) +-- > Left "Cannot unify Int and Bool" +-- > +-- > -- We CAN use a term of type 'forall a. a -> Int' as 'Int -> Int' +-- > > typeOf (undefined :: Int -> Int) `isInstanceOf` typeOf (undefined :: ANY -> Int) +-- > Right () +-- > +-- > -- We CAN use a term of type 'forall a b. a -> b' as 'forall a. a -> a' +-- > > typeOf (undefined :: ANY -> ANY) `isInstanceOf` typeOf (undefined :: ANY -> ANY1) +-- > Right () +-- > +-- > -- We CANNOT use a term of type 'forall a. a -> a' as 'forall a b. a -> b' +-- > > typeOf (undefined :: ANY -> ANY1) `isInstanceOf` typeOf (undefined :: ANY -> ANY) +-- > Left "Cannot unify Succ and Zero" +-- > +-- > -- We CAN use a term of type 'forall a. a' as 'forall a. a -> a' +-- > > typeOf (undefined :: ANY -> ANY) `isInstanceOf` typeOf (undefined :: ANY) +-- > Right () +-- > +-- > -- We CANNOT use a term of type 'forall a. a -> a' as 'forall a. a' +-- > > typeOf (undefined :: ANY) `isInstanceOf` typeOf (undefined :: ANY -> ANY) +-- > Left "Cannot unify Skolem and ->" +-- +-- (Admittedly, the quality of the type errors could be improved.) +-- +-- [Examples of funResultTy] +-- +-- > -- Apply fn of type (forall a. a -> a) to arg of type Bool gives Bool +-- > > funResultTy (typeOf (undefined :: ANY -> ANY)) (typeOf (undefined :: Bool)) +-- > Right Bool +-- > +-- > -- Apply fn of type (forall a b. a -> b -> a) to arg of type Bool gives forall a. a -> Bool +-- > > funResultTy (typeOf (undefined :: ANY -> ANY1 -> ANY)) (typeOf (undefined :: Bool)) +-- > Right (ANY -> Bool) -- forall a. a -> Bool +-- > +-- > -- Apply fn of type (forall a. (Bool -> a) -> a) to argument of type (forall a. a -> a) gives Bool +-- > > funResultTy (typeOf (undefined :: (Bool -> ANY) -> ANY)) (typeOf (undefined :: ANY -> ANY)) +-- > Right Bool +-- > +-- > -- Apply fn of type (forall a b. a -> b -> a) to arg of type (forall a. a -> a) gives (forall a b. a -> b -> b) +-- > > funResultTy (typeOf (undefined :: ANY -> ANY1 -> ANY)) (typeOf (undefined :: ANY1 -> ANY1)) +-- > Right (ANY -> ANY1 -> ANY1) +-- > +-- > -- Cannot apply function of type (forall a. (a -> a) -> a -> a) to arg of type (Int -> Bool) +-- > > funResultTy (typeOf (undefined :: (ANY -> ANY) -> (ANY -> ANY))) (typeOf (undefined :: Int -> Bool)) +-- > Left "Cannot unify Int and Bool" module Data.Rank1Typeable - ( Rank1TypeRep - , underlyingTypeRep + ( -- * Basic types + TypeRep , typeOf + , splitTyConApp + , mkTyConApp + , underlyingTypeRep + -- * Operations on type representations + , isInstanceOf + , funResultTy + , TypeError -- * Type variables + , TypVar + , Zero + , Succ + , V0 + , V1 + , V2 + , V3 + , V4 + , V5 + , V6 + , V7 + , V8 + , V9 , ANY , ANY1 , ANY2 @@ -13,95 +91,240 @@ module Data.Rank1Typeable , ANY7 , ANY8 , ANY9 - -- * Internal functions - , occurs - , subst - , unify + -- * Re-exports from Typeable + , Typeable ) where -import Data.Typeable (Typeable, TypeRep, TyCon) -import Control.Arrow ((***)) -import qualified Data.Typeable as Typeable (typeOf, splitTyConApp, mkTyConApp) +import Prelude hiding (succ) +import Control.Arrow ((***), second) +import Control.Monad (void) +import Control.Applicative ((<$>)) +import Data.List (intersperse, isPrefixOf) +import Data.Maybe (fromMaybe) +import Data.Typeable (Typeable, TyCon, tyConName, mkTyCon3) +import Data.Typeable.Internal (listTc, funTc) +import qualified Data.Typeable as Typeable + ( TypeRep + , typeOf + , splitTyConApp + , mkTyConApp + ) -newtype Rank1TypeRep = Rank1TypeRep { underlyingTypeRep :: TypeRep } +-------------------------------------------------------------------------------- +-- The basic type -- +-------------------------------------------------------------------------------- + +-- | Dynamic type representation with support for rank-1 types +newtype TypeRep = TypeRep { + -- | Return the underlying standard ("Data.Typeable") type representation + underlyingTypeRep :: Typeable.TypeRep + } deriving Eq -instance Show Rank1TypeRep where - show = show . underlyingTypeRep - -typeOf :: Typeable a => a -> Rank1TypeRep -typeOf = Rank1TypeRep . Typeable.typeOf - -data ANY deriving Typeable -data ANY1 deriving Typeable -data ANY2 deriving Typeable -data ANY3 deriving Typeable -data ANY4 deriving Typeable -data ANY5 deriving Typeable -data ANY6 deriving Typeable -data ANY7 deriving Typeable -data ANY8 deriving Typeable -data ANY9 deriving Typeable - -tvars :: [Rank1TypeRep] -tvars = - [ typeOf (undefined :: ANY) - , typeOf (undefined :: ANY1) - , typeOf (undefined :: ANY2) - , typeOf (undefined :: ANY3) - , typeOf (undefined :: ANY4) - , typeOf (undefined :: ANY5) - , typeOf (undefined :: ANY6) - , typeOf (undefined :: ANY7) - , typeOf (undefined :: ANY8) - , typeOf (undefined :: ANY9) - ] - -isTVar :: Rank1TypeRep -> Bool -isTVar = flip elem tvars - -splitTyConApp :: Rank1TypeRep -> (TyCon, [Rank1TypeRep]) +-- | The type representation of any 'Typeable' term +typeOf :: Typeable a => a -> TypeRep +typeOf = TypeRep . Typeable.typeOf + +-------------------------------------------------------------------------------- +-- Constructors/destructors (views) -- +-------------------------------------------------------------------------------- + +-- | Split a type representation into the application of +-- a type constructor and its argument +splitTyConApp :: TypeRep -> (TyCon, [TypeRep]) splitTyConApp t = let (c, ts) = Typeable.splitTyConApp (underlyingTypeRep t) - in (c, map Rank1TypeRep ts) + in (c, map TypeRep ts) -mkTyConApp :: TyCon -> [Rank1TypeRep] -> Rank1TypeRep +-- | Inverse of 'splitTyConApp' +mkTyConApp :: TyCon -> [TypeRep] -> TypeRep mkTyConApp c ts - = Rank1TypeRep (Typeable.mkTyConApp c (map underlyingTypeRep ts)) - -occurs :: Rank1TypeRep -> Rank1TypeRep -> Bool -occurs x t - | x == t = True - | otherwise = let (_, ts) = splitTyConApp t - in any (occurs x) ts - -subst :: Rank1TypeRep -> Rank1TypeRep -> Rank1TypeRep -> Rank1TypeRep -subst x t t' - | x == t' = t - | otherwise = let (c, ts) = splitTyConApp t' - in mkTyConApp c (map (subst x t) ts) - -unify :: Monad m => Rank1TypeRep -> Rank1TypeRep -> m [(Rank1TypeRep, Rank1TypeRep)] -unify t1 t2 = unify' [(t1, t2)] - -unify' :: Monad m - => [(Rank1TypeRep, Rank1TypeRep)] - -> m [(Rank1TypeRep, Rank1TypeRep)] -unify' [] = - return [] -unify' ((x, t) : eqs) | isTVar x && not (isTVar t) = - if x `occurs` t - then fail "Occurs check" - else do - s <- unify' (map (subst x t *** subst x t) eqs) - return ((x, t) : s) -unify' ((t, x) : eqs) | isTVar x && not (isTVar t) = - unify' ((x, t) : eqs) -unify' ((x, x') : eqs) | isTVar x && isTVar x' && x == x' = - unify' eqs -unify' ((t1, t2) : eqs) = do - let (c1, ts1) = splitTyConApp t1 - (c2, ts2) = splitTyConApp t2 - if c1 /= c2 - then fail $ "Cannot unify' " ++ show c1 ++ " and " ++ show c2 - else unify' (zip ts1 ts2 ++ eqs) + = TypeRep (Typeable.mkTyConApp c (map underlyingTypeRep ts)) + +isTypVar :: TypeRep -> Maybe Var +isTypVar (splitTyConApp -> (c, [t])) | c == typVar = Just t +isTypVar _ = Nothing + +mkTypVar :: Var -> TypeRep +mkTypVar x = mkTyConApp typVar [x] + +typVar :: TyCon +typVar = let (c, _) = splitTyConApp (typeOf (undefined :: TypVar V0)) in c + +skolem :: TyCon +skolem = let (c, _) = splitTyConApp (typeOf (undefined :: Skolem V0)) in c + +-------------------------------------------------------------------------------- +-- Type variables -- +-------------------------------------------------------------------------------- + +data TypVar a deriving Typeable +data Skolem a deriving Typeable +data Zero deriving Typeable +data Succ a deriving Typeable + +type V0 = Zero +type V1 = Succ V0 +type V2 = Succ V1 +type V3 = Succ V2 +type V4 = Succ V3 +type V5 = Succ V4 +type V6 = Succ V5 +type V7 = Succ V6 +type V8 = Succ V7 +type V9 = Succ V8 + +type ANY = TypVar V0 +type ANY1 = TypVar V1 +type ANY2 = TypVar V2 +type ANY3 = TypVar V3 +type ANY4 = TypVar V4 +type ANY5 = TypVar V5 +type ANY6 = TypVar V6 +type ANY7 = TypVar V7 +type ANY8 = TypVar V8 +type ANY9 = TypVar V9 + +-------------------------------------------------------------------------------- +-- Operations on type reps -- +-------------------------------------------------------------------------------- + +-- | If 'isInstanceOf' fails it returns a type error +type TypeError = String + +-- | @t1 `isInstanceOf` t2@ checks if @t1@ is an instance of @t2@ +isInstanceOf :: TypeRep -> TypeRep -> Either TypeError () +isInstanceOf t1 t2 = void (unify (skolemize t1) t2) + +-- | @funResultTy t1 t2@ is the type of the result when applying a function +-- of type @t1@ to an argument of type @t2@ +funResultTy :: TypeRep -> TypeRep -> Either TypeError TypeRep +funResultTy (splitTyConApp -> (fc, [farg, fres])) x | fc == funTc = do + s <- unify (alphaRename "f" farg) (alphaRename "x" x) + return (normalize (subst s (alphaRename "f" fres))) +funResultTy f _ = + Left $ show f ++ " is not a function" + +-------------------------------------------------------------------------------- +-- Alpha-renaming and normalization -- +-------------------------------------------------------------------------------- + +alphaRename :: String -> TypeRep -> TypeRep +alphaRename prefix (isTypVar -> Just x) = + mkTypVar (mkTyConApp (mkTyCon prefix) [x]) +alphaRename prefix (splitTyConApp -> (c, ts)) = + mkTyConApp c (map (alphaRename prefix) ts) + +tvars :: TypeRep -> [Var] +tvars (isTypVar -> Just x) = [x] +tvars (splitTyConApp -> (_, ts)) = concatMap tvars ts + +normalize :: TypeRep -> TypeRep +normalize t = subst (zip (tvars t) anys) t + where + anys :: [TypeRep] + anys = map mkTypVar (iterate succ zero) + + succ :: TypeRep -> TypeRep + succ = mkTyConApp succTyCon . (:[]) + + zero :: TypeRep + zero = mkTyConApp zeroTyCon [] + +mkTyCon :: String -> TyCon +mkTyCon = mkTyCon3 "rank1typeable" "Data.Rank1Typeable" + +succTyCon :: TyCon +succTyCon = let (c, _) = splitTyConApp (typeOf (undefined :: Succ Zero)) in c + +zeroTyCon :: TyCon +zeroTyCon = let (c, _) = splitTyConApp (typeOf (undefined :: Zero)) in c + +-------------------------------------------------------------------------------- +-- Unification -- +-------------------------------------------------------------------------------- + +type Substitution = [(Var, TypeRep)] +type Equation = (TypeRep, TypeRep) +type Var = TypeRep + +skolemize :: TypeRep -> TypeRep +skolemize (isTypVar -> Just x) = mkTyConApp skolem [x] +skolemize (splitTyConApp -> (c, ts)) = mkTyConApp c (map skolemize ts) + +occurs :: Var -> TypeRep -> Bool +occurs x (isTypVar -> Just x') = x == x' +occurs x (splitTyConApp -> (_, ts)) = any (occurs x) ts + +subst :: Substitution -> TypeRep -> TypeRep +subst s (isTypVar -> Just x) = fromMaybe (mkTypVar x) (lookup x s) +subst s (splitTyConApp -> (c, ts)) = mkTyConApp c (map (subst s) ts) + +unify :: TypeRep + -> TypeRep + -> Either TypeError Substitution +unify = \t1 t2 -> go [] [(t1, t2)] + where + go :: Substitution + -> [Equation] + -> Either TypeError Substitution + go acc [] = + return acc + go acc ((t1, t2) : eqs) | t1 == t2 = -- Note: equality check is fast + go acc eqs + go acc ((isTypVar -> Just x, t) : eqs) = + if x `occurs` t + then Left "Occurs check" + else go ((x, t) : map (second $ subst [(x, t)]) acc) + (map (subst [(x, t)] *** subst [(x, t)]) eqs) + go acc ((t, isTypVar -> Just x) : eqs) = + go acc ((mkTypVar x, t) : eqs) + go acc ((splitTyConApp -> (c1, ts1), splitTyConApp -> (c2, ts2)) : eqs) = + if c1 /= c2 + then Left $ "Cannot unify " ++ show c1 ++ " and " ++ show c2 + else go acc (zip ts1 ts2 ++ eqs) + +-------------------------------------------------------------------------------- +-- Pretty-printing -- +-------------------------------------------------------------------------------- + +instance Show TypeRep where + showsPrec p (splitTyConApp -> (tycon, tys)) = + case tys of + [] -> showsPrec p tycon + [anyIdx -> Just i] | tycon == typVar -> showString "ANY" . showIdx i + [x] | tycon == listTc -> + showChar '[' . shows x . showChar ']' + [a,r] | tycon == funTc -> + showParen (p > 8) $ showsPrec 9 a + . showString " -> " + . showsPrec 8 r + xs | isTupleTyCon tycon -> + showTuple xs + _ -> + showParen (p > 9) $ showsPrec p tycon + . showChar ' ' + . showArgs tys + where + showIdx 0 = showString "" + showIdx i = shows i + +showArgs :: Show a => [a] -> ShowS +showArgs [] = id +showArgs [a] = showsPrec 10 a +showArgs (a:as) = showsPrec 10 a . showString " " . showArgs as + +anyIdx :: TypeRep -> Maybe Int +anyIdx (splitTyConApp -> (c, [])) | c == zeroTyCon = Just 0 +anyIdx (splitTyConApp -> (c, [t])) | c == succTyCon = (+1) <$> anyIdx t +anyIdx _ = Nothing + +showTuple :: [TypeRep] -> ShowS +showTuple args = showChar '(' + . foldr (.) id ( intersperse (showChar ',') + $ map (showsPrec 10) args + ) + . showChar ')' + +isTupleTyCon :: TyCon -> Bool +isTupleTyCon = isPrefixOf "(," . tyConName From 9b884911d491508537dcb5f12bf0941321e1d716 Mon Sep 17 00:00:00 2001 From: Ryan Newton Date: Thu, 9 Aug 2012 16:20:25 -0400 Subject: [PATCH 0165/2357] I had a problem with JustPingC both in gcc 4.4.6 and ICC 12. Tweaked it. --- benchmarks/JustPingC.c | 4 ++-- benchmarks/Makefile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmarks/JustPingC.c b/benchmarks/JustPingC.c index 9391dc8d..8f6416e9 100644 --- a/benchmarks/JustPingC.c +++ b/benchmarks/JustPingC.c @@ -79,7 +79,7 @@ int client(int pings) { printf("starting client\n"); struct addrinfo hints, *res; - int error, client_socket; + int error, client_socket, i; memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_INET; @@ -102,7 +102,7 @@ int client(int pings) { return -1; } - for(int i = 0; i < pings; i++) { + for(i = 0; i < pings; i++) { double timestamp_before = timestamp(); send(client_socket, "ping123", 8, 0); diff --git a/benchmarks/Makefile b/benchmarks/Makefile index 1eae30d0..cd417272 100644 --- a/benchmarks/Makefile +++ b/benchmarks/Makefile @@ -8,7 +8,7 @@ INCLUDES=-i${ROOT}/network-transport/src -i${ROOT}/network-transport-tcp/src # PROF_GHC=-prof -fprof-auto # PROF_EXE=+RTS -pa -RTS -GCC=gcc -std=c99 +GCC=gcc GHC=ghc -rtsopts -XRankNTypes -XScopedTypeVariables -XDeriveDataTypeable -XCPP -XGeneralizedNewtypeDeriving -optP-include -optPcabal_macros.h all: NewTransport.ps Indirection.ps Headers.ps From 257318c2f7066b9be6fc2117867384715090cdc1 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 09:42:35 +0100 Subject: [PATCH 0166/2357] Use the new rank1dynamic package in static --- distributed-static.cabal | 10 +- src/Control/Distributed/Static.hs | 181 ++++++++++-------- .../Distributed/Static/Internal/Dynamic.hs | 121 ------------ .../Distributed/Static/Internal/TypeRep.hs | 33 ---- 4 files changed, 107 insertions(+), 238 deletions(-) delete mode 100644 src/Control/Distributed/Static/Internal/Dynamic.hs delete mode 100644 src/Control/Distributed/Static/Internal/TypeRep.hs diff --git a/distributed-static.cabal b/distributed-static.cabal index 38fdecbe..8c96b5ec 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -13,14 +13,12 @@ Build-Type: Simple Cabal-Version: >=1.8 Library - Exposed-Modules: Control.Distributed.Static, - Control.Distributed.Static.Internal.Dynamic, - Control.Distributed.Static.Internal.TypeRep + Exposed-Modules: Control.Distributed.Static Build-Depends: base ==4.5.*, + rank1dynamic >= 0.1 && < 0.2, containers >= 0.4 && < 0.5, - bytestring >= 0.0 && < 0.11, - binary >= 0.5 && < 0.6, - ghc-prim >= 0.2 && < 0.3 + bytestring >= 0.9 && < 0.11, + binary >= 0.5 && < 0.6 HS-Source-Dirs: src Extensions: DeriveDataTypeable, ScopedTypeVariables, diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 4d0a8da9..0c552b68 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -17,16 +17,16 @@ module Control.Distributed.Static , staticLabel , staticApply , staticDuplicate - , staticTypeOf +-- , staticTypeOf -- * Eliminating static values , RemoteTable , initRemoteTable , registerStatic - , resolveStatic + , unstatic -- * Closures , Closure(Closure) + , unclosure , staticClosure - , resolveClosure -- * Static values , idStatic , composeStatic @@ -50,21 +50,13 @@ module Control.Distributed.Static , closureSplit -- * Re-exports , Dynamic - , toDyn - , unsafeToDyn - , fromDyn + , toDynamic , fromDynamic , dynTypeRep ) where -import Prelude hiding (id, (.)) -import Data.Typeable - ( Typeable - , TypeRep - , typeOf - , funResultTy - ) -import Data.Maybe (fromJust) +import Prelude hiding (id, (.), const, flip, fst, snd) +import qualified Prelude (const, flip, fst, snd) import Data.Binary ( Binary(get, put) , Put @@ -80,26 +72,26 @@ import qualified Data.Map as Map (lookup, empty, insert) import Control.Applicative ((<$>), (<*>)) import Control.Category (Category(id, (.))) import qualified Control.Arrow as Arrow (first, second, (***), app) -import Control.Distributed.Static.Internal.Dynamic - ( Dynamic - , dynApply - , toDyn - , unsafeToDyn - , fromDyn - , dynTypeRep - , fromDynamic - , unsafeCastDyn +import Data.Rank1Dynamic (Dynamic, toDynamic, fromDynamic, dynApply, dynTypeRep) +import Data.Rank1Typeable + ( Typeable + , typeOf + , ANY + , ANY1 + , ANY2 + , ANY3 + , ANY4 + , isInstanceOf ) -import Control.Distributed.Static.Internal.TypeRep (compareTypeRep) -------------------------------------------------------------------------------- -- Introducing static values -- -------------------------------------------------------------------------------- data StaticLabel = - StaticLabel String TypeRep + StaticLabel String | StaticApply StaticLabel StaticLabel - | StaticDuplicate StaticLabel TypeRep + | StaticDuplicate StaticLabel deriving (Typeable, Show) -- | A static value. Static is opaque; see 'staticLabel', 'staticApply', @@ -108,59 +100,64 @@ newtype Static a = Static StaticLabel deriving (Typeable, Show) instance Typeable a => Binary (Static a) where - put (Static label) = putStaticLabel label + put (Static label) = putStaticLabel label >> put (typeOf (undefined :: a)) get = do - label <- getStaticLabel - if typeOfStaticLabel label `compareTypeRep` typeOf (undefined :: a) - then return $ Static label - else fail "Static.get: type error" + label <- getStaticLabel + typeRep <- get + case typeOf (undefined :: a) `isInstanceOf` typeRep of + Left err -> fail $ "Static.get: type error: " ++ err + Right () -> return (Static label) -- We don't want StaticLabel to be its own Binary instance putStaticLabel :: StaticLabel -> Put -putStaticLabel (StaticLabel string typ) = - putWord8 0 >> put string >> put typ +putStaticLabel (StaticLabel string) = + putWord8 0 >> put string putStaticLabel (StaticApply label1 label2) = putWord8 1 >> putStaticLabel label1 >> putStaticLabel label2 -putStaticLabel (StaticDuplicate label typ) = - putWord8 2 >> putStaticLabel label >> put typ +putStaticLabel (StaticDuplicate label) = + putWord8 2 >> putStaticLabel label getStaticLabel :: Get StaticLabel getStaticLabel = do header <- getWord8 case header of - 0 -> StaticLabel <$> get <*> get + 0 -> StaticLabel <$> get 1 -> StaticApply <$> getStaticLabel <*> getStaticLabel - 2 -> StaticDuplicate <$> getStaticLabel <*> get + 2 -> StaticDuplicate <$> getStaticLabel _ -> fail "StaticLabel.get: invalid" - + -- | Create a primitive static value. -- -- It is the responsibility of the client code to make sure the corresponding -- entry in the 'RemoteTable' has the appropriate type. -staticLabel :: forall a. Typeable a => String -> Static a -staticLabel label = Static (StaticLabel label (typeOf (undefined :: a))) +staticLabel :: forall a. String -> Static a +staticLabel = Static . StaticLabel -- | Apply two static values staticApply :: Static (a -> b) -> Static a -> Static b staticApply (Static f) (Static x) = Static (StaticApply f x) -- | Co-monadic 'duplicate' for static values -staticDuplicate :: forall a. Typeable a => Static a -> Static (Static a) -staticDuplicate (Static x) = - Static (StaticDuplicate x (typeOf (undefined :: Static a))) +staticDuplicate :: forall a. Static a -> Static (Static a) +staticDuplicate (Static x) = Static (StaticDuplicate x) +{- -- | @staticTypeOf (x :: a)@ mimicks @static (undefined :: a)@ -- even if -- @x@ is not static, the type of @x@ always is. staticTypeOf :: forall a. Typeable a => a -> Static a staticTypeOf _ = Static (StaticLabel "$undefined" (typeOf (undefined :: a))) typeOfStaticLabel :: StaticLabel -> TypeRep +typeOfStaticLabel = undefined +{- typeOfStaticLabel (StaticLabel _ typ) = typ typeOfStaticLabel (StaticApply f x) = fromJust $ funResultTy (typeOfStaticLabel f) (typeOfStaticLabel x) typeOfStaticLabel (StaticDuplicate _ typ) = typ +-} +-} -------------------------------------------------------------------------------- -- Eliminating static values -- @@ -172,39 +169,51 @@ newtype RemoteTable = RemoteTable (Map String Dynamic) -- | Initial remote table initRemoteTable :: RemoteTable initRemoteTable = - registerStatic "$id" (unsafeToDyn identity) - . registerStatic "$compose" (unsafeToDyn compose) - . registerStatic "$const" (unsafeToDyn const) - . registerStatic "$flip" (unsafeToDyn flip) - . registerStatic "$fst" (unsafeToDyn fst) - . registerStatic "$snd" (unsafeToDyn snd) - . registerStatic "$first" (unsafeToDyn first) - . registerStatic "$second" (unsafeToDyn second) - . registerStatic "$split" (unsafeToDyn split) - . registerStatic "$unit" (toDyn ()) - . registerStatic "$app" (unsafeToDyn app) - . registerStatic "$decodeEnvPair" (toDyn decodeEnvPair) + registerStatic "$id" (toDynamic identity) + . registerStatic "$compose" (toDynamic compose) + . registerStatic "$const" (toDynamic const) + . registerStatic "$flip" (toDynamic flip) + . registerStatic "$fst" (toDynamic fst) + . registerStatic "$snd" (toDynamic snd) + . registerStatic "$first" (toDynamic first) + . registerStatic "$second" (toDynamic second) + . registerStatic "$split" (toDynamic split) + . registerStatic "$unit" (toDynamic ()) + . registerStatic "$app" (toDynamic app) + . registerStatic "$decodeEnvPair" (toDynamic decodeEnvPair) $ RemoteTable Map.empty where - identity :: a -> a + identity :: ANY -> ANY identity = id - compose :: (b -> c) -> (a -> b) -> a -> c + compose :: (ANY2 -> ANY3) -> (ANY1 -> ANY2) -> ANY1 -> ANY3 compose = (.) + + const :: ANY1 -> ANY2 -> ANY1 + const = Prelude.const - first :: (a -> b) -> (a, c) -> (b, c) + flip :: (ANY1 -> ANY2 -> ANY3) -> ANY2 -> ANY1 -> ANY3 + flip = Prelude.flip + + fst :: (ANY1, ANY2) -> ANY1 + fst = Prelude.fst + + snd :: (ANY1, ANY2) -> ANY2 + snd = Prelude.snd + + first :: (ANY1 -> ANY2) -> (ANY1, ANY3) -> (ANY2, ANY3) first = Arrow.first - second :: (a -> b) -> (c, a) -> (c, b) + second :: (ANY1 -> ANY2) -> (ANY3, ANY1) -> (ANY3, ANY2) second = Arrow.second - split :: (a -> b) -> (a' -> b') -> (a, a') -> (b, b') + split :: (ANY1 -> ANY3) -> (ANY2 -> ANY4) -> (ANY1, ANY2) -> (ANY3, ANY4) split = (Arrow.***) decodeEnvPair :: ByteString -> (ByteString, ByteString) decodeEnvPair = decode - app :: (a -> b, a) -> b + app :: (ANY1 -> ANY2, ANY1) -> ANY2 app = Arrow.app -- | Register a static label @@ -212,16 +221,32 @@ registerStatic :: String -> Dynamic -> RemoteTable -> RemoteTable registerStatic label dyn (RemoteTable rtable) = RemoteTable (Map.insert label dyn rtable) --- | Resolve a Static value. -resolveStatic :: RemoteTable -> Static a -> Maybe Dynamic -resolveStatic (RemoteTable rtable) (Static (StaticLabel string typ)) = do - unsafeCastDyn (const typ) <$> Map.lookup string rtable -resolveStatic rtable (Static (StaticApply static1 static2)) = do - f <- resolveStatic rtable (Static static1) - x <- resolveStatic rtable (Static static2) - f `dynApply` x -resolveStatic _rtable (Static (StaticDuplicate static typ)) = - return . unsafeCastDyn (const typ) $ unsafeToDyn (Static static) +-- Pseudo-type: RemoteTable -> Static a -> a +resolveStaticLabel :: RemoteTable -> StaticLabel -> Maybe Dynamic +resolveStaticLabel (RemoteTable rtable) (StaticLabel label) = + Map.lookup label rtable +resolveStaticLabel rtable (StaticApply label1 label2) = do + f <- resolveStaticLabel rtable label1 + x <- resolveStaticLabel rtable label2 + case f `dynApply` x of + Left _err -> Nothing + Right y -> Just y +resolveStaticLabel rtable (StaticDuplicate label) = do + x <- resolveStaticLabel rtable label -- Resolve only to get type info + case toDynamic mkStatic `dynApply` x of + Left _err -> Nothing + Right y -> Just y + where + mkStatic :: ANY -> Static ANY + mkStatic _ = Static label + +unstatic :: Typeable a => RemoteTable -> Static a -> Maybe a +unstatic rtable (Static static) = + case resolveStaticLabel rtable static of + Nothing -> Nothing + Just dyn -> case fromDynamic dyn of + Left _err -> Nothing + Right x -> Just x -------------------------------------------------------------------------------- -- Closures -- @@ -235,16 +260,16 @@ instance Typeable a => Binary (Closure a) where put (Closure static env) = put static >> put env get = Closure <$> get <*> get +-- | Resolve a closure +unclosure :: Typeable a => RemoteTable -> Closure a -> Maybe a +unclosure rtable (Closure static env) = do + f <- unstatic rtable static + return (f env) + -- | Convert a static value into a closure. staticClosure :: forall a. Typeable a => Static a -> Closure a staticClosure static = Closure (staticConst static) empty --- | Resolve a Closure -resolveClosure :: RemoteTable -> Static a -> ByteString -> Maybe Dynamic -resolveClosure rtable static env = do - decoder <- resolveStatic rtable static - decoder `dynApply` toDyn env - -------------------------------------------------------------------------------- -- Predefined static values -- -------------------------------------------------------------------------------- @@ -348,5 +373,5 @@ closureCompose :: (Typeable a, Typeable b, Typeable c) closureCompose g f = composeStatic `closureApplyStatic` g `closureApply` f closureSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') - => Closure (a -> b) -> Closure (a' -> b') -> Closure ((a, a') -> (b, b')) + => Closure (a -> b) -> Closure (a' -> b') -> Closure ((a, a') -> (b, b')) closureSplit f g = splitStatic `closureApplyStatic` f `closureApply` g diff --git a/src/Control/Distributed/Static/Internal/Dynamic.hs b/src/Control/Distributed/Static/Internal/Dynamic.hs deleted file mode 100644 index 14000956..00000000 --- a/src/Control/Distributed/Static/Internal/Dynamic.hs +++ /dev/null @@ -1,121 +0,0 @@ -module Control.Distributed.Static.Internal.Dynamic - ( -- * The @Dynamic@ type - Dynamic - -- * Converting to and from @Dynamic@ - , toDyn - , fromDyn - , fromDynamic - -- * Applying functions of dynamic type - , dynApply - , dynApp - , dynTypeRep - -- * Additions - , unsafeToDyn - , unsafeCastDyn - ) where - -import Control.Exception (Exception) -import qualified GHC.Prim as GHC (Any) -import Data.Typeable (Typeable, TypeRep, typeOf, funResultTy) -import Unsafe.Coerce (unsafeCoerce) - -------------------------------------------------------------- --- --- The type Dynamic --- -------------------------------------------------------------- - -{-| - A value of type 'Dynamic' is an object encapsulated together with its type. - - A 'Dynamic' may only represent a monomorphic value; an attempt to - create a value of type 'Dynamic' from a polymorphically-typed - expression will result in an ambiguity error (see 'toDyn'). - - 'Show'ing a value of type 'Dynamic' returns a pretty-printed representation - of the object\'s type; useful for debugging. --} -data Dynamic = Dynamic TypeRep Obj - deriving Typeable - -instance Show Dynamic where - -- the instance just prints the type representation. - showsPrec _ (Dynamic t _) = - showString "<<" . - showsPrec 0 t . - showString ">>" - -instance Exception Dynamic - --- Use GHC's primitive 'Any' type to hold the dynamically typed value. --- --- In GHC's new eval/apply execution model this type must not look --- like a data type. If it did, GHC would use the constructor convention --- when evaluating it, and this will go wrong if the object is really a --- function. Using Any forces GHC to use --- a fallback convention for evaluating it that works for all types. -type Obj = GHC.Any - --- | Converts an arbitrary value into an object of type 'Dynamic'. --- --- The type of the object must be an instance of 'Typeable', which --- ensures that only monomorphically-typed objects may be converted to --- 'Dynamic'. To convert a polymorphic object into 'Dynamic', give it --- a monomorphic type signature. For example: --- --- > toDyn (id :: Int -> Int) --- -toDyn :: Typeable a => a -> Dynamic -toDyn v = Dynamic (typeOf v) (unsafeCoerce v) - --- | Converts a 'Dynamic' object back into an ordinary Haskell value of --- the correct type. See also 'fromDynamic'. -fromDyn :: Typeable a - => Dynamic -- ^ the dynamically-typed object - -> a -- ^ a default value - -> a -- ^ returns: the value of the first argument, if - -- it has the correct type, otherwise the value of - -- the second argument. -fromDyn (Dynamic t v) def - | typeOf def == t = unsafeCoerce v - | otherwise = def - --- | Converts a 'Dynamic' object back into an ordinary Haskell value of --- the correct type. See also 'fromDyn'. -fromDynamic - :: Typeable a - => Dynamic -- ^ the dynamically-typed object - -> Maybe a -- ^ returns: @'Just' a@, if the dynamically-typed - -- object has the correct type (and @a@ is its value), - -- or 'Nothing' otherwise. -fromDynamic (Dynamic t v) = - case unsafeCoerce v of - r | t == typeOf r -> Just r - | otherwise -> Nothing - --- (f::(a->b)) `dynApply` (x::a) = (f a)::b -dynApply :: Dynamic -> Dynamic -> Maybe Dynamic -dynApply (Dynamic t1 f) (Dynamic t2 x) = - case funResultTy t1 t2 of - Just t3 -> Just (Dynamic t3 ((unsafeCoerce f) x)) - Nothing -> Nothing - -dynApp :: Dynamic -> Dynamic -> Dynamic -dynApp f x = case dynApply f x of - Just r -> r - Nothing -> error ("Type error in dynamic application.\n" ++ - "Can't apply function " ++ show f ++ - " to argument " ++ show x) - -dynTypeRep :: Dynamic -> TypeRep -dynTypeRep (Dynamic tr _) = tr - --------------------------------------------------------------------------------- --- Additions -- --------------------------------------------------------------------------------- - -unsafeToDyn :: a -> Dynamic -unsafeToDyn x = Dynamic (error "No type rep") (unsafeCoerce x) - -unsafeCastDyn :: (TypeRep -> TypeRep) -> Dynamic -> Dynamic -unsafeCastDyn f (Dynamic t x) = Dynamic (f t) x diff --git a/src/Control/Distributed/Static/Internal/TypeRep.hs b/src/Control/Distributed/Static/Internal/TypeRep.hs deleted file mode 100644 index 9d8da5a2..00000000 --- a/src/Control/Distributed/Static/Internal/TypeRep.hs +++ /dev/null @@ -1,33 +0,0 @@ --- | 'Binary' instances for 'TypeRep', and 'TypeRep' equality (bug workaround) -{-# OPTIONS_GHC -fno-warn-orphans #-} -module Control.Distributed.Static.Internal.TypeRep (compareTypeRep) where - -import Control.Applicative ((<$>), (<*>)) -import Data.Binary (Binary(get, put)) -import Data.Typeable.Internal (TypeRep(..), TyCon(..)) -import GHC.Fingerprint.Type (Fingerprint(..)) - -instance Binary Fingerprint where - put (Fingerprint hi lo) = put hi >> put lo - get = Fingerprint <$> get <*> get - -instance Binary TypeRep where - put (TypeRep fp tyCon ts) = put fp >> put tyCon >> put ts - get = TypeRep <$> get <*> get <*> get - -instance Binary TyCon where - put (TyCon hash package modul name) = put hash >> put package >> put modul >> put name - get = TyCon <$> get <*> get <*> get <*> get - --- | Compare two type representations --- --- For base >= 4.6 this compares fingerprints, but older versions of base --- have a bug in the fingerprint construction --- () -compareTypeRep :: TypeRep -> TypeRep -> Bool -#if ! MIN_VERSION_base(4,6,0) -compareTypeRep (TypeRep _ con ts) (TypeRep _ con' ts') - = con == con' && all (uncurry compareTypeRep) (zip ts ts') -#else -compareTypeRep = (==) -#endif From 0e39efd1dabc7c9929d576dc51f26422cca95caf Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 09:42:35 +0100 Subject: [PATCH 0167/2357] Use the new rank1dynamic package in static --- rank1dynamic.cabal | 6 ++++-- src/Data/Rank1Typeable.hs | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index f9f77927..c7010c10 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -18,9 +18,11 @@ Library Exposed-Modules: Data.Rank1Dynamic, Data.Rank1Typeable Build-Depends: base ==4.5.*, - ghc-prim >= 0.2 && < 0.3 + ghc-prim >= 0.2 && < 0.3, + binary >= 0.5 && < 0.6 HS-Source-Dirs: src GHC-Options: -Wall Extensions: EmptyDataDecls, DeriveDataTypeable, - ViewPatterns + ViewPatterns, + CPP diff --git a/src/Data/Rank1Typeable.hs b/src/Data/Rank1Typeable.hs index 6b5a096c..fff8148c 100644 --- a/src/Data/Rank1Typeable.hs +++ b/src/Data/Rank1Typeable.hs @@ -101,8 +101,10 @@ import Control.Monad (void) import Control.Applicative ((<$>)) import Data.List (intersperse, isPrefixOf) import Data.Maybe (fromMaybe) -import Data.Typeable (Typeable, TyCon, tyConName, mkTyCon3) -import Data.Typeable.Internal (listTc, funTc) +import Data.Typeable (Typeable, tyConName, mkTyCon3) +import Data.Typeable.Internal (listTc, funTc, TyCon(TyCon)) +import Data.Binary (Binary(get, put)) +import GHC.Fingerprint.Type (Fingerprint(..)) import qualified Data.Typeable as Typeable ( TypeRep , typeOf @@ -119,7 +121,37 @@ newtype TypeRep = TypeRep { -- | Return the underlying standard ("Data.Typeable") type representation underlyingTypeRep :: Typeable.TypeRep } - deriving Eq + +-- | Compare two type representations +-- +-- For base >= 4.6 this compares fingerprints, but older versions of base +-- have a bug in the fingerprint construction +-- () +instance Eq TypeRep where +#if ! MIN_VERSION_base(4,6,0) + (splitTyConApp -> (c1, ts1)) == (splitTyConApp -> (c2, ts2)) = + c1 == c2 && all (uncurry (==)) (zip ts1 ts2) +#else + t1 == t2 = underlyingTypeRep t1 == underlyingTypeRep t2 +#endif + +-- Binary instance for 'TypeRep', avoiding orphan instances +instance Binary TypeRep where + put (splitTyConApp -> (TyCon (Fingerprint hi lo) package modul name, ts)) = do + put hi + put lo + put package + put modul + put name + put ts + get = do + hi <- get + lo <- get + package <- get + modul <- get + name <- get + ts <- get + return $ mkTyConApp (TyCon (Fingerprint hi lo) package modul name) ts -- | The type representation of any 'Typeable' term typeOf :: Typeable a => a -> TypeRep From dd7e7bda6b9ebbfdbc6b54a9df67883cb4e8eaaf Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 10:16:42 +0100 Subject: [PATCH 0168/2357] Clean up static --- src/Control/Distributed/Static.hs | 137 +++++------------------------- 1 file changed, 20 insertions(+), 117 deletions(-) diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 0c552b68..0941ede1 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -17,7 +17,6 @@ module Control.Distributed.Static , staticLabel , staticApply , staticDuplicate --- , staticTypeOf -- * Eliminating static values , RemoteTable , initRemoteTable @@ -27,18 +26,6 @@ module Control.Distributed.Static , Closure(Closure) , unclosure , staticClosure - -- * Static values - , idStatic - , composeStatic - , constStatic - , flipStatic - , fstStatic - , sndStatic - , firstStatic - , secondStatic - , splitStatic - , unitStatic - , appStatic -- * Combinators on static values , staticCompose , staticSplit @@ -49,14 +36,13 @@ module Control.Distributed.Static , closureCompose , closureSplit -- * Re-exports + , Typeable , Dynamic , toDynamic - , fromDynamic - , dynTypeRep ) where -import Prelude hiding (id, (.), const, flip, fst, snd) -import qualified Prelude (const, flip, fst, snd) +import Prelude hiding (const, fst, snd) +import qualified Prelude (const, fst, snd) import Data.Binary ( Binary(get, put) , Put @@ -70,9 +56,8 @@ import Data.ByteString.Lazy (ByteString, empty) import Data.Map (Map) import qualified Data.Map as Map (lookup, empty, insert) import Control.Applicative ((<$>), (<*>)) -import Control.Category (Category(id, (.))) -import qualified Control.Arrow as Arrow (first, second, (***), app) -import Data.Rank1Dynamic (Dynamic, toDynamic, fromDynamic, dynApply, dynTypeRep) +import qualified Control.Arrow as Arrow ((***), app) +import Data.Rank1Dynamic (Dynamic, toDynamic, fromDynamic, dynApply) import Data.Rank1Typeable ( Typeable , typeOf @@ -141,24 +126,6 @@ staticApply (Static f) (Static x) = Static (StaticApply f x) staticDuplicate :: forall a. Static a -> Static (Static a) staticDuplicate (Static x) = Static (StaticDuplicate x) -{- --- | @staticTypeOf (x :: a)@ mimicks @static (undefined :: a)@ -- even if --- @x@ is not static, the type of @x@ always is. -staticTypeOf :: forall a. Typeable a => a -> Static a -staticTypeOf _ = Static (StaticLabel "$undefined" (typeOf (undefined :: a))) - -typeOfStaticLabel :: StaticLabel -> TypeRep -typeOfStaticLabel = undefined -{- -typeOfStaticLabel (StaticLabel _ typ) - = typ -typeOfStaticLabel (StaticApply f x) - = fromJust $ funResultTy (typeOfStaticLabel f) (typeOfStaticLabel x) -typeOfStaticLabel (StaticDuplicate _ typ) - = typ --} --} - -------------------------------------------------------------------------------- -- Eliminating static values -- -------------------------------------------------------------------------------- @@ -169,84 +136,54 @@ newtype RemoteTable = RemoteTable (Map String Dynamic) -- | Initial remote table initRemoteTable :: RemoteTable initRemoteTable = - registerStatic "$id" (toDynamic identity) - . registerStatic "$compose" (toDynamic compose) + registerStatic "$compose" (toDynamic compose) . registerStatic "$const" (toDynamic const) - . registerStatic "$flip" (toDynamic flip) - . registerStatic "$fst" (toDynamic fst) - . registerStatic "$snd" (toDynamic snd) - . registerStatic "$first" (toDynamic first) - . registerStatic "$second" (toDynamic second) . registerStatic "$split" (toDynamic split) - . registerStatic "$unit" (toDynamic ()) . registerStatic "$app" (toDynamic app) . registerStatic "$decodeEnvPair" (toDynamic decodeEnvPair) $ RemoteTable Map.empty where - identity :: ANY -> ANY - identity = id - compose :: (ANY2 -> ANY3) -> (ANY1 -> ANY2) -> ANY1 -> ANY3 compose = (.) const :: ANY1 -> ANY2 -> ANY1 const = Prelude.const - flip :: (ANY1 -> ANY2 -> ANY3) -> ANY2 -> ANY1 -> ANY3 - flip = Prelude.flip - - fst :: (ANY1, ANY2) -> ANY1 - fst = Prelude.fst - - snd :: (ANY1, ANY2) -> ANY2 - snd = Prelude.snd - - first :: (ANY1 -> ANY2) -> (ANY1, ANY3) -> (ANY2, ANY3) - first = Arrow.first - - second :: (ANY1 -> ANY2) -> (ANY3, ANY1) -> (ANY3, ANY2) - second = Arrow.second - split :: (ANY1 -> ANY3) -> (ANY2 -> ANY4) -> (ANY1, ANY2) -> (ANY3, ANY4) split = (Arrow.***) - decodeEnvPair :: ByteString -> (ByteString, ByteString) - decodeEnvPair = decode - app :: (ANY1 -> ANY2, ANY1) -> ANY2 app = Arrow.app + decodeEnvPair :: ByteString -> (ByteString, ByteString) + decodeEnvPair = decode + -- | Register a static label registerStatic :: String -> Dynamic -> RemoteTable -> RemoteTable registerStatic label dyn (RemoteTable rtable) = RemoteTable (Map.insert label dyn rtable) -- Pseudo-type: RemoteTable -> Static a -> a -resolveStaticLabel :: RemoteTable -> StaticLabel -> Maybe Dynamic +resolveStaticLabel :: RemoteTable -> StaticLabel -> Either String Dynamic resolveStaticLabel (RemoteTable rtable) (StaticLabel label) = - Map.lookup label rtable + case Map.lookup label rtable of + Nothing -> Left $ "Invalid static label '" ++ label ++ "'" + Just d -> Right d resolveStaticLabel rtable (StaticApply label1 label2) = do f <- resolveStaticLabel rtable label1 x <- resolveStaticLabel rtable label2 - case f `dynApply` x of - Left _err -> Nothing - Right y -> Just y + f `dynApply` x resolveStaticLabel rtable (StaticDuplicate label) = do x <- resolveStaticLabel rtable label -- Resolve only to get type info - case toDynamic mkStatic `dynApply` x of - Left _err -> Nothing - Right y -> Just y + toDynamic mkStatic `dynApply` x where mkStatic :: ANY -> Static ANY mkStatic _ = Static label -unstatic :: Typeable a => RemoteTable -> Static a -> Maybe a -unstatic rtable (Static static) = - case resolveStaticLabel rtable static of - Nothing -> Nothing - Just dyn -> case fromDynamic dyn of - Left _err -> Nothing - Right x -> Just x +unstatic :: Typeable a => RemoteTable -> Static a -> Either String a +unstatic rtable (Static static) = do + dyn <- resolveStaticLabel rtable static + fromDynamic dyn -------------------------------------------------------------------------------- -- Closures -- @@ -261,7 +198,7 @@ instance Typeable a => Binary (Closure a) where get = Closure <$> get <*> get -- | Resolve a closure -unclosure :: Typeable a => RemoteTable -> Closure a -> Maybe a +unclosure :: Typeable a => RemoteTable -> Closure a -> Either String a unclosure rtable (Closure static env) = do f <- unstatic rtable static return (f env) @@ -274,11 +211,6 @@ staticClosure static = Closure (staticConst static) empty -- Predefined static values -- -------------------------------------------------------------------------------- --- | Static version of 'id' -idStatic :: (Typeable a) - => Static (a -> a) -idStatic = staticLabel "$id" - -- | Static version of ('Prelude..') composeStatic :: (Typeable a, Typeable b, Typeable c) => Static ((b -> c) -> (a -> b) -> a -> c) @@ -289,40 +221,11 @@ constStatic :: (Typeable a, Typeable b) => Static (a -> b -> a) constStatic = staticLabel "$const" --- | Static version of 'flip' -flipStatic :: (Typeable a, Typeable b, Typeable c) - => Static ((a -> b -> c) -> b -> a -> c) -flipStatic = staticLabel "$flip" - --- | Static version of 'fst' -fstStatic :: (Typeable a, Typeable b) - => Static ((a, b) -> a) -fstStatic = staticLabel "$fst" - --- | Static version of 'snd' -sndStatic :: (Typeable a, Typeable b) - => Static ((a, b) -> b) -sndStatic = staticLabel "$snd" - --- | Static version of 'Arrow.first' -firstStatic :: (Typeable a, Typeable b, Typeable c) - => Static ((a -> b) -> (a, c) -> (b, c)) -firstStatic = staticLabel "$first" - --- | Static version of 'Arrow.second' -secondStatic :: (Typeable a, Typeable b, Typeable c) - => Static ((a -> b) -> (c, a) -> (c, b)) -secondStatic = staticLabel "$second" - -- | Static version of ('Arrow.***') splitStatic :: (Typeable a, Typeable a', Typeable b, Typeable b') => Static ((a -> b) -> (a' -> b') -> (a, a') -> (b, b')) splitStatic = staticLabel "$split" --- | Static version of @()@ -unitStatic :: Static () -unitStatic = staticLabel "$unit" - -- | Static version of 'Arrow.app' appStatic :: (Typeable a, Typeable b) => Static ((a -> b, a) -> b) From 4f2e43f51cb7a1f075ba5bf93ebc2b914d435741 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 11:55:15 +0100 Subject: [PATCH 0169/2357] Clean up and document Static better --- distributed-static.cabal | 22 ++- src/Control/Distributed/Static.hs | 259 +++++++++++++++++++++++------- 2 files changed, 220 insertions(+), 61 deletions(-) diff --git a/distributed-static.cabal b/distributed-static.cabal index 8c96b5ec..ac808300 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -1,7 +1,19 @@ Name: distributed-static Version: 0.1.0.0 -Synopsis: Workaround the absence of "static" in ghc --- description: +Synopsis: Compositional, type-safe, polymorphic static values and closures +Description: /Towards Haskell in the Cloud/ (Epstein et al, Haskell + Symposium 2011) introduces the concept of /static/ values: + values that are known at compile time. In a distributed + setting where all nodes are running the same executable, + static values can be serialized simply by transmitting a + code pointer to the value. This however requires special + compiler support, which is not yet available in ghc. We + can mimick the behaviour by keeping an explicit mapping + ('RemoteTable') from labels to values (and making sure + that all distributed nodes are using the same + 'RemoteTable'). In this module we implement this mimickry + and various extensions: type safety (including for + polymorphic static values) and compositionality. Homepage: http://www.github.com/haskell-distributed/distributed-process License: BSD3 License-File: LICENSE @@ -14,14 +26,12 @@ Cabal-Version: >=1.8 Library Exposed-Modules: Control.Distributed.Static - Build-Depends: base ==4.5.*, + Build-Depends: base >= 4 && < 5, rank1dynamic >= 0.1 && < 0.2, containers >= 0.4 && < 0.5, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6 HS-Source-Dirs: src Extensions: DeriveDataTypeable, - ScopedTypeVariables, - RankNTypes, - CPP + ScopedTypeVariables GHC-Options: -Wall diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 0941ede1..21b9aa58 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -1,44 +1,201 @@ --- | /Towards Haskell in the Cloud/ introduces a new typing construct called --- @static@. Unfortunately, ghc does not yet include support for @static@. In --- this module we provide a workaround, which at the same time also extends --- the functionality of @static@ as described in the paper and makes it more --- expressive. +-- | /Towards Haskell in the Cloud/ (Epstein et al, Haskell Symposium 2011) +-- introduces the concept of /static/ values: values that are known at compile +-- time. In a distributed setting where all nodes are running the same +-- executable, static values can be serialized simply by transmitting a code +-- pointer to the value. This however requires special compiler support, which +-- is not yet available in ghc. We can mimick the behaviour by keeping an +-- explicit mapping ('RemoteTable') from labels to values (and making sure that +-- all distributed nodes are using the same 'RemoteTable'). In this module +-- we implement this mimickry and various extensions. +-- +-- [Dynamic type checking] +-- +-- The paper stipulates that 'Static' values should have a free 'Binary' +-- instance: +-- +-- > instance Binary (Static a) +-- +-- This however is not (runtime) type safe: for instance, what would be the +-- behaviour of +-- +-- > f :: Static Int -> Static Bool +-- > f = decode . encode +-- +-- For this reason we work only with 'Typeable' terms in this module, and +-- implement runtime checks +-- +-- > instance Typeable a => Binary (Static a) +-- +-- The above function 'f' typechecks but throws an exception if executed. The +-- type representation we use, however, is not the standard +-- 'Data.Typeable.TypeRep' from "Data.Typeable" but +-- 'Data.Rank1Typeable.TypeRep' from "Data.Rank1Typeable". This means that we +-- can represent polymorphic static values (see below for an example). +-- +-- Since the runtime mapping ('RemoteTable') contains values of different types, +-- it maps labels ('String's) to 'Data.Rank1Dynamic.Dynamic' values. Again, we +-- use the implementation from "Data.Rank1Dynamic" so that we can store +-- polymorphic dynamic values. +-- +-- [Compositionality] +-- +-- Static values as described in the paper are not compositional: there is no +-- way to combine two static values and get a static value out of it. This +-- makes sense when interpreting static strictly as /known at compile time/, +-- but it severely limits expressiveness. However, the main motivation for +-- 'static' is not that they are known at compile time but rather that +-- /they provide a free/ 'Binary' /instance/. We therefore provide two basic +-- constructors for 'Static' values: +-- +-- > staticLabel :: String -> Static a +-- > staticApply :: Static (a -> b) -> Static a -> Static b +-- +-- The first constructor refers to a label in a 'RemoteTable'. The second +-- allows to apply a static function to a static argument, and makes 'Static' +-- compositional: once we have 'staticApply' we can implement numerous derived +-- combinators on 'Static' values (we define a few in this module; see +-- 'staticCompose', 'staticSplit', and 'staticConst'). +-- +-- [Closures] +-- +-- Closures in functional programming arise when we partially apply a function. +-- A closure is a code pointer together with a runtime data structure that +-- represents the value of the free variables of the function. A 'Closure' +-- represents these closures explicitly so that they can be serialized: +-- +-- > data Closure a = Closure (Static (ByteString -> a)) ByteString +-- +-- See /Towards Haskell in the Cloud/ for the rationale behind representing +-- the function closure environment in serialized ('ByteString') form. Any +-- static value can trivially be turned into a 'Closure' ('staticClosure'). +-- Moreover, since 'Static' is now compositional, we can also define derived +-- operators on 'Closure' values ('closureApplyStatic', 'closureApply', +-- 'closureCompose', 'closureSplit'). +-- +-- [Monomorphic example] +-- +-- Suppose we are working in the context of some distributed environment, with +-- a monadic type 'Process' representing processes, 'NodeId' representing node +-- addresses and 'ProcessId' representing process addresses. Suppose further +-- that we have a primitive -- --- The main idea (already discussed in /Towards Haskell in the Cloud/, section --- /Faking It/) is to maintain a runtime mapping from labels (strings) to --- values. A basic static value (introduced using 'staticLabel') then is an --- index into this runtime mapping. It is the responsibility of the client code --- to make sure that the corresponding value in the 'RemoteTable' matches the --- type of the label, although we do runtime type checks where possible. For --- this reason, we only work with 'Typeable' values in this module. +-- > sendInt :: ProcessId -> Int -> Process () +-- +-- We might want to define +-- +-- > sendIntClosure :: ProcessId -> Closure (Int -> Process ()) +-- +-- In order to do that, we need a static version of 'send', and a static +-- decoder for 'ProcessId': +-- +-- > sendIntStatic :: Static (ProcessId -> Int -> Process ()) +-- > sendIntStatic = staticLabel "$send" +-- +-- > decodeProcessIdStatic :: Static (ByteString -> Int) +-- > decodeProcessIdStatic = staticLabel "$decodeProcessId" +-- +-- where of course we have to make sure to use an appropriate 'RemoteTable': +-- +-- > rtable :: RemoteTable +-- > rtable = registerStatic "$send" (toDynamic sendInt) +-- > . registerStatic "$decodeProcessId" (toDynamic (decode :: ByteString -> Int)) +-- > $ initRemoteTable +-- +-- We can now define 'sendIntClosure': +-- +-- > sendIntClosure :: ProcessId -> Closure (Int -> Process ()) +-- > sendIntClosure pid = Closure decoder (encode pid) +-- > where +-- > decoder :: Static (ByteString -> Int -> Process ()) +-- > decoder = sendIntStatic `staticCompose` decodeProcessIdStatic +-- +-- [Polymorphic example] +-- +-- Suppose we wanted to define a primitive +-- +-- > sendIntResult :: ProcessId -> Closure (Process Int) -> Closure (Process ()) +-- +-- which turns a process that computes an integer into a process that computes +-- the integer and then sends it someplace else. +-- +-- We can define +-- +-- > bindStatic :: (Typeable a, Typeable b) => Static (Process a -> (a -> Process b) -> Process b) +-- > bindStatic = staticLabel "$bind" +-- +-- provided that we register this label: +-- +-- > rtable :: RemoteTable +-- > rtable = ... +-- > . registerStatic "$bind" ((>>=) :: Process ANY1 -> (ANY1 -> Process ANY2) -> Process ANY2) +-- > $ initRemoteTable +-- +-- (Note that we are using the special 'Data.Rank1Typeable.ANY1' and +-- 'Data.Rank1Typeable.ANY2' types from "Data.Rank1Typeable" to represent this +-- polymorphic value.) Once we have a static bind we can define +-- +-- > sendIntResult :: ProcessId -> Closure (Process Int) -> Closure (Process ()) +-- > sendIntResult pid cl = bindStatic `closureApplyStatic` cl `closureApply` sendIntClosure pid +-- +-- [Dealing with qualified types] +-- +-- In the above we were careful to avoid qualified types. Suppose that we have +-- instead +-- +-- > send :: Binary a => ProcessId -> a -> Process () +-- +-- If we now want to define 'sendClosure', analogous to 'sendIntClosure' above, +-- we somehow need to include the 'Binary' instance in the closure -- after +-- all, we can ship this closure someplace else, where it needs to accept an +-- 'a', /then encode it/, and send it off. In order to do this, we need to turn +-- the Binary instance into an explicit dictionary: +-- +-- > data BinaryDict a where +-- > BinaryDict :: Binary a => BinaryDict a +-- > +-- > sendDict :: BinaryDict a -> ProcessId -> a -> Process () +-- > sendDict BinaryDict = send +-- +-- Now 'sendDict' is a normal polymorphic value: +-- +-- > sendDictStatic :: Static (BinaryDict a -> ProcessId -> a -> Process ()) +-- > sendDictStatic = staticLabel "$sendDict" +-- > +-- > rtable :: RemoteTable +-- > rtable = ... +-- > . registerStatic "$sendDict" (sendDict :: BinaryDict ANY -> ProcessId -> ANY -> Process ()) +-- > $ initRemoteTable +-- +-- so that we can define +-- +-- > sendClosure :: Static (BinaryDict a) -> Process a -> Closure (a -> Process ()) +-- > sendClosure dict pid = Closure decoder (encode pid) +-- > where +-- > decoder :: Static (ByteString -> a -> Process ()) +-- > decoder = (sendDictStatic `staticApply` dict) `staticCompose` decodeProcessIdStatic module Control.Distributed.Static - ( -- * Introducing static values + ( -- * Static values Static , staticLabel , staticApply - , staticDuplicate - -- * Eliminating static values - , RemoteTable - , initRemoteTable - , registerStatic - , unstatic - -- * Closures - , Closure(Closure) - , unclosure - , staticClosure - -- * Combinators on static values + -- * Derived static combinators , staticCompose , staticSplit , staticConst - -- * Combinators on closures + -- * Closures + , Closure(Closure) + -- * Derived closure combinators + , staticClosure , closureApplyStatic , closureApply , closureCompose , closureSplit - -- * Re-exports - , Typeable - , Dynamic - , toDynamic + -- * Resolution + , RemoteTable + , initRemoteTable + , registerStatic + , unstatic + , unclosure ) where import Prelude hiding (const, fst, snd) @@ -61,7 +218,6 @@ import Data.Rank1Dynamic (Dynamic, toDynamic, fromDynamic, dynApply) import Data.Rank1Typeable ( Typeable , typeOf - , ANY , ANY1 , ANY2 , ANY3 @@ -76,11 +232,9 @@ import Data.Rank1Typeable data StaticLabel = StaticLabel String | StaticApply StaticLabel StaticLabel - | StaticDuplicate StaticLabel deriving (Typeable, Show) --- | A static value. Static is opaque; see 'staticLabel', 'staticApply', --- 'staticDuplicate' or 'staticTypeOf'. +-- | A static value. Static is opaque; see 'staticLabel' and 'staticApply'. newtype Static a = Static StaticLabel deriving (Typeable, Show) @@ -99,8 +253,6 @@ putStaticLabel (StaticLabel string) = putWord8 0 >> put string putStaticLabel (StaticApply label1 label2) = putWord8 1 >> putStaticLabel label1 >> putStaticLabel label2 -putStaticLabel (StaticDuplicate label) = - putWord8 2 >> putStaticLabel label getStaticLabel :: Get StaticLabel getStaticLabel = do @@ -108,24 +260,19 @@ getStaticLabel = do case header of 0 -> StaticLabel <$> get 1 -> StaticApply <$> getStaticLabel <*> getStaticLabel - 2 -> StaticDuplicate <$> getStaticLabel _ -> fail "StaticLabel.get: invalid" -- | Create a primitive static value. -- -- It is the responsibility of the client code to make sure the corresponding -- entry in the 'RemoteTable' has the appropriate type. -staticLabel :: forall a. String -> Static a +staticLabel :: String -> Static a staticLabel = Static . StaticLabel -- | Apply two static values staticApply :: Static (a -> b) -> Static a -> Static b staticApply (Static f) (Static x) = Static (StaticApply f x) --- | Co-monadic 'duplicate' for static values -staticDuplicate :: forall a. Static a -> Static (Static a) -staticDuplicate (Static x) = Static (StaticDuplicate x) - -------------------------------------------------------------------------------- -- Eliminating static values -- -------------------------------------------------------------------------------- @@ -166,20 +313,15 @@ registerStatic label dyn (RemoteTable rtable) -- Pseudo-type: RemoteTable -> Static a -> a resolveStaticLabel :: RemoteTable -> StaticLabel -> Either String Dynamic resolveStaticLabel (RemoteTable rtable) (StaticLabel label) = - case Map.lookup label rtable of - Nothing -> Left $ "Invalid static label '" ++ label ++ "'" - Just d -> Right d + case Map.lookup label rtable of + Nothing -> Left $ "Invalid static label '" ++ label ++ "'" + Just d -> Right d resolveStaticLabel rtable (StaticApply label1 label2) = do - f <- resolveStaticLabel rtable label1 - x <- resolveStaticLabel rtable label2 - f `dynApply` x -resolveStaticLabel rtable (StaticDuplicate label) = do - x <- resolveStaticLabel rtable label -- Resolve only to get type info - toDynamic mkStatic `dynApply` x - where - mkStatic :: ANY -> Static ANY - mkStatic _ = Static label + f <- resolveStaticLabel rtable label1 + x <- resolveStaticLabel rtable label2 + f `dynApply` x +-- | Resolve a static value unstatic :: Typeable a => RemoteTable -> Static a -> Either String a unstatic rtable (Static static) = do dyn <- resolveStaticLabel rtable static @@ -204,7 +346,7 @@ unclosure rtable (Closure static env) = do return (f env) -- | Convert a static value into a closure. -staticClosure :: forall a. Typeable a => Static a -> Closure a +staticClosure :: Typeable a => Static a -> Closure a staticClosure static = Closure (staticConst static) empty -------------------------------------------------------------------------------- @@ -235,14 +377,17 @@ appStatic = staticLabel "$app" -- Combinators on static values -- -------------------------------------------------------------------------------- +-- | Static version of ('Prelude..') staticCompose :: (Typeable a, Typeable b, Typeable c) => Static (b -> c) -> Static (a -> b) -> Static (a -> c) staticCompose g f = composeStatic `staticApply` g `staticApply` f +-- | Static version of ('Control.Arrow.***') staticSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') => Static (a -> b) -> Static (a' -> b') -> Static ((a, a') -> (b, b')) staticSplit f g = splitStatic `staticApply` f `staticApply` g +-- | Static version of 'Prelude.const' staticConst :: (Typeable a, Typeable b) => Static a -> Static (b -> a) staticConst x = constStatic `staticApply` x @@ -251,7 +396,8 @@ staticConst x = constStatic `staticApply` x -- Combinators on Closures -- -------------------------------------------------------------------------------- -closureApplyStatic :: forall a b. (Typeable a, Typeable b) +-- | Apply a static function to a closure +closureApplyStatic :: (Typeable a, Typeable b) => Static (a -> b) -> Closure a -> Closure b closureApplyStatic f (Closure decoder env) = Closure (f `staticCompose` decoder) env @@ -259,6 +405,7 @@ closureApplyStatic f (Closure decoder env) = decodeEnvPairStatic :: Static (ByteString -> (ByteString, ByteString)) decodeEnvPairStatic = staticLabel "$decodeEnvPair" +-- | Closure application closureApply :: forall a b. (Typeable a, Typeable b) => Closure (a -> b) -> Closure a -> Closure b closureApply (Closure fdec fenv) (Closure xdec xenv) = @@ -271,10 +418,12 @@ closureApply (Closure fdec fenv) (Closure xdec xenv) = `staticCompose` decodeEnvPairStatic +-- | Closure composition closureCompose :: (Typeable a, Typeable b, Typeable c) => Closure (b -> c) -> Closure (a -> b) -> Closure (a -> c) closureCompose g f = composeStatic `closureApplyStatic` g `closureApply` f +-- | Closure version of ('Arrow.***') closureSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') => Closure (a -> b) -> Closure (a' -> b') -> Closure ((a, a') -> (b, b')) closureSplit f g = splitStatic `closureApplyStatic` f `closureApply` g From a7d1061ae884c7254b3aa10df1b55e3390b64be2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 15:40:38 +0100 Subject: [PATCH 0170/2357] More cleanup, and fix bug in BuiltIn The bug wasn't caught by the unit tests because the tests were broken; fixed that too. Added a note to the documentation of Static explaining what the bug was and how to correct it. --- src/Control/Distributed/Static.hs | 52 +++++++++++++++++-------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 21b9aa58..3edd2e1b 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -173,6 +173,29 @@ -- > where -- > decoder :: Static (ByteString -> a -> Process ()) -- > decoder = (sendDictStatic `staticApply` dict) `staticCompose` decodeProcessIdStatic +-- +-- [Word of Caution] +-- +-- You should not /define/ functions on 'ANY' and co. For example, the following +-- definition of 'rtable' is incorrect: +-- +-- > rtable :: RemoteTable +-- > rtable = registerStatic "$sdictSendPort" sdictSendPort +-- > $ initRemoteTable +-- > where +-- > sdictSendPort :: SerializableDict ANY -> SerializableDict (SendPort ANY) +-- > sdictSendPort SerializableDict = SerializableDict +-- +-- This definition of 'sdictSendPort' ignores its argument completely, and +-- constructs a 'SerializableDict' for the /monomorphic/ type @SendPort ANY@, +-- which isn't what you want. Instead, you should do +-- +-- > rtable :: RemoteTable +-- > rtable = registerStatic "$sdictSendPort" (sdictSendPort :: SerializableDict ANY -> SerializableDict (SendPort ANY)) +-- > $ initRemoteTable +-- > where +-- > sdictSendPort :: forall a. SerializableDict a -> SerializableDict (SendPort a) +-- > sdictSendPort SerializableDict = SerializableDict module Control.Distributed.Static ( -- * Static values Static @@ -198,8 +221,6 @@ module Control.Distributed.Static , unclosure ) where -import Prelude hiding (const, fst, snd) -import qualified Prelude (const, fst, snd) import Data.Binary ( Binary(get, put) , Put @@ -213,7 +234,7 @@ import Data.ByteString.Lazy (ByteString, empty) import Data.Map (Map) import qualified Data.Map as Map (lookup, empty, insert) import Control.Applicative ((<$>), (<*>)) -import qualified Control.Arrow as Arrow ((***), app) +import Control.Arrow as Arrow ((***), app) import Data.Rank1Dynamic (Dynamic, toDynamic, fromDynamic, dynApply) import Data.Rank1Typeable ( Typeable @@ -283,27 +304,12 @@ newtype RemoteTable = RemoteTable (Map String Dynamic) -- | Initial remote table initRemoteTable :: RemoteTable initRemoteTable = - registerStatic "$compose" (toDynamic compose) - . registerStatic "$const" (toDynamic const) - . registerStatic "$split" (toDynamic split) - . registerStatic "$app" (toDynamic app) - . registerStatic "$decodeEnvPair" (toDynamic decodeEnvPair) + registerStatic "$compose" (toDynamic ((.) :: (ANY2 -> ANY3) -> (ANY1 -> ANY2) -> ANY1 -> ANY3)) + . registerStatic "$const" (toDynamic (const :: ANY1 -> ANY2 -> ANY1)) + . registerStatic "$split" (toDynamic ((***) :: (ANY1 -> ANY3) -> (ANY2 -> ANY4) -> (ANY1, ANY2) -> (ANY3, ANY4))) + . registerStatic "$app" (toDynamic (app :: (ANY1 -> ANY2, ANY1) -> ANY2)) + . registerStatic "$decodeEnvPair" (toDynamic (decode :: ByteString -> (ByteString, ByteString))) $ RemoteTable Map.empty - where - compose :: (ANY2 -> ANY3) -> (ANY1 -> ANY2) -> ANY1 -> ANY3 - compose = (.) - - const :: ANY1 -> ANY2 -> ANY1 - const = Prelude.const - - split :: (ANY1 -> ANY3) -> (ANY2 -> ANY4) -> (ANY1, ANY2) -> (ANY3, ANY4) - split = (Arrow.***) - - app :: (ANY1 -> ANY2, ANY1) -> ANY2 - app = Arrow.app - - decodeEnvPair :: ByteString -> (ByteString, ByteString) - decodeEnvPair = decode -- | Register a static label registerStatic :: String -> Dynamic -> RemoteTable -> RemoteTable From 4859ffb5e91d4ec385cfa25f18b0d565a55867cc Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 16:56:27 +0100 Subject: [PATCH 0171/2357] Improve doc --- rank1dynamic.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index c7010c10..d8a51184 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -1,6 +1,6 @@ Name: rank1dynamic Version: 0.1.0.0 -Synopsis: Version of Data.Dynamic with support for rank-1 polymorphic types +Synopsis: Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types Description: "Data.Typeable" and "Data.Dynamic" only support monomorphic types. In this package we provide similar functionality but with support for rank-1 polymorphic types. From 94cd0b2a8baf79f201931434e6644f45f5c1b7c6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 17:31:09 +0100 Subject: [PATCH 0172/2357] Relax version bounds --- ChangeLog | 4 ++++ distributed-process-simplelocalnet.cabal | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 2086d0db..bdec8533 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-08-09 Edsko de Vries 0.2.0.4 + +* Relax version bounds for distributed-process + 2012-07-17 Edsko de Vries 0.2.0.3 * Improve documentation diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 618361ab..beea4ae8 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.3 +Version: 0.2.0.4 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -34,7 +34,7 @@ Library transformers >= 0.2 && < 0.4, network-transport >= 0.2 && < 0.3, network-transport-tcp >= 0.2 && < 0.3, - distributed-process >= 0.2 && < 0.3 + distributed-process >= 0.2 && < 0.4 Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast Extensions: RankNTypes, From 11d963a4a4124ee2ffffb712892550626fdfdc61 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 10 Aug 2012 17:45:54 +0100 Subject: [PATCH 0173/2357] Improve docs --- src/Network/Transport.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 6c327706..1922dee4 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -60,6 +60,11 @@ data EndPoint = EndPoint { -- | Lightweight connection to an endpoint. data Connection = Connection { -- | Send a message on this connection. + -- + -- 'send' provides vectored I/O, and allows multiple data segments to be + -- sent using a single call (cf. 'Network.Socket.ByteString.sendMany'). + -- Note that this segment structure is entirely unrelated to the segment + -- structure /returned/ by a 'Received' event. send :: [ByteString] -> IO (Either (TransportError SendErrorCode) ()) -- | Close the connection. , close :: IO () From 42f40284343f1864efda9277118c17f34e4dc6e0 Mon Sep 17 00:00:00 2001 From: Mark Wright Date: Sat, 11 Aug 2012 23:42:27 +1000 Subject: [PATCH 0174/2357] correct the dep to allow the tests to build --- distributed-process-simplelocalnet.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index beea4ae8..848e7645 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -56,7 +56,7 @@ Test-Suite TestSimpleLocalnet transformers >= 0.2 && < 0.4, network-transport >= 0.2 && < 0.3, network-transport-tcp >= 0.2 && < 0.3, - distributed-process >= 0.2 && < 0.3 + distributed-process >= 0.2 && < 0.4 Extensions: RankNTypes, DeriveDataTypeable ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind From 7ec4a910e4f9235e1f564c59d96bf0cbce00a43d Mon Sep 17 00:00:00 2001 From: Mark Wright Date: Sat, 11 Aug 2012 23:43:56 +1000 Subject: [PATCH 0175/2357] rank1dynamic builds with ghc 7.7.20120806 --- rank1dynamic.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index d8a51184..1d6c8f19 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -17,8 +17,8 @@ Cabal-Version: >=1.8 Library Exposed-Modules: Data.Rank1Dynamic, Data.Rank1Typeable - Build-Depends: base ==4.5.*, - ghc-prim >= 0.2 && < 0.3, + Build-Depends: base >= 4.5 && < 4.7, + ghc-prim >= 0.2 && < 0.4, binary >= 0.5 && < 0.6 HS-Source-Dirs: src GHC-Options: -Wall From a4d37c629b649338c6e85eefd6a8845dcb40af6b Mon Sep 17 00:00:00 2001 From: Mark Wright Date: Sat, 11 Aug 2012 23:45:26 +1000 Subject: [PATCH 0176/2357] distributed-static builds with ghc 7.7.20120806 --- distributed-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-static.cabal b/distributed-static.cabal index ac808300..81d58f8b 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -28,7 +28,7 @@ Library Exposed-Modules: Control.Distributed.Static Build-Depends: base >= 4 && < 5, rank1dynamic >= 0.1 && < 0.2, - containers >= 0.4 && < 0.5, + containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6 HS-Source-Dirs: src From 1040790ffcae7e82113646f5cb4d4d71ec29d6c8 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 14 Aug 2012 10:53:29 +0100 Subject: [PATCH 0177/2357] Make Closure opaque --- src/Control/Distributed/Static.hs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 3edd2e1b..84a09d5c 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -206,7 +206,8 @@ module Control.Distributed.Static , staticSplit , staticConst -- * Closures - , Closure(Closure) + , Closure + , closure -- * Derived closure combinators , staticClosure , closureApplyStatic @@ -345,6 +346,11 @@ instance Typeable a => Binary (Closure a) where put (Closure static env) = put static >> put env get = Closure <$> get <*> get +closure :: Static (ByteString -> a) -- ^ Decoder + -> ByteString -- ^ Encoded closure environment + -> Closure a +closure = Closure + -- | Resolve a closure unclosure :: Typeable a => RemoteTable -> Closure a -> Either String a unclosure rtable (Closure static env) = do @@ -353,7 +359,7 @@ unclosure rtable (Closure static env) = do -- | Convert a static value into a closure. staticClosure :: Typeable a => Static a -> Closure a -staticClosure static = Closure (staticConst static) empty +staticClosure static = closure (staticConst static) empty -------------------------------------------------------------------------------- -- Predefined static values -- @@ -406,7 +412,7 @@ staticConst x = constStatic `staticApply` x closureApplyStatic :: (Typeable a, Typeable b) => Static (a -> b) -> Closure a -> Closure b closureApplyStatic f (Closure decoder env) = - Closure (f `staticCompose` decoder) env + closure (f `staticCompose` decoder) env decodeEnvPairStatic :: Static (ByteString -> (ByteString, ByteString)) decodeEnvPairStatic = staticLabel "$decodeEnvPair" @@ -415,7 +421,7 @@ decodeEnvPairStatic = staticLabel "$decodeEnvPair" closureApply :: forall a b. (Typeable a, Typeable b) => Closure (a -> b) -> Closure a -> Closure b closureApply (Closure fdec fenv) (Closure xdec xenv) = - Closure decoder (encode (fenv, xenv)) + closure decoder (encode (fenv, xenv)) where decoder :: Static (ByteString -> b) decoder = appStatic From 013c724770e58d92b5876fd5827331d77faa29d5 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 16 Aug 2012 14:28:31 +0100 Subject: [PATCH 0178/2357] Bumb version of distributed-static due to backwards incompatible API change --- distributed-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-static.cabal b/distributed-static.cabal index 81d58f8b..037363d2 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -1,5 +1,5 @@ Name: distributed-static -Version: 0.1.0.0 +Version: 0.2.0.0 Synopsis: Compositional, type-safe, polymorphic static values and closures Description: /Towards Haskell in the Cloud/ (Epstein et al, Haskell Symposium 2011) introduces the concept of /static/ values: From 1fad4af20a4df0ecb4547621f47ad85d8fa0f44a Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 16 Aug 2012 15:43:52 +0100 Subject: [PATCH 0179/2357] Documentation fix --- src/Control/Distributed/Static.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 84a09d5c..3dfce7ce 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -104,7 +104,7 @@ -- We can now define 'sendIntClosure': -- -- > sendIntClosure :: ProcessId -> Closure (Int -> Process ()) --- > sendIntClosure pid = Closure decoder (encode pid) +-- > sendIntClosure pid = closure decoder (encode pid) -- > where -- > decoder :: Static (ByteString -> Int -> Process ()) -- > decoder = sendIntStatic `staticCompose` decodeProcessIdStatic @@ -169,7 +169,7 @@ -- so that we can define -- -- > sendClosure :: Static (BinaryDict a) -> Process a -> Closure (a -> Process ()) --- > sendClosure dict pid = Closure decoder (encode pid) +-- > sendClosure dict pid = closure decoder (encode pid) -- > where -- > decoder :: Static (ByteString -> a -> Process ()) -- > decoder = (sendDictStatic `staticApply` dict) `staticCompose` decodeProcessIdStatic From 337e177da14903996d5e3e357aefdf4519e6d3a6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 16 Aug 2012 15:46:51 +0100 Subject: [PATCH 0180/2357] Add ChangeLog --- ChangeLog | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..133c7c8f --- /dev/null +++ b/ChangeLog @@ -0,0 +1,8 @@ +2012-08-16 Edsko de Vries 0.2 + +* Hide the 'Closure' constructor and export 'closure' instead so that we are +free to change the internal representation + +2012-08-10 Edsko de Vries 0.1 + +* Initial release From 0bc909aa7b091ba589ec4f19fc40e02645327a59 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 20 Aug 2012 11:49:53 +0100 Subject: [PATCH 0181/2357] Allow for colons in hostnames. Bumb to 0.2.0.3. --- ChangeLog | 4 ++++ network-transport-tcp.cabal | 2 +- src/Network/Transport/TCP.hs | 19 +++++++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 5a6d69d8..ed027400 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-08-20 Edsko de Vries 0.2.0.3 + +* Allow for colons in hostnames (for IPv6) + 2012-07-16 Edsko de Vries 0.2.0.2 * Base 4.6 compatibility diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index c7d7d2ad..5c76b998 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -1,5 +1,5 @@ Name: network-transport-tcp -Version: 0.2.0.2 +Version: 0.2.0.3 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index b40c51ef..f01f0bf7 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -97,7 +97,7 @@ import Control.Exception ( IOException import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (concat) -import qualified Data.ByteString.Char8 as BSC (pack, unpack, split) +import qualified Data.ByteString.Char8 as BSC (pack, unpack) import Data.Int (Int32) import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap (empty) @@ -1539,7 +1539,7 @@ encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ decodeEndPointAddress :: EndPointAddress -> Maybe (N.HostName, N.ServiceName, EndPointId) decodeEndPointAddress (EndPointAddress bs) = - case map BSC.unpack $ BSC.split ':' bs of + case splitMaxFromEnd (== ':') 2 $ BSC.unpack bs of [host, port, endPointIdStr] -> case reads endPointIdStr of [(endPointId, "")] -> Just (host, port, endPointId) @@ -1547,6 +1547,21 @@ decodeEndPointAddress (EndPointAddress bs) = _ -> Nothing +-- | @spltiMaxFromEnd p n xs@ splits list @xs@ at elements matching @p@, +-- returning at most @p@ segments -- counting from the /end/ +-- +-- > splitMaxFromEnd (== ':') 2 "ab:cd:ef:gh" == ["ab:cd", "ef", "gh"] +splitMaxFromEnd :: (a -> Bool) -> Int -> [a] -> [[a]] +splitMaxFromEnd p = \n -> go [[]] n . reverse + where + -- go :: [[a]] -> Int -> [a] -> [[a]] + go accs _ [] = accs + go ([] : accs) 0 xs = reverse xs : accs + go (acc : accs) n (x:xs) = + if p x then go ([] : acc : accs) (n - 1) xs + else go ((x : acc) : accs) n xs + go _ _ _ = error "Bug in splitMaxFromEnd" + -------------------------------------------------------------------------------- -- Functions from TransportInternals -- -------------------------------------------------------------------------------- From 3ea36bc9a7211cc638da37dc329abead67aa5c78 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 20 Aug 2012 15:08:49 +0100 Subject: [PATCH 0182/2357] Improve docs --- .../Distributed/Process/Backend/Azure.hs | 71 ++++++++++++------- .../Process/Backend/Azure/GenericMain.hs | 5 +- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 0a7eee84..bae817d2 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -7,16 +7,17 @@ module Control.Distributed.Process.Backend.Azure -- * Re-exports from Azure Service Management , CloudService(..) , VirtualMachine(..) + , Endpoint(..) + , AzureSetup , Azure.cloudServices -- * Remote and local processes , ProcessPair(..) , RemoteProcess , LocalProcess + , localSend , localExpect , remoteSend , remoteThrow - , remoteSend' - , localSend ) where import System.Environment (getEnv) @@ -45,7 +46,7 @@ import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad (void, unless) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) -import Control.Exception (Exception, catches, Handler(Handler)) +import Control.Exception (Exception, catches, Handler(Handler), throwIO) import Control.Monad.IO.Class (MonadIO, liftIO) -- Azure @@ -101,20 +102,18 @@ data Backend = Backend { , copyToVM :: VirtualMachine -> IO () -- | Check the MD5 hash of the remote executable , checkMD5 :: VirtualMachine -> IO Bool - -- | @runOnVM dict vm port p@ starts a CH node on port 'port' and runs 'p' - , callOnVM :: forall a. - VirtualMachine - -> String - -> ProcessPair a - -> IO a - -- | Create a new CH node and run the specified process in the background. - -- The CH node will exit when the process exists. - , spawnOnVM :: VirtualMachine - -> String - -> Closure (Backend -> Process ()) - -> IO () + -- | @runOnVM vm port pp@ starts a new CH node on machine @vm@ and then + -- runs the specified process pair. The CH node will shut down when the + -- /local/ process exists. @callOnVM@ returns the returned by the local + -- process on exit. + , callOnVM :: forall a. VirtualMachine -> String -> ProcessPair a -> IO a + -- | Create a new CH node and run the specified process. + -- The CH node will shut down when the /remote/ process exists. @spawnOnVM@ + -- returns as soon as the process has been spawned. + , spawnOnVM :: VirtualMachine -> String -> RemoteProcess () -> IO () } deriving (Typeable) +-- | Azure connection parameters data AzureParameters = AzureParameters { azureSetup :: AzureSetup , azureSshUserName :: FilePath @@ -270,19 +269,42 @@ localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) -- Local and remote processes -- -------------------------------------------------------------------------------- +-- | A process pair consists of a remote process and a local process. The local +-- process can send messages to the remote process using 'localSend' and wait +-- for messages from the remote process using 'localExpect'. The remote process +-- can send messages to the local process using 'remoteSend', and wait for +-- messages from the local process using the standard Cloud Haskell primitives. +-- +-- See also 'callOnVM'. data ProcessPair a = ProcessPair { ppairRemote :: RemoteProcess () , ppairLocal :: LocalProcess a } +-- | The process to run on the remote node (see 'ProcessPair' and 'callOnVM'). type RemoteProcess a = Closure (Backend -> Process a) +-- | The process to run on the local node (see 'ProcessPair' and 'callOnVM'). newtype LocalProcess a = LocalProcess { unLocalProcess :: ReaderT SSH.Channel IO a } deriving (Functor, Monad, MonadIO, MonadReader SSH.Channel) runLocalProcess :: LocalProcess a -> SSH.Channel -> IO a runLocalProcess = runReaderT . unLocalProcess +-- | Send a messages from the local process to the remote process +-- (see 'ProcessPair') +localSend :: Serializable a => a -> LocalProcess () +localSend x = LocalProcess $ do + ch <- ask + liftIO $ mapM_ (SSH.writeChannel ch) + . prependLength + . messageToPayload + . createMessage + $ x + +-- | Wait for a message from the remote process (see 'ProcessPair'). +-- Note that unlike for the standard Cloud Haskell 'expect' it will result in a +-- runtime error if the remote process sends a message of type other than @a@. localExpect :: Serializable a => LocalProcess a localExpect = LocalProcess $ do ch <- ask @@ -294,20 +316,17 @@ localExpect = LocalProcess $ do then error (decode msg) else return (decode msg) -localSend :: Serializable a => a -> LocalProcess () -localSend x = LocalProcess $ do - ch <- ask - liftIO $ mapM_ (SSH.writeChannel ch) - . prependLength - . messageToPayload - . createMessage - $ x - +-- | Send a message from the remote process to the local process (see +-- 'ProcessPair'). Note that the remote process can use the standard Cloud +-- Haskell primitives to /receive/ messages from the local process. remoteSend :: Serializable a => a -> Process () remoteSend = liftIO . remoteSend' 0 -remoteThrow :: Exception e => e -> Process () -remoteThrow = liftIO . remoteSend' 1 . show +-- | If the remote process encounters an error it can use 'remoteThrow'. This +-- will cause the exception to be raised (as a user-exception, not as the +-- original type) in the local process (as well as in the remote process). +remoteThrow :: Exception e => e -> IO () +remoteThrow e = remoteSend' 1 (show e) >> throwIO e remoteSend' :: Serializable a => Int -> a -> IO () remoteSend' flags x = do diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index 13f18fe6..dc4ed7d9 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -85,7 +85,6 @@ import Control.Distributed.Process.Backend.Azure , Backend(findVMs, copyToVM, checkMD5, callOnVM, spawnOnVM) , ProcessPair(..) , RemoteProcess - , remoteSend' , remoteThrow ) @@ -190,7 +189,7 @@ onVmRun rtable host port bg = do if bg then detach $ startCH proc lprocMVar runProcess (\_ -> return ()) else do - startCH proc lprocMVar forkProcess remoteThrow + startCH proc lprocMVar forkProcess (liftIO . remoteThrow) lproc <- readMVar lprocMVar queueFromHandle stdin (processQueue lproc) where @@ -202,7 +201,7 @@ onVmRun rtable host port bg = do startCH rproc lprocMVar go exceptionHandler = do mTransport <- createTransport host port defaultTCPParameters case mTransport of - Left err -> remoteSend' 1 (show err) + Left err -> remoteThrow err Right transport -> do node <- newLocalNode transport rtable void . go node $ do From 300e77ff76bbe075291b06e945fb25a851c362c0 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 08:45:09 +0100 Subject: [PATCH 0183/2357] Improve docs, relax bound on containers --- .../Distributed/Process/Backend/SimpleLocalnet.hs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index d1d34b13..1cd23555 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -305,14 +305,21 @@ terminateAllSlaves backend = do -- Master nodes -------------------------------------------------------------------------------- --- | 'startMaster' finds all slaves currently available on the local network --- (which should therefore be started first), redirects all log messages to --- itself, and then calls the specified process, passing the list of slaves --- nodes. +-- | 'startMaster' finds all slaves /currently/ available on the local network, +-- redirects all log messages to itself, and then calls the specified process, +-- passing the list of slaves nodes. -- -- Terminates when the specified process terminates. If you want to terminate -- the slaves when the master terminates, you should manually call -- 'terminateAllSlaves'. +-- +-- If you start more slave nodes after having started the master node, you can +-- discover them with later calls to 'findSlaves', but be aware that you will +-- need to call 'redirectLogHere' to redirect their logs to the master node. +-- +-- Note that you can use functionality of "SimpleLocalnet" directly (through +-- 'Backend'), instead of using 'startMaster'/'startSlave', if the master/slave +-- distinction does not suit your application. startMaster :: Backend -> ([NodeId] -> Process ()) -> IO () startMaster backend proc = do node <- newLocalNode backend From fe133b2d48413507525f9ccf1261aa7280fc7c1e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 09:12:22 +0100 Subject: [PATCH 0184/2357] Initialize the backend on the server This completes the low-level Azure API. --- demos/Conway.hs | 34 ++++++++------ .../Distributed/Process/Backend/Azure.hs | 44 ++++++++++++++----- .../Process/Backend/Azure/GenericMain.hs | 35 +++++++++------ 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/demos/Conway.hs b/demos/Conway.hs index 31885638..f4dafd7d 100644 --- a/demos/Conway.hs +++ b/demos/Conway.hs @@ -2,6 +2,8 @@ import Data.Data (Typeable, Data) import Data.Binary (Binary(get, put)) import Data.Binary.Generic (getGeneric, putGeneric) +import Control.Monad (forever) +import Control.Monad.IO.Class (liftIO) import Control.Distributed.Process ( Process , expect @@ -11,9 +13,12 @@ import Control.Distributed.Process.Closure , mkClosure ) import Control.Distributed.Process.Backend.Azure - ( Backend - , ProcessPair(..) + ( Backend(findVMs) + , ProcessPair(ProcessPair) , RemoteProcess + , LocalProcess + , remoteSend + , localExpect ) import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) @@ -25,23 +30,24 @@ instance Binary ControllerMsg where get = getGeneric put = putGeneric -conwayController :: () -> Backend -> Process () -conwayController () _backend = go - where - go = do - msg <- expect - case msg of - ControllerExit -> - return () +conwayStart :: () -> Backend -> Process () +conwayStart () backend = do + vms <- liftIO $ findVMs backend + remoteSend (show vms) + +remotable ['conwayStart] -remotable ['conwayController] +echo :: LocalProcess () +echo = forever $ do + msg <- localExpect + liftIO $ putStrLn msg main :: IO () main = genericMain __remoteTable callable spawnable where callable :: String -> IO (ProcessPair ()) - callable _ = error "spawnable: unknown" + callable "start" = return $ ProcessPair ($(mkClosure 'conwayStart) ()) echo + callable _ = error "callable: unknown" spawnable :: String -> IO (RemoteProcess ()) - spawnable "controller" = return $ $(mkClosure 'conwayController) () - spawnable _ = error "callable: unknown" + spawnable _ = error "spawnable: unknown" diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index bae817d2..704fd8c8 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -24,7 +24,7 @@ import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) import System.IO (stdout, hFlush) -import Data.Binary (encode, decode) +import Data.Binary (Binary(get, put), encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString as BSS ( ByteString @@ -43,7 +43,7 @@ import qualified Data.ByteString.Lazy as BSL ) import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) -import Control.Applicative ((<$>)) +import Control.Applicative ((<$>), (<*>)) import Control.Monad (void, unless) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) import Control.Exception (Exception, catches, Handler(Handler), throwIO) @@ -125,6 +125,19 @@ data AzureParameters = AzureParameters { , azureSshLocalPath :: FilePath } +instance Binary AzureParameters where + put params = do + put (azureSetup params) + put (azureSshUserName params) + put (azureSshPublicKey params) + put (azureSshPrivateKey params) + put (azureSshPassphrase params) + put (azureSshKnownHosts params) + put (azureSshRemotePath params) + put (azureSshLocalPath params) + get = + AzureParameters <$> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get + -- | Create default azure parameters defaultAzureParameters :: String -- ^ Azure subscription ID -> FilePath -- ^ Path to X509 certificate @@ -155,8 +168,8 @@ initializeBackend params cloudService = findVMs = apiFindVMs params cloudService , copyToVM = apiCopyToVM params , checkMD5 = apiCheckMD5 params - , callOnVM = apiCallOnVM params - , spawnOnVM = apiSpawnOnVM params + , callOnVM = apiCallOnVM params cloudService + , spawnOnVM = apiSpawnOnVM params cloudService } -- | Find virtual machines @@ -175,21 +188,26 @@ apiCopyToVM params vm = -- | Call a process on a VM apiCallOnVM :: AzureParameters + -> String -> VirtualMachine -> String -> ProcessPair a -> IO a -apiCallOnVM params vm port ppair = +apiCallOnVM params cloudService vm port ppair = withSSH2 params vm $ \s -> do let exe = "PATH=. " ++ azureSshRemotePath params ++ " onvm run " ++ " --host " ++ vmIpAddress vm ++ " --port " ++ port + ++ " --cloud-service " ++ cloudService ++ " 2>&1" + let paramsEnc = encode params (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe - _ <- SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc)) - _ <- SSH.writeAllChannel ch rprocEnc + SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc)) + SSH.writeAllChannel ch rprocEnc + SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) + SSH.writeAllChannel ch paramsEnc runLocalProcess (ppairLocal ppair) ch if status == 0 then return r @@ -199,22 +217,28 @@ apiCallOnVM params vm port ppair = rprocEnc = encode (ppairRemote ppair) apiSpawnOnVM :: AzureParameters + -> String -> VirtualMachine -> String -> Closure (Backend -> Process ()) -> IO () -apiSpawnOnVM params vm port proc = +apiSpawnOnVM params cloudService vm port proc = withSSH2 params vm $ \s -> do + -- TODO: reduce duplication with apiCallOnVM let exe = "PATH=. " ++ azureSshRemotePath params ++ " onvm run " ++ " --host " ++ vmIpAddress vm ++ " --port " ++ port + ++ " --cloud-service " ++ cloudService ++ " --background " ++ " 2>&1" + let paramsEnc = encode params (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe - _ <- SSH.writeChannel ch (encodeInt32 (BSL.length procEnc)) - _ <- SSH.writeAllChannel ch procEnc + SSH.writeChannel ch (encodeInt32 (BSL.length procEnc)) + SSH.writeAllChannel ch procEnc + SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) + SSH.writeAllChannel ch paramsEnc SSH.channelSendEOF ch SSH.readAllChannel ch unless (status == 0) $ error (BSLC.unpack r) diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs index dc4ed7d9..1630c8f3 100644 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs @@ -149,6 +149,7 @@ genericMain remoteTable callable spawnable = do onVmRun (remoteTable initRemoteTable) (onVmIP vmCmd) (onVmPort vmCmd) + (onVmService vmCmd) (onVmBackground vmCmd) SSH.exit where @@ -178,27 +179,29 @@ azureParameters opts (Just sshOpts) = do -- Executing a closure on the VM -- -------------------------------------------------------------------------------- -onVmRun :: RemoteTable -> String -> String -> Bool -> IO () -onVmRun rtable host port bg = do +onVmRun :: RemoteTable -> String -> String -> String -> Bool -> IO () +onVmRun rtable host port cloudService bg = do hSetBinaryMode stdin True hSetBinaryMode stdout True - mProcEnc <- getWithLength stdin - forM_ mProcEnc $ \procEnc -> do - let proc = decode procEnc - lprocMVar <- newEmptyMVar :: IO (MVar LocalProcess) - if bg - then detach $ startCH proc lprocMVar runProcess (\_ -> return ()) - else do - startCH proc lprocMVar forkProcess (liftIO . remoteThrow) - lproc <- readMVar lprocMVar - queueFromHandle stdin (processQueue lproc) + Just procEnc <- getWithLength stdin + Just paramsEnc <- getWithLength stdin + backend <- initializeBackend (decode paramsEnc) cloudService + let proc = decode procEnc + lprocMVar <- newEmptyMVar :: IO (MVar LocalProcess) + if bg + then detach $ startCH proc lprocMVar backend runProcess (\_ -> return ()) + else do + startCH proc lprocMVar backend forkProcess (liftIO . remoteThrow) + lproc <- readMVar lprocMVar + queueFromHandle stdin (processQueue lproc) where startCH :: RemoteProcess () -> MVar LocalProcess + -> Backend -> (LocalNode -> Process () -> IO a) -> (SomeException -> Process ()) -> IO () - startCH rproc lprocMVar go exceptionHandler = do + startCH rproc lprocMVar backend go exceptionHandler = do mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> remoteThrow err @@ -206,7 +209,6 @@ onVmRun rtable host port bg = do node <- newLocalNode transport rtable void . go node $ do ask >>= liftIO . putMVar lprocMVar - let backend = error "TODO: backend not initialized in onVmRun" proc <- unClosure rproc :: Process (Backend -> Process ()) catch (proc backend) exceptionHandler @@ -291,6 +293,7 @@ data OnVmCommand = OnVmRun { onVmIP :: String , onVmPort :: String + , onVmService :: String , onVmBackground :: Bool } deriving Show @@ -387,6 +390,10 @@ onVmRunParser = OnVmRun & metavar "PORT" & help "port number" ) + <*> strOption ( long "cloud-service" + & metavar "CS" + & help "Cloud service name" + ) <*> switch ( long "background" & help "Run the process in the background" ) From e049724ca3f5229c6717b58714fb0b401da11a54 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 11:33:42 +0100 Subject: [PATCH 0185/2357] Start documentation: describe Azure setup --- .../Distributed/Process/Backend/Azure.hs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 704fd8c8..73fd439b 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -1,3 +1,141 @@ +-- | This module provides the low-level API to running Cloud Haskell on +-- Microsoft Azure virtual machines (). Virtual +-- machines within an Azure cloud service can talk to each other directly using +-- standard Cloud Haskell primitives (using TCP/IP under the hood); to talk to +-- the remote machines from your local machine you can use the primitives +-- provided in this module (which use ssh under the hood). It looks something +-- like +-- +-- > _ _ +-- > ( ` )_ +-- > ( ) `) Azure cloud service +-- > (_ (_ . _) _) +-- > +-- > | +-- > | ssh connection +-- > | +-- > +-- > +---+ +-- > | | Local machine +-- > +---+ +-- +-- In this module we concentrate on the link between the local machine to the +-- remote machines. "Control.Distributed.Process.Backend.Azure.Process" provides +-- a higher-level interface that can be used in code that runs on the remote +-- machines. +-- +-- /NOTE/: It is unfortunate that the local machine cannot talk to the remote +-- machine using the standard Cloud Haskell primitives. In an ideal world, we +-- could just start a Cloud Haskell node on the local machine, too. +-- Unfortunately, Cloud Haskell does not yet support using multiple network +-- transports (TCP/IP vs SSH). This is a temporary workaround. +-- +-- [Azure Setup] +-- +-- In this section we describe how to set up an Azure Cloud Service for use +-- with Cloud Haskell, starting from a brand new Azure account. It is not +-- intended as an Azure tutorial, but as a guide to making the right choices to +-- get Cloud Haskell up and running as quickly as possible. +-- +-- An Azure /Cloud Service/ is a set of virtual machines that can talk to each +-- other directly over TCP/IP (they are part of the same private network). You +-- don't create the cloud service directly; instead, after you have set up your +-- first virtual machine as a /stand alone/ virtual machine, you can /connect/ +-- subsequent virtual machines to the first virtual machine, thereby implicitly +-- setting up a Cloud Service. +-- +-- We have only tested Cloud Haskell with Linux based virtual machines; +-- Windows based virtual machines /might/ work, but you'll be entering +-- unchartered territory. Cloud Haskell assumes that all nodes run the same +-- binary code; hence, you must use the same OS on all virtual machines, +-- /as well as on your local machine/. We use Ubuntu Server 12.04 LTS for our +-- tests (running on VirtualBox on our local machine). +-- +-- When you set up your virtual machine, you can pick an arbitrary name; these +-- names are for your own use only and do not need to be globally unique. Set a +-- username and password; you should use the same username on all virtual +-- machines. You should also upload +-- an SSH key for authentication (see +-- /Converting OpenSSH keys for use on Windows Azure Linux VM's/, +-- , for +-- information on how to convert a standard Linux @id_rsa.pub@ public key to +-- X509 format suitable for Azure). For the first VM you create select +-- /Standalone Virtual Machine/, and pick an appropriate DNS name. The DNS name +-- /does/ have to be globally unique, and will also be the name of the Cloud +-- Service. For subsequent virtual machines, select +-- /Connect to Existing Virtual Machine/ instead and then select the first VM +-- you created. +-- +-- In these notes, we assume three virtual machines called @CHDemo1@, +-- @CHDemo2@, and @CHDemo3@, all part of the @CloudHaskellDemo@ cloud service. +-- +-- [Obtaining a Management Certificate] +-- +-- Azure authentication is by means of an X509 certificate and corresponding +-- private key. /Create management certificates for Linux in Windows Azure/, +-- , +-- describes how you can create a management certificate for Azure, download it +-- as a @.publishsettings@ file, and extract an @.pfx@ file from it. You cannot +-- use this @.pfx@ directly; instead, you will need to extract an X509 +-- certificate from it and a private key in suitable format. You can use the +-- @openssl@ command line tool for both tasks; assuming that you stored the +-- @.pfx@ file as @credentials.pfx@, to extract the X509 certificate: +-- +-- > openssl pkcs12 -in credentials.pfx -nokeys -out credentials.x509 +-- +-- And to extract the private key: +-- +-- > openssl pkcs12 -in credentials.pfx -nocerts -nodes | openssl rsa -out credentials.private +-- +-- (@openssl pkcs12@ outputs the private key in PKCS#8 format (BEGIN PRIVATE +-- KEY), but we need it in PKCS#1 format (BEGIN RSA PRIVATE KEY). +-- +-- [Testing the Setup] +-- +-- Build and install the @distributed-process-azure@ package, making sure to +-- pass the @build-demos@ flag to Cabal. +-- +-- > cabal-dev install distributed-process-azure -f build-demos +-- +-- We can use any of the demos to test our setup; we will use the @ping@ demo: +-- +-- > cloud-haskell-azure-ping list \ +-- > --subscription-id <> \ +-- > --certificate /path/to/credentials.x509 \ +-- > --private /path/to/credentials.private +-- +-- (you can find your subscription ID in the @.publishsettings@ file from the previous step). +-- If everything went well, this will output something like +-- +-- > Cloud Service "CloudHaskellDemo" +-- > VIRTUAL MACHINES +-- > Virtual Machine "CHDemo3" +-- > IP 10.119.182.127 +-- > INPUT ENDPOINTS +-- > Input endpoint "SSH" +-- > Port 50136 +-- > VIP 168.63.31.38 +-- > Virtual Machine "CHDemo2" +-- > IP 10.59.238.125 +-- > INPUT ENDPOINTS +-- > Input endpoint "SSH" +-- > Port 63365 +-- > VIP 168.63.31.38 +-- > Virtual Machine "CHDemo1" +-- > IP 10.59.224.122 +-- > INPUT ENDPOINTS +-- > Input endpoint "SSH" +-- > Port 22 +-- > VIP 168.63.31.38 +-- +-- The IP addresses listed are /internal/ IP addresses; they can be used by the +-- virtual machines to talk to each other, but not by the outside world to talk +-- to the virtual machines. To do that, you will need to use the VIP (Virtual +-- IP) address instead, which you will notice is the same for all virtual +-- machines that are part of the cloud service. The corresponding DNS name +-- (here @CloudHaskellDemo.cloudapp.net@) will also resolve to this (V)IP +-- address. To login to individual machines (through SSH) you will need to use +-- the specific port mentioned under INPUT ENDPOINTS. module Control.Distributed.Process.Backend.Azure ( -- * Initialization Backend(..) From 422b6c161f5bbccc34cc2c2857ef3a474e35a5d2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 13:11:51 +0100 Subject: [PATCH 0186/2357] First draft of the docs for the low-level API --- demos/Echo.hs | 14 +- demos/Ping.hs | 34 +- .../Distributed/Process/Backend/Azure.hs | 297 +++++++++++++++++- 3 files changed, 309 insertions(+), 36 deletions(-) diff --git a/demos/Echo.hs b/demos/Echo.hs index 1239e392..b5809668 100644 --- a/demos/Echo.hs +++ b/demos/Echo.hs @@ -16,28 +16,28 @@ import Control.Distributed.Process.Backend.Azure ) import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) -echoServer :: () -> Backend -> Process () -echoServer () _backend = forever $ do +echoRemote :: () -> Backend -> Process () +echoRemote () _backend = forever $ do str <- expect remoteSend (str :: String) -remotable ['echoServer] +remotable ['echoRemote] -echoClient :: LocalProcess () -echoClient = do +echoLocal :: LocalProcess () +echoLocal = do str <- liftIO $ putStr "# " >> hFlush stdout >> getLine unless (null str) $ do localSend str liftIO $ putStr "Echo: " >> hFlush stdout echo <- localExpect liftIO $ putStrLn echo - echoClient + echoLocal main :: IO () main = genericMain __remoteTable callable spawnable where callable :: String -> IO (ProcessPair ()) - callable "echo" = return $ ProcessPair ($(mkClosure 'echoServer) ()) echoClient + callable "echo" = return $ ProcessPair ($(mkClosure 'echoRemote) ()) echoLocal callable _ = error "callable: unknown" spawnable :: String -> IO (RemoteProcess ()) diff --git a/demos/Ping.hs b/demos/Ping.hs index b4810692..5e321b87 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -26,8 +26,16 @@ import Control.Distributed.Process.Backend.Azure import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) -pingClient :: () -> Backend -> Process () -pingClient () _backend = do +pingServer :: () -> Backend -> Process () +pingServer () _backend = do + us <- getSelfPid + liftIO $ BSL.writeFile "pingServer.pid" (encode us) + forever $ do + them <- expect + send them () + +pingClientRemote :: () -> Backend -> Process () +pingClientRemote () _backend = do mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") case mPingServerEnc of Left err -> @@ -45,26 +53,18 @@ pingClient () _backend = do then remoteSend $ "Ping server at " ++ show pingServerPid ++ " ok" else remoteSend $ "Ping server at " ++ show pingServerPid ++ " failure" -pingServer :: () -> Backend -> Process () -pingServer () _backend = do - us <- getSelfPid - liftIO $ BSL.writeFile "pingServer.pid" (encode us) - forever $ do - them <- expect - send them () - -remotable ['pingClient, 'pingServer] +remotable ['pingClientRemote, 'pingServer] -receiveString :: LocalProcess () -receiveString = localExpect >>= liftIO . putStrLn +pingClientLocal :: LocalProcess () +pingClientLocal = localExpect >>= liftIO . putStrLn main :: IO () main = genericMain __remoteTable callable spawnable where callable :: String -> IO (ProcessPair ()) - callable "ping" = return $ ProcessPair ($(mkClosure 'pingClient) ()) receiveString - callable _ = error "callable: unknown" + callable "client" = return $ ProcessPair ($(mkClosure 'pingClientRemote) ()) pingClientLocal + callable _ = error "callable: unknown" spawnable :: String -> IO (RemoteProcess ()) - spawnable "pingServer" = return $ ($(mkClosure 'pingServer) ()) - spawnable _ = error "spawnable: unknown" + spawnable "server" = return $ ($(mkClosure 'pingServer) ()) + spawnable _ = error "spawnable: unknown" diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 73fd439b..7707ab65 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -1,10 +1,9 @@ --- | This module provides the low-level API to running Cloud Haskell on --- Microsoft Azure virtual machines (). Virtual --- machines within an Azure cloud service can talk to each other directly using --- standard Cloud Haskell primitives (using TCP/IP under the hood); to talk to --- the remote machines from your local machine you can use the primitives --- provided in this module (which use ssh under the hood). It looks something --- like +-- | This module provides the API for running Cloud Haskell on Microsoft Azure +-- virtual machines (). Virtual machines within an +-- Azure cloud service can talk to each other directly using standard Cloud +-- Haskell primitives (using TCP/IP under the hood); to talk to the remote +-- machines from your local machine you can use the primitives provided in this +-- module (which use ssh under the hood). It looks something like -- -- > _ _ -- > ( ` )_ @@ -19,11 +18,6 @@ -- > | | Local machine -- > +---+ -- --- In this module we concentrate on the link between the local machine to the --- remote machines. "Control.Distributed.Process.Backend.Azure.Process" provides --- a higher-level interface that can be used in code that runs on the remote --- machines. --- -- /NOTE/: It is unfortunate that the local machine cannot talk to the remote -- machine using the standard Cloud Haskell primitives. In an ideal world, we -- could just start a Cloud Haskell node on the local machine, too. @@ -66,6 +60,17 @@ -- /Connect to Existing Virtual Machine/ instead and then select the first VM -- you created. -- +-- Once your virtual machines have been set up, you have to make sure that the +-- user you created when you created the VM can ssh from any virtual machine to +-- any other using public key authentication. Moreover, you have to make sure +-- that @libssh2@ is installed; if you are using the Ubuntu image we recommend +-- you can install @libssh2@ using +-- +-- > sudo apt-get install libssh2-1 +-- +-- (TODO: if you don't install libssh2 things will break without a clear error +-- message.) +-- -- In these notes, we assume three virtual machines called @CHDemo1@, -- @CHDemo2@, and @CHDemo3@, all part of the @CloudHaskellDemo@ cloud service. -- @@ -136,6 +141,274 @@ -- (here @CloudHaskellDemo.cloudapp.net@) will also resolve to this (V)IP -- address. To login to individual machines (through SSH) you will need to use -- the specific port mentioned under INPUT ENDPOINTS. +-- +-- [Overview of the API] +-- +-- The Azure 'Backend' provides low-level functionality for interacting with +-- Azure virtual machines. 'findVMs' finds all currently available virtual +-- machines; 'copyToVM' copies the executable to a specified VM (recall that +-- all VMs, as well as the local machine, are assumed to run the same OS so +-- that they can all run the same binary), and 'checkMD5' checks the MD5 hash +-- of the executable on a remote machine. +-- +-- 'callOnVM' and 'spawnOnVM' deal with setting up Cloud Haskell nodes. +-- 'spawnOnVM' takes a virtual machine and a port number, as well as a +-- @RemoteProcess ()@, starts the executable on the remote node, sets up a new +-- Cloud Haskell node, and then runs the specified process. The Cloud Haskell +-- node will be shut down when the given process terminates. 'RemoteProcess' is +-- defined as +-- +-- > type RemoteProcess a = Closure (Backend -> Process a) +-- +-- (If you don't know what a 'Closure' is you should read +-- "Control.Distributed.Process.Closure".) 'spawnOnVM' terminates once the +-- Cloud Haskell node has been set up. +-- +-- 'callOnVM' is similar to 'spawnOnVM', but it takes a /pair/ of processes: +-- one to run on the remote host (on a newly created Cloud Haskell node), and +-- one to run on the local machine. In this case, the new Cloud Haskell node +-- will be terminated when the /local/ process terminates. 'callOnVM' is useful +-- because the remote process and the local process can communicate through a +-- set of primitives provided in this module ('localSend', 'localExpect', and +-- 'remoteSend' -- there is no 'remoteExpect'; instead the remote process can +-- use the standard Cloud Haskell 'expect' primitive). +-- +-- [First Example: Echo] +-- +-- The @echo@ demo starts a new Cloud Haskell node, waits for input from the +-- user on the local machine, sends this to the remote machine. The remote +-- machine will echo this back; the local machine will wait for the echo, show +-- the echo, and repeat. +-- +-- Before you can try it you will first need to copy the executable to the remote server: +-- +-- > cloud-haskell-azure-echo install \ +-- > --subscription-id < \ +-- > --certificate /path/to/credentials.x509 \ +-- > --private /path/to/credentials.private \ +-- > --user <> \ +-- > --cloud-service <> \ +-- > --virtual-machine <> +-- +-- (If you leave out the @--virtual-machine@ argument the binary will be copied +-- to every virtual machine in the specified cloud service). Once installed, +-- you can run it as follows: +-- +-- > cloud-haskell-azure-echo run \ +-- > --subscription-id < \ +-- > --certificate /path/to/credentials.x509 \ +-- > --private /path/to/credentials.private \ +-- > --user <> \ +-- > --cloud-service <> \ +-- > --virtual-machine <> \ +-- > --port 8080 \ +-- > --closure echo +-- > CHDemo1: # Everything I type will be echoed back +-- > Echo: Everything I type will be echoed back +-- > # Until I enter a blank line +-- > Echo: Until I enter a blank line +-- > # +-- +-- "Control.Distributed.Process.Backend.Azure.GenericMain" provides a generic +-- main function that you can to structure your code. It provides command line +-- arguments such as the ones we saw in section /Testing the Setup/, it +-- initializes the Azure backend, and it takes care of running your code on the +-- remote machines. You don't have to use 'genericMain' if you prefer not to, +-- but then it will be your own responsibility to initialize Azure and to make +-- sure that your executable does the right thing when it's invoked on the +-- remote node. In these notes we will assume that you will use 'genericMain'. +-- +-- The full @echo@ demo is +-- +-- > {-# LANGUAGE TemplateHaskell #-} +-- > +-- > import System.IO (hFlush, stdout) +-- > import Control.Monad (unless, forever) +-- > import Control.Monad.IO.Class (liftIO) +-- > import Control.Distributed.Process (Process, expect) +-- > import Control.Distributed.Process.Closure (remotable, mkClosure) +-- > import Control.Distributed.Process.Backend.Azure +-- > ( Backend +-- > , ProcessPair(..) +-- > , RemoteProcess +-- > , LocalProcess +-- > , localExpect +-- > , remoteSend +-- > , localSend +-- > ) +-- > import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) +-- > +-- > echoRemote :: () -> Backend -> Process () +-- > echoRemote () _backend = forever $ do +-- > str <- expect +-- > remoteSend (str :: String) +-- > +-- > remotable ['echoRemote] +-- > +-- > echoLocal :: LocalProcess () +-- > echoLocal = do +-- > str <- liftIO $ putStr "# " >> hFlush stdout >> getLine +-- > unless (null str) $ do +-- > localSend str +-- > liftIO $ putStr "Echo: " >> hFlush stdout +-- > echo <- localExpect +-- > liftIO $ putStrLn echo +-- > echoLocal +-- > +-- > main :: IO () +-- > main = genericMain __remoteTable callable spawnable +-- > where +-- > callable :: String -> IO (ProcessPair ()) +-- > callable "echo" = return $ ProcessPair ($(mkClosure 'echoRemote) ()) echoLocal +-- > callable _ = error "callable: unknown" +-- > +-- > spawnable :: String -> IO (RemoteProcess ()) +-- > spawnable _ = error "spawnable: unknown" +-- +-- 'genericMain' expects three arguments: the first is the standard +-- '__remoteTable' argument familiar from +-- "Control.Distributed.Process.Closure"; the second and third should map +-- strings to process pairs (for use with 'callOnVM') or remote processes (for +-- use with 'spawnOnVM') respectively. +-- +-- When you invoke the @echo@ demo with @--closure echo@ the 'genericMain' +-- function calls 'callOnVM' with the process pair consisting of 'echoRemote' +-- and 'echoLocal'. Hopefully the definition of these two functions is +-- self-explanatory. +-- +-- [Second Example: Ping] +-- +-- The second example differs from the @echo@ demo in that it uses both +-- 'callable' ('callOnVM') and 'spawnable' ('spawnOnVM'). It uses the latter to +-- install a ping server which keeps running in the background; it uses the +-- former to run a ping client which sends a request to the ping server and +-- outputs the response. As with the @echo@ server, we must first copy the +-- executable: +-- +-- > cloud-haskell-azure-ping install \ +-- > --subscription-id < \ +-- > --certificate /path/to/credentials.x509 \ +-- > --private /path/to/credentials.private \ +-- > --user <> \ +-- > --cloud-service <> \ +-- > CHDemo3: Done +-- > CHDemo2: Done +-- > CHDemo1: Done +-- +-- Now we can start the ping server on every virtual machine in the cloud +-- service (to install it to a single virtual machine only, pass the +-- @--virtual-machine@ argument): +-- +-- > cloud-haskell-azure-ping run \ +-- > --subscription-id < \ +-- > --certificate /path/to/credentials.x509 \ +-- > --private /path/to/credentials.private \ +-- > --user <> \ +-- > --cloud-service <> \ +-- > --port 8080 +-- > --closure server +-- > --background +-- > CHDemo3: OK +-- > CHDemo2: OK +-- > CHDemo1: OK +-- +-- Finally, we can run the ping client: +-- +-- > cloud-haskell-azure-ping run +-- > --subscription-id < \ +-- > --certificate /path/to/credentials.x509 \ +-- > --private /path/to/credentials.private \ +-- > --user <> \ +-- > --cloud-service <> \ +-- > --port 8081 +-- > --closure client +-- > CHDemo3: Ping server at pid://10.119.182.127:8080:0:2 ok +-- > CHDemo2: Ping server at pid://10.59.238.125:8080:0:2 ok +-- > CHDemo1: Ping server at pid://10.59.224.122:8080:0:2 ok +-- +-- Note that we must pass a different port number, because the client will run +-- within its own Cloud Haskell instance. +-- +-- The code for the ping server is similar to the echo server, but demonstrates +-- both 'callable' and 'spawnable' and shows one way to discover nodes. +-- +-- > {-# LANGUAGE TemplateHaskell #-} +-- > +-- > import Data.Binary (encode, decode) +-- > import Control.Monad (forever) +-- > import Control.Monad.IO.Class (liftIO) +-- > import Control.Exception (try, IOException) +-- > import Control.Distributed.Process +-- > ( Process +-- > , getSelfPid +-- > , expect +-- > , send +-- > , monitor +-- > , receiveWait +-- > , match +-- > , ProcessMonitorNotification(..) +-- > ) +-- > import Control.Distributed.Process.Closure (remotable, mkClosure) +-- > import Control.Distributed.Process.Backend.Azure +-- > ( Backend +-- > , ProcessPair(..) +-- > , RemoteProcess +-- > , LocalProcess +-- > , localExpect +-- > , remoteSend +-- > ) +-- > import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) +-- > import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) +-- > +-- > pingServer :: () -> Backend -> Process () +-- > pingServer () _backend = do +-- > us <- getSelfPid +-- > liftIO $ BSL.writeFile "pingServer.pid" (encode us) +-- > forever $ do +-- > them <- expect +-- > send them () +-- > +-- > pingClientRemote :: () -> Backend -> Process () +-- > pingClientRemote () _backend = do +-- > mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") +-- > case mPingServerEnc of +-- > Left err -> +-- > remoteSend $ "Ping server not found: " ++ show (err :: IOException) +-- > Right pingServerEnc -> do +-- > let pingServerPid = decode pingServerEnc +-- > pid <- getSelfPid +-- > _ref <- monitor pingServerPid +-- > send pingServerPid pid +-- > gotReply <- receiveWait +-- > [ match (\() -> return True) +-- > , match (\(ProcessMonitorNotification {}) -> return False) +-- > ] +-- > if gotReply +-- > then remoteSend $ "Ping server at " ++ show pingServerPid ++ " ok" +-- > else remoteSend $ "Ping server at " ++ show pingServerPid ++ " failure" +-- > +-- > remotable ['pingClientRemote, 'pingServer] +-- > +-- > pingClientLocal :: LocalProcess () +-- > pingClientLocal = localExpect >>= liftIO . putStrLn +-- > +-- > main :: IO () +-- > main = genericMain __remoteTable callable spawnable +-- > where +-- > callable :: String -> IO (ProcessPair ()) +-- > callable "client" = return $ ProcessPair ($(mkClosure 'pingClientRemote) ()) pingClientLocal +-- > callable _ = error "callable: unknown" +-- > +-- > spawnable :: String -> IO (RemoteProcess ()) +-- > spawnable "server" = return $ ($(mkClosure 'pingServer) ()) +-- > spawnable _ = error "spawnable: unknown" +-- +-- The ping server stores its PID in a file, which the ping client attempts to +-- read. This kind of pattern is typical, and is provided for in the high-level API. +-- +-- [Using the High-Level API] +-- +-- TODO: Does not yet exist. module Control.Distributed.Process.Backend.Azure ( -- * Initialization Backend(..) From e1f651dafa74409f12fd5fbab63d3401aeed52cc Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 13:34:38 +0100 Subject: [PATCH 0187/2357] Minor fixes --- .../Distributed/Process/Backend/Azure.hs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 7707ab65..4819a40b 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -22,7 +22,8 @@ -- machine using the standard Cloud Haskell primitives. In an ideal world, we -- could just start a Cloud Haskell node on the local machine, too. -- Unfortunately, Cloud Haskell does not yet support using multiple network --- transports (TCP/IP vs SSH). This is a temporary workaround. +-- transports within the same system (i.e. both TCP/IP and SSH). This is a +-- temporary workaround. -- -- [Azure Setup] -- @@ -45,11 +46,11 @@ -- /as well as on your local machine/. We use Ubuntu Server 12.04 LTS for our -- tests (running on VirtualBox on our local machine). -- --- When you set up your virtual machine, you can pick an arbitrary name; these --- names are for your own use only and do not need to be globally unique. Set a --- username and password; you should use the same username on all virtual --- machines. You should also upload --- an SSH key for authentication (see +-- When you set up your virtual machine, you can pick an arbitrary virtual +-- machine name; these names are for your own use only and do not need to be +-- globally unique. Set a username and password; you should use the same +-- username on all virtual machines. You should also upload an SSH key for +-- authentication (see -- /Converting OpenSSH keys for use on Windows Azure Linux VM's/, -- , for -- information on how to convert a standard Linux @id_rsa.pub@ public key to @@ -63,8 +64,8 @@ -- Once your virtual machines have been set up, you have to make sure that the -- user you created when you created the VM can ssh from any virtual machine to -- any other using public key authentication. Moreover, you have to make sure --- that @libssh2@ is installed; if you are using the Ubuntu image we recommend --- you can install @libssh2@ using +-- that @libssh2@ is installed; if you are using the Ubuntu image you can +-- install @libssh2@ using -- -- > sudo apt-get install libssh2-1 -- @@ -72,7 +73,7 @@ -- message.) -- -- In these notes, we assume three virtual machines called @CHDemo1@, --- @CHDemo2@, and @CHDemo3@, all part of the @CloudHaskellDemo@ cloud service. +-- @CHDemo2@, and @CHDemo3@, all part of a @CloudHaskellDemo@ cloud service. -- -- [Obtaining a Management Certificate] -- @@ -161,8 +162,9 @@ -- > type RemoteProcess a = Closure (Backend -> Process a) -- -- (If you don't know what a 'Closure' is you should read --- "Control.Distributed.Process.Closure".) 'spawnOnVM' terminates once the --- Cloud Haskell node has been set up. +-- "Control.Distributed.Process.Closure".); the remote process will be supplied +-- with an Azure backend initialized with the same parameters. 'spawnOnVM' +-- returns once the Cloud Haskell node has been set up. -- -- 'callOnVM' is similar to 'spawnOnVM', but it takes a /pair/ of processes: -- one to run on the remote host (on a newly created Cloud Haskell node), and @@ -183,7 +185,7 @@ -- Before you can try it you will first need to copy the executable to the remote server: -- -- > cloud-haskell-azure-echo install \ --- > --subscription-id < \ +-- > --subscription-id <> \ -- > --certificate /path/to/credentials.x509 \ -- > --private /path/to/credentials.private \ -- > --user <> \ @@ -195,7 +197,7 @@ -- you can run it as follows: -- -- > cloud-haskell-azure-echo run \ --- > --subscription-id < \ +-- > --subscription-id <> \ -- > --certificate /path/to/credentials.x509 \ -- > --private /path/to/credentials.private \ -- > --user <> \ @@ -210,7 +212,7 @@ -- > # -- -- "Control.Distributed.Process.Backend.Azure.GenericMain" provides a generic --- main function that you can to structure your code. It provides command line +-- main function that you can use to structure your code. It provides command line -- arguments such as the ones we saw in section /Testing the Setup/, it -- initializes the Azure backend, and it takes care of running your code on the -- remote machines. You don't have to use 'genericMain' if you prefer not to, @@ -286,7 +288,7 @@ -- executable: -- -- > cloud-haskell-azure-ping install \ --- > --subscription-id < \ +-- > --subscription-id <> \ -- > --certificate /path/to/credentials.x509 \ -- > --private /path/to/credentials.private \ -- > --user <> \ @@ -300,7 +302,7 @@ -- @--virtual-machine@ argument): -- -- > cloud-haskell-azure-ping run \ --- > --subscription-id < \ +-- > --subscription-id <> \ -- > --certificate /path/to/credentials.x509 \ -- > --private /path/to/credentials.private \ -- > --user <> \ @@ -315,7 +317,7 @@ -- Finally, we can run the ping client: -- -- > cloud-haskell-azure-ping run --- > --subscription-id < \ +-- > --subscription-id <> \ -- > --certificate /path/to/credentials.x509 \ -- > --private /path/to/credentials.private \ -- > --user <> \ @@ -329,7 +331,7 @@ -- Note that we must pass a different port number, because the client will run -- within its own Cloud Haskell instance. -- --- The code for the ping server is similar to the echo server, but demonstrates +-- The code for the ping demo is similar to the echo demo, but demonstrates -- both 'callable' and 'spawnable' and shows one way to discover nodes. -- -- > {-# LANGUAGE TemplateHaskell #-} From b91718cd4d320b240afc7aec5d5b587258e84bf0 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 17:11:58 +0100 Subject: [PATCH 0188/2357] Get rid of genericMain --- demos/Conway.hs | 30 +- demos/Echo.hs | 30 +- demos/Ping.hs | 36 +- distributed-process-azure.cabal | 3 +- .../Distributed/Process/Backend/Azure.hs | 371 +++++++++------- .../Process/Backend/Azure/GenericMain.hs | 407 ------------------ 6 files changed, 268 insertions(+), 609 deletions(-) delete mode 100644 src/Control/Distributed/Process/Backend/Azure/GenericMain.hs diff --git a/demos/Conway.hs b/demos/Conway.hs index f4dafd7d..f885b938 100644 --- a/demos/Conway.hs +++ b/demos/Conway.hs @@ -1,4 +1,5 @@ {-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-} +import System.Environment (getArgs) import Data.Data (Typeable, Data) import Data.Binary (Binary(get, put)) import Data.Binary.Generic (getGeneric, putGeneric) @@ -13,14 +14,6 @@ import Control.Distributed.Process.Closure , mkClosure ) import Control.Distributed.Process.Backend.Azure - ( Backend(findVMs) - , ProcessPair(ProcessPair) - , RemoteProcess - , LocalProcess - , remoteSend - , localExpect - ) -import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) data ControllerMsg = ControllerExit @@ -43,11 +36,16 @@ echo = forever $ do liftIO $ putStrLn msg main :: IO () -main = genericMain __remoteTable callable spawnable - where - callable :: String -> IO (ProcessPair ()) - callable "start" = return $ ProcessPair ($(mkClosure 'conwayStart) ()) echo - callable _ = error "callable: unknown" - - spawnable :: String -> IO (RemoteProcess ()) - spawnable _ = error "spawnable: unknown" +main = do + args <- getArgs + case args of + "onvm":args' -> onVmMain __remoteTable args' + cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do + params <- defaultAzureParameters sid x509 pkey + let params' = params { azureSshUserName = user } + backend <- initializeBackend params' cloudService + Just vm <- findNamedVM backend virtualMachine + case cmd of + "start" -> callOnVM backend vm port $ + ProcessPair ($(mkClosure 'conwayStart) ()) + echo diff --git a/demos/Echo.hs b/demos/Echo.hs index b5809668..0a6289a0 100644 --- a/demos/Echo.hs +++ b/demos/Echo.hs @@ -1,20 +1,12 @@ {-# LANGUAGE TemplateHaskell #-} import System.IO (hFlush, stdout) +import System.Environment (getArgs) import Control.Monad (unless, forever) import Control.Monad.IO.Class (liftIO) import Control.Distributed.Process (Process, expect) import Control.Distributed.Process.Closure (remotable, mkClosure) import Control.Distributed.Process.Backend.Azure - ( Backend - , ProcessPair(..) - , RemoteProcess - , LocalProcess - , localExpect - , remoteSend - , localSend - ) -import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) echoRemote :: () -> Backend -> Process () echoRemote () _backend = forever $ do @@ -34,11 +26,15 @@ echoLocal = do echoLocal main :: IO () -main = genericMain __remoteTable callable spawnable - where - callable :: String -> IO (ProcessPair ()) - callable "echo" = return $ ProcessPair ($(mkClosure 'echoRemote) ()) echoLocal - callable _ = error "callable: unknown" - - spawnable :: String -> IO (RemoteProcess ()) - spawnable _ = error "spawnable: unknown" +main = do + args <- getArgs + case args of + "onvm":args' -> onVmMain __remoteTable args' + sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do + params <- defaultAzureParameters sid x509 pkey + let params' = params { azureSshUserName = user } + backend <- initializeBackend params' cloudService + Just vm <- findNamedVM backend virtualMachine + callOnVM backend vm port $ + ProcessPair ($(mkClosure 'echoRemote) ()) + echoLocal diff --git a/demos/Ping.hs b/demos/Ping.hs index 5e321b87..2f115f31 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -1,5 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} +import System.Environment (getArgs) import Data.Binary (encode, decode) import Control.Monad (forever) import Control.Monad.IO.Class (liftIO) @@ -16,14 +17,6 @@ import Control.Distributed.Process ) import Control.Distributed.Process.Closure (remotable, mkClosure) import Control.Distributed.Process.Backend.Azure - ( Backend - , ProcessPair(..) - , RemoteProcess - , LocalProcess - , localExpect - , remoteSend - ) -import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) pingServer :: () -> Backend -> Process () @@ -59,12 +52,21 @@ pingClientLocal :: LocalProcess () pingClientLocal = localExpect >>= liftIO . putStrLn main :: IO () -main = genericMain __remoteTable callable spawnable - where - callable :: String -> IO (ProcessPair ()) - callable "client" = return $ ProcessPair ($(mkClosure 'pingClientRemote) ()) pingClientLocal - callable _ = error "callable: unknown" - - spawnable :: String -> IO (RemoteProcess ()) - spawnable "server" = return $ ($(mkClosure 'pingServer) ()) - spawnable _ = error "spawnable: unknown" +main = do + args <- getArgs + case args of + "onvm":args' -> onVmMain __remoteTable args' + "list":sid:x509:pkey:_ -> do + params <- defaultAzureParameters sid x509 pkey + css <- cloudServices (azureSetup params) + mapM_ print css + cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do + params <- defaultAzureParameters sid x509 pkey + let params' = params { azureSshUserName = user } + backend <- initializeBackend params' cloudService + Just vm <- findNamedVM backend virtualMachine + case cmd of + "server" -> spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) + "client" -> callOnVM backend vm port $ + ProcessPair ($(mkClosure 'pingClientRemote) ()) + pingClientLocal diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 7544ad52..e87d7bf8 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -39,8 +39,7 @@ Library unix >= 2.5 && < 2.6, network-transport >= 0.2 && < 0.3, mtl >= 2.1 && < 2.2 - Exposed-modules: Control.Distributed.Process.Backend.Azure, - Control.Distributed.Process.Backend.Azure.GenericMain + Exposed-modules: Control.Distributed.Process.Backend.Azure Extensions: ViewPatterns, RankNTypes, ExistentialQuantification, diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 4819a40b..a5508119 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -103,13 +103,13 @@ -- -- > cabal-dev install distributed-process-azure -f build-demos -- --- We can use any of the demos to test our setup; we will use the @ping@ demo: +-- We can the @cloud-haskell-azure-ping@ demo to test our setup: -- -- > cloud-haskell-azure-ping list \ --- > --subscription-id <> \ --- > --certificate /path/to/credentials.x509 \ --- > --private /path/to/credentials.private --- +-- > <> \ +-- > /path/to/credentials.x509 \ +-- > /path/to/credentials.private +-- -- (you can find your subscription ID in the @.publishsettings@ file from the previous step). -- If everything went well, this will output something like -- @@ -182,63 +182,35 @@ -- machine will echo this back; the local machine will wait for the echo, show -- the echo, and repeat. -- --- Before you can try it you will first need to copy the executable to the remote server: --- --- > cloud-haskell-azure-echo install \ --- > --subscription-id <> \ --- > --certificate /path/to/credentials.x509 \ --- > --private /path/to/credentials.private \ --- > --user <> \ --- > --cloud-service <> \ --- > --virtual-machine <> --- --- (If you leave out the @--virtual-machine@ argument the binary will be copied --- to every virtual machine in the specified cloud service). Once installed, --- you can run it as follows: --- --- > cloud-haskell-azure-echo run \ --- > --subscription-id <> \ --- > --certificate /path/to/credentials.x509 \ --- > --private /path/to/credentials.private \ --- > --user <> \ --- > --cloud-service <> \ --- > --virtual-machine <> \ --- > --port 8080 \ --- > --closure echo --- > CHDemo1: # Everything I type will be echoed back --- > Echo: Everything I type will be echoed back +-- Before you can try it you will first need to copy the executable (for +-- example, using scp, although the Azure backend also provides this natively +-- in Haskell). Once that's done, you can run the demo as follows: +-- +-- > cloud-haskell-azure-echo \ +-- > <> \ +-- > /path/to/credentials.x509 \ +-- > /path/to/credentials.private \ +-- > <> \ +-- > <> \ +-- > <> \ +-- > <> +-- > # Everything I type gets echoed back +-- > Echo: Everything I type gets echoed back -- > # Until I enter a blank line -- > Echo: Until I enter a blank line -- > # -- --- "Control.Distributed.Process.Backend.Azure.GenericMain" provides a generic --- main function that you can use to structure your code. It provides command line --- arguments such as the ones we saw in section /Testing the Setup/, it --- initializes the Azure backend, and it takes care of running your code on the --- remote machines. You don't have to use 'genericMain' if you prefer not to, --- but then it will be your own responsibility to initialize Azure and to make --- sure that your executable does the right thing when it's invoked on the --- remote node. In these notes we will assume that you will use 'genericMain'. --- -- The full @echo@ demo is -- -- > {-# LANGUAGE TemplateHaskell #-} -- > -- > import System.IO (hFlush, stdout) +-- > import System.Environment (getArgs) -- > import Control.Monad (unless, forever) -- > import Control.Monad.IO.Class (liftIO) -- > import Control.Distributed.Process (Process, expect) -- > import Control.Distributed.Process.Closure (remotable, mkClosure) -- > import Control.Distributed.Process.Backend.Azure --- > ( Backend --- > , ProcessPair(..) --- > , RemoteProcess --- > , LocalProcess --- > , localExpect --- > , remoteSend --- > , localSend --- > ) --- > import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) -- > -- > echoRemote :: () -> Backend -> Process () -- > echoRemote () _backend = forever $ do @@ -258,84 +230,61 @@ -- > echoLocal -- > -- > main :: IO () --- > main = genericMain __remoteTable callable spawnable --- > where --- > callable :: String -> IO (ProcessPair ()) --- > callable "echo" = return $ ProcessPair ($(mkClosure 'echoRemote) ()) echoLocal --- > callable _ = error "callable: unknown" --- > --- > spawnable :: String -> IO (RemoteProcess ()) --- > spawnable _ = error "spawnable: unknown" --- --- 'genericMain' expects three arguments: the first is the standard --- '__remoteTable' argument familiar from --- "Control.Distributed.Process.Closure"; the second and third should map --- strings to process pairs (for use with 'callOnVM') or remote processes (for --- use with 'spawnOnVM') respectively. --- --- When you invoke the @echo@ demo with @--closure echo@ the 'genericMain' --- function calls 'callOnVM' with the process pair consisting of 'echoRemote' --- and 'echoLocal'. Hopefully the definition of these two functions is --- self-explanatory. +-- > main = do +-- > args <- getArgs +-- > case args of +-- > "onvm":args' -> onVmMain __remoteTable args' +-- > sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do +-- > params <- defaultAzureParameters sid x509 pkey +-- > let params' = params { azureSshUserName = user } +-- > backend <- initializeBackend params' cloudService +-- > Just vm <- findNamedVM backend virtualMachine +-- > callOnVM backend vm port $ +-- > ProcessPair ($(mkClosure 'echoRemote) ()) +-- > echoLocal -- -- [Second Example: Ping] -- -- The second example differs from the @echo@ demo in that it uses both --- 'callable' ('callOnVM') and 'spawnable' ('spawnOnVM'). It uses the latter to +-- 'callOnVM' and 'spawnOnVM'. It uses the latter to -- install a ping server which keeps running in the background; it uses the -- former to run a ping client which sends a request to the ping server and --- outputs the response. As with the @echo@ server, we must first copy the --- executable: --- --- > cloud-haskell-azure-ping install \ --- > --subscription-id <> \ --- > --certificate /path/to/credentials.x509 \ --- > --private /path/to/credentials.private \ --- > --user <> \ --- > --cloud-service <> \ --- > CHDemo3: Done --- > CHDemo2: Done --- > CHDemo1: Done --- --- Now we can start the ping server on every virtual machine in the cloud --- service (to install it to a single virtual machine only, pass the --- @--virtual-machine@ argument): --- --- > cloud-haskell-azure-ping run \ --- > --subscription-id <> \ --- > --certificate /path/to/credentials.x509 \ --- > --private /path/to/credentials.private \ --- > --user <> \ --- > --cloud-service <> \ --- > --port 8080 --- > --closure server --- > --background --- > CHDemo3: OK --- > CHDemo2: OK --- > CHDemo1: OK +-- outputs the response. +-- +-- As with the @echo@ demo, make sure to copy the executable to the remote server first. +-- Once that is done, you can start a ping server on a virtual machine using +-- +-- > cloud-haskell-azure-ping server \ +-- > <> \ +-- > /path/to/credentials.x509 \ +-- > /path/to/credentials.private \ +-- > <> \ +-- > <> \ +-- > <> \ +-- > <> -- -- Finally, we can run the ping client: -- --- > cloud-haskell-azure-ping run --- > --subscription-id <> \ --- > --certificate /path/to/credentials.x509 \ --- > --private /path/to/credentials.private \ --- > --user <> \ --- > --cloud-service <> \ --- > --port 8081 --- > --closure client --- > CHDemo3: Ping server at pid://10.119.182.127:8080:0:2 ok --- > CHDemo2: Ping server at pid://10.59.238.125:8080:0:2 ok --- > CHDemo1: Ping server at pid://10.59.224.122:8080:0:2 ok +-- > cloud-haskell-azure-ping client \ +-- > <> \ +-- > /path/to/credentials.x509 \ +-- > /path/to/credentials.private \ +-- > <> \ +-- > <> \ +-- > <> \ +-- > <> +-- > Ping server at pid://10.59.224.122:8080:0:2 ok -- -- Note that we must pass a different port number, because the client will run -- within its own Cloud Haskell instance. -- --- The code for the ping demo is similar to the echo demo, but demonstrates --- both 'callable' and 'spawnable' and shows one way to discover nodes. +-- The code for the @ping@ demo is similar to the @echo@ demo, but uses both +-- 'callOnVM' and 'spawnOnVM' and demonstrates a way to discover processes (in +-- this case, through a PID file). -- -- > {-# LANGUAGE TemplateHaskell #-} -- > +-- > import System.Environment (getArgs) -- > import Data.Binary (encode, decode) -- > import Control.Monad (forever) -- > import Control.Monad.IO.Class (liftIO) @@ -352,14 +301,6 @@ -- > ) -- > import Control.Distributed.Process.Closure (remotable, mkClosure) -- > import Control.Distributed.Process.Backend.Azure --- > ( Backend --- > , ProcessPair(..) --- > , RemoteProcess --- > , LocalProcess --- > , localExpect --- > , remoteSend --- > ) --- > import Control.Distributed.Process.Backend.Azure.GenericMain (genericMain) -- > import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) -- > -- > pingServer :: () -> Backend -> Process () @@ -395,28 +336,34 @@ -- > pingClientLocal = localExpect >>= liftIO . putStrLn -- > -- > main :: IO () --- > main = genericMain __remoteTable callable spawnable --- > where --- > callable :: String -> IO (ProcessPair ()) --- > callable "client" = return $ ProcessPair ($(mkClosure 'pingClientRemote) ()) pingClientLocal --- > callable _ = error "callable: unknown" --- > --- > spawnable :: String -> IO (RemoteProcess ()) --- > spawnable "server" = return $ ($(mkClosure 'pingServer) ()) --- > spawnable _ = error "spawnable: unknown" --- --- The ping server stores its PID in a file, which the ping client attempts to --- read. This kind of pattern is typical, and is provided for in the high-level API. --- --- [Using the High-Level API] --- --- TODO: Does not yet exist. +-- > main = do +-- > args <- getArgs +-- > case args of +-- > "onvm":args' -> onVmMain __remoteTable args' +-- > "list":sid:x509:pkey:_ -> do +-- > params <- defaultAzureParameters sid x509 pkey +-- > css <- cloudServices (azureSetup params) +-- > mapM_ print css +-- > cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do +-- > params <- defaultAzureParameters sid x509 pkey +-- > let params' = params { azureSshUserName = user } +-- > backend <- initializeBackend params' cloudService +-- > Just vm <- findNamedVM backend virtualMachine +-- > case cmd of +-- > "server" -> spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) +-- > "client" -> callOnVM backend vm port $ +-- > ProcessPair ($(mkClosure 'pingClientRemote) ()) +-- > pingClientLocal module Control.Distributed.Process.Backend.Azure ( -- * Initialization Backend(..) , AzureParameters(..) , defaultAzureParameters , initializeBackend + -- * Utilities + , findNamedVM + -- * On-VM main + , onVmMain -- * Re-exports from Azure Service Management , CloudService(..) , VirtualMachine(..) @@ -433,10 +380,22 @@ module Control.Distributed.Process.Backend.Azure , remoteThrow ) where +import Prelude hiding (catch) import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) -import System.IO (stdout, hFlush) +import System.IO + ( stdout + , hFlush + , hSetBinaryMode + , stdin + , stdout + , stderr + , Handle + , hClose + ) +import qualified System.Posix.Process as Posix (forkProcess, createSession) +import Data.Maybe (listToMaybe) import Data.Binary (Binary(get, put), encode, decode) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString as BSS @@ -444,6 +403,7 @@ import qualified Data.ByteString as BSS , length , concat , hPut + , hGet ) import qualified Data.ByteString.Char8 as BSSC (pack) import qualified Data.ByteString.Lazy as BSL @@ -453,14 +413,23 @@ import qualified Data.ByteString.Lazy as BSL , fromChunks , toChunks , hPut + , hGet ) import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) +import Data.Foldable (forM_) import Control.Applicative ((<$>), (<*>)) import Control.Monad (void, unless) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) -import Control.Exception (Exception, catches, Handler(Handler), throwIO) +import Control.Exception + ( Exception + , catches + , Handler(Handler) + , throwIO + , SomeException + ) import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, readMVar, putMVar) -- Azure import Network.Azure.ServiceManagement @@ -499,12 +468,30 @@ import qualified Network.SSH.Client.LibSSH2.Errors as SSH ) -- CH -import Control.Distributed.Process (Process, Closure) +import Control.Distributed.Process + ( Process + , Closure + , RemoteTable + , catch + , unClosure + ) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Process.Internal.Types - ( messageToPayload +import qualified Control.Distributed.Process.Internal.Types as CH + ( LocalNode + , LocalProcess(processQueue) + , Message + , payloadToMessage + , messageToPayload , createMessage ) +import Control.Distributed.Process.Node + ( runProcess + , forkProcess + , newLocalNode + , initRemoteTable + ) +import Control.Distributed.Process.Internal.CQueue (CQueue, enqueue) +import Network.Transport.TCP (createTransport, defaultTCPParameters) import Network.Transport.Internal (encodeInt32, decodeInt32, prependLength) -- | Azure backend @@ -609,10 +596,11 @@ apiCallOnVM :: AzureParameters apiCallOnVM params cloudService vm port ppair = withSSH2 params vm $ \s -> do let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm run " - ++ " --host " ++ vmIpAddress vm - ++ " --port " ++ port - ++ " --cloud-service " ++ cloudService + ++ " onvm" + ++ " " ++ vmIpAddress vm + ++ " " ++ port + ++ " " ++ cloudService + ++ " False" ++ " 2>&1" let paramsEnc = encode params (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do @@ -639,11 +627,11 @@ apiSpawnOnVM params cloudService vm port proc = withSSH2 params vm $ \s -> do -- TODO: reduce duplication with apiCallOnVM let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm run " - ++ " --host " ++ vmIpAddress vm - ++ " --port " ++ port - ++ " --cloud-service " ++ cloudService - ++ " --background " + ++ " onvm" + ++ " " ++ vmIpAddress vm + ++ " " ++ port + ++ " " ++ cloudService + ++ " True" ++ " 2>&1" let paramsEnc = encode params (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do @@ -702,6 +690,15 @@ catchSshError s io = localHash :: AzureParameters -> IO MD5Digest localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) +-------------------------------------------------------------------------------- +-- Utilities -- +-------------------------------------------------------------------------------- + +-- | Find a virtual machine with a particular name +findNamedVM :: Backend -> String -> IO (Maybe VirtualMachine) +findNamedVM backend vm = + listToMaybe . filter ((== vm) . vmName) <$> findVMs backend + -------------------------------------------------------------------------------- -- Local and remote processes -- -------------------------------------------------------------------------------- @@ -735,8 +732,8 @@ localSend x = LocalProcess $ do ch <- ask liftIO $ mapM_ (SSH.writeChannel ch) . prependLength - . messageToPayload - . createMessage + . CH.messageToPayload + . CH.createMessage $ x -- | Wait for a message from the remote process (see 'ProcessPair'). @@ -773,6 +770,80 @@ remoteSend' flags x = do BSL.hPut stdout enc hFlush stdout +-------------------------------------------------------------------------------- +-- On-VM main -- +-------------------------------------------------------------------------------- + +-- | Program main when run on the VM. A typical 'main' function looks like +-- +-- > main :: IO () +-- > main = do +-- > args <- getArgs +-- > case args of +-- > "onvm":args' -> onVmMain __remoteTable args' +-- > _ -> -- your normal main +onVmMain :: (RemoteTable -> RemoteTable) -> [String] -> IO () +onVmMain rtable [host, port, cloudService, bg] = do + hSetBinaryMode stdin True + hSetBinaryMode stdout True + Just procEnc <- getWithLength stdin + Just paramsEnc <- getWithLength stdin + backend <- initializeBackend (decode paramsEnc) cloudService + let proc = decode procEnc + lprocMVar <- newEmptyMVar :: IO (MVar CH.LocalProcess) + if read bg + then detach $ startCH proc lprocMVar backend runProcess (\_ -> return ()) + else do + startCH proc lprocMVar backend forkProcess (liftIO . remoteThrow) + lproc <- readMVar lprocMVar + queueFromHandle stdin (CH.processQueue lproc) + where + startCH :: RemoteProcess () + -> MVar CH.LocalProcess + -> Backend + -> (CH.LocalNode -> Process () -> IO a) + -> (SomeException -> Process ()) + -> IO () + startCH rproc lprocMVar backend go exceptionHandler = do + mTransport <- createTransport host port defaultTCPParameters + case mTransport of + Left err -> remoteThrow err + Right transport -> do + node <- newLocalNode transport (rtable initRemoteTable) + void . go node $ do + ask >>= liftIO . putMVar lprocMVar + proc <- unClosure rproc :: Process (Backend -> Process ()) + catch (proc backend) exceptionHandler +onVmMain _ _ + = error "Invalid arguments passed on onVmMain" + +-- | Read a 4-byte length @l@ and then an @l@-byte payload +-- +-- Returns Nothing on EOF +getWithLength :: Handle -> IO (Maybe BSL.ByteString) +getWithLength h = do + lenEnc <- BSS.hGet h 4 + if BSS.length lenEnc < 4 + then return Nothing + else do + let len = decodeInt32 lenEnc + bs <- BSL.hGet h len + if BSL.length bs < fromIntegral len + then return Nothing + else return (Just bs) + +queueFromHandle :: Handle -> CQueue CH.Message -> IO () +queueFromHandle h q = do + mPayload <- getWithLength stdin + forM_ mPayload $ \payload -> do + enqueue q $ CH.payloadToMessage (BSL.toChunks payload) + queueFromHandle h q + +detach :: IO () -> IO () +detach io = do + mapM_ hClose [stdin, stdout, stderr] + void . Posix.forkProcess $ void Posix.createSession >> io + -------------------------------------------------------------------------------- -- SSH utilities -- -------------------------------------------------------------------------------- diff --git a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs b/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs deleted file mode 100644 index 1630c8f3..00000000 --- a/src/Control/Distributed/Process/Backend/Azure/GenericMain.hs +++ /dev/null @@ -1,407 +0,0 @@ --- | Generic main -module Control.Distributed.Process.Backend.Azure.GenericMain - ( genericMain - , ProcessPair(..) - ) where - -import Prelude hiding (catch) -import System.Exit (exitSuccess, exitFailure) -import System.IO - ( hFlush - , stdout - , stdin - , stderr - , hSetBinaryMode - , hClose - , Handle - ) -import Data.Foldable (forM_) -import Data.Binary (decode) -import qualified Data.ByteString.Lazy as BSL (ByteString, hGet, toChunks, length) -import qualified Data.ByteString as BSS (hGet, length) -import Control.Monad (unless, forM, void) -import Control.Monad.Reader (ask) -import Control.Exception (SomeException) -import Control.Applicative ((<$>), (<*>), optional) -import Control.Monad.IO.Class (liftIO) -import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, readMVar) - --- Posix -import qualified System.Posix.Process as Posix (forkProcess, createSession) - --- SSH -import qualified Network.SSH.Client.LibSSH2.Foreign as SSH - ( initialize - , exit - ) - --- Command line options -import Options.Applicative - ( Parser - , strOption - , long - , (&) - , metavar - , help - , subparser - , command - , info - , progDesc - , execParser - , helper - , fullDesc - , header - , switch - ) - --- CH -import Control.Distributed.Process - ( RemoteTable - , Process - , unClosure - , catch - ) -import Control.Distributed.Process.Node - ( newLocalNode - , runProcess - , forkProcess - , initRemoteTable - , LocalNode - ) -import Control.Distributed.Process.Internal.Types - ( LocalProcess(processQueue) - , payloadToMessage - , Message - ) -import Control.Distributed.Process.Internal.CQueue (CQueue, enqueue) - --- Azure -import Control.Distributed.Process.Backend.Azure - ( AzureParameters(azureSshUserName, azureSetup) - , defaultAzureParameters - , initializeBackend - , cloudServices - , VirtualMachine(vmName) - , Backend(findVMs, copyToVM, checkMD5, callOnVM, spawnOnVM) - , ProcessPair(..) - , RemoteProcess - , remoteThrow - ) - --- Transport -import Network.Transport.Internal (decodeInt32) -import Network.Transport.TCP (createTransport, defaultTCPParameters) - --------------------------------------------------------------------------------- --- Main -- --------------------------------------------------------------------------------- - -genericMain :: (RemoteTable -> RemoteTable) -- ^ Standard CH remote table - -> (String -> IO (ProcessPair ())) -- ^ Closures to support in 'run' - -> (String -> IO (RemoteProcess ())) -- ^ Closures to support in @run --background@ - -> IO () -genericMain remoteTable callable spawnable = do - _ <- SSH.initialize True - cmd <- execParser opts - case cmd of - List {} -> do - params <- azureParameters (azureOptions cmd) Nothing - css <- cloudServices (azureSetup params) - mapM_ print css - CopyTo {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params (targetService (target cmd)) - vms <- findMatchingVMs backend (targetVM (target cmd)) - forM_ vms $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - copyToVM backend vm - putStrLn "Done" - CheckMD5 {} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params (targetService (target cmd)) - vms <- findMatchingVMs backend (targetVM (target cmd)) - matches <- forM vms $ \vm -> do - unless (status cmd) $ putStr (vmName vm ++ ": ") >> hFlush stdout - match <- checkMD5 backend vm - unless (status cmd) $ putStrLn $ if match then "OK" else "FAILED" - return match - if and matches - then exitSuccess - else exitFailure - RunOn {} | background cmd -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params (targetService (target cmd)) - vms <- findMatchingVMs backend (targetVM (target cmd)) - rProc <- spawnable (closureId cmd) - forM_ vms $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - spawnOnVM backend vm (remotePort cmd) rProc - putStrLn "OK" - RunOn {} {- not (background cmd) -} -> do - params <- azureParameters (azureOptions cmd) (Just (sshOptions cmd)) - backend <- initializeBackend params (targetService (target cmd)) - vms <- findMatchingVMs backend (targetVM (target cmd)) - procPair <- callable (closureId cmd) - forM_ vms $ \vm -> do - putStr (vmName vm ++ ": ") >> hFlush stdout - callOnVM backend vm (remotePort cmd) procPair - OnVmCommand (vmCmd@OnVmRun {}) -> - onVmRun (remoteTable initRemoteTable) - (onVmIP vmCmd) - (onVmPort vmCmd) - (onVmService vmCmd) - (onVmBackground vmCmd) - SSH.exit - where - opts = info (helper <*> commandParser) - ( fullDesc - & header "Cloud Haskell backend for Azure" - ) - -findMatchingVMs :: Backend -> Maybe String -> IO [VirtualMachine] -findMatchingVMs backend Nothing = findVMs backend -findMatchingVMs backend (Just vm) = filter ((== vm) . vmName) `fmap` findVMs backend - -azureParameters :: AzureOptions -> Maybe SshOptions -> IO AzureParameters -azureParameters opts Nothing = - defaultAzureParameters (subscriptionId opts) - (pathToCert opts) - (pathToKey opts) -azureParameters opts (Just sshOpts) = do - params <- defaultAzureParameters (subscriptionId opts) - (pathToCert opts) - (pathToKey opts) - return params { - azureSshUserName = remoteUser sshOpts - } - --------------------------------------------------------------------------------- --- Executing a closure on the VM -- --------------------------------------------------------------------------------- - -onVmRun :: RemoteTable -> String -> String -> String -> Bool -> IO () -onVmRun rtable host port cloudService bg = do - hSetBinaryMode stdin True - hSetBinaryMode stdout True - Just procEnc <- getWithLength stdin - Just paramsEnc <- getWithLength stdin - backend <- initializeBackend (decode paramsEnc) cloudService - let proc = decode procEnc - lprocMVar <- newEmptyMVar :: IO (MVar LocalProcess) - if bg - then detach $ startCH proc lprocMVar backend runProcess (\_ -> return ()) - else do - startCH proc lprocMVar backend forkProcess (liftIO . remoteThrow) - lproc <- readMVar lprocMVar - queueFromHandle stdin (processQueue lproc) - where - startCH :: RemoteProcess () - -> MVar LocalProcess - -> Backend - -> (LocalNode -> Process () -> IO a) - -> (SomeException -> Process ()) - -> IO () - startCH rproc lprocMVar backend go exceptionHandler = do - mTransport <- createTransport host port defaultTCPParameters - case mTransport of - Left err -> remoteThrow err - Right transport -> do - node <- newLocalNode transport rtable - void . go node $ do - ask >>= liftIO . putMVar lprocMVar - proc <- unClosure rproc :: Process (Backend -> Process ()) - catch (proc backend) exceptionHandler - --- | Read a 4-byte length @l@ and then an @l@-byte payload --- --- Returns Nothing on EOF -getWithLength :: Handle -> IO (Maybe BSL.ByteString) -getWithLength h = do - lenEnc <- BSS.hGet h 4 - if BSS.length lenEnc < 4 - then return Nothing - else do - let len = decodeInt32 lenEnc - bs <- BSL.hGet h len - if BSL.length bs < fromIntegral len - then return Nothing - else return (Just bs) - -queueFromHandle :: Handle -> CQueue Message -> IO () -queueFromHandle h q = do - mPayload <- getWithLength stdin - forM_ mPayload $ \payload -> do - enqueue q $ payloadToMessage (BSL.toChunks payload) - queueFromHandle h q - -detach :: IO () -> IO () -detach io = do - mapM_ hClose [stdin, stdout, stderr] - void . Posix.forkProcess $ void Posix.createSession >> io - --------------------------------------------------------------------------------- --- Command line options -- --------------------------------------------------------------------------------- - -data AzureOptions = AzureOptions { - subscriptionId :: String - , pathToCert :: FilePath - , pathToKey :: FilePath - } - deriving Show - -data SshOptions = SshOptions { - remoteUser :: String - } - deriving Show - -data Target = Target { - targetService :: String - , targetVM :: Maybe String - } - deriving Show - -data Command = - List { - azureOptions :: AzureOptions - } - | CopyTo { - azureOptions :: AzureOptions - , sshOptions :: SshOptions - , target :: Target - } - | CheckMD5 { - azureOptions :: AzureOptions - , sshOptions :: SshOptions - , target :: Target - , status :: Bool - } - | RunOn { - azureOptions :: AzureOptions - , sshOptions :: SshOptions - , target :: Target - , remotePort :: String - , closureId :: String - , background :: Bool - } - | OnVmCommand { - _onVmCommand :: OnVmCommand - } - deriving Show - -data OnVmCommand = - OnVmRun { - onVmIP :: String - , onVmPort :: String - , onVmService :: String - , onVmBackground :: Bool - } - deriving Show - -azureOptionsParser :: Parser AzureOptions -azureOptionsParser = AzureOptions - <$> strOption ( long "subscription-id" - & metavar "SID" - & help "Azure subscription ID" - ) - <*> strOption ( long "certificate" - & metavar "CERT" - & help "X509 certificate" - ) - <*> strOption ( long "private" - & metavar "PRI" - & help "Private key in PKCS#1 format" - ) - -sshOptionsParser :: Parser SshOptions -sshOptionsParser = SshOptions - <$> strOption ( long "user" - & metavar "USER" - & help "Remove SSH username" - ) - -listParser :: Parser Command -listParser = List <$> azureOptionsParser - -copyToParser :: Parser Command -copyToParser = CopyTo - <$> azureOptionsParser - <*> sshOptionsParser - <*> targetParser - -targetParser :: Parser Target -targetParser = Target - <$> strOption ( long "cloud-service" - & metavar "CS" - & help "Cloud service name" - ) - <*> optional (strOption ( long "virtual-machine" - & metavar "VM" - & help "Virtual machine name (all VMs if unspecified)" - )) - -checkMD5Parser :: Parser Command -checkMD5Parser = CheckMD5 - <$> azureOptionsParser - <*> sshOptionsParser - <*> targetParser - <*> switch ( long "status" - & help "Don't output anything, status code shows success" - ) - -commandParser :: Parser Command -commandParser = subparser - ( command "list" (info listParser - (progDesc "List Azure cloud services")) - & command "install" (info copyToParser - (progDesc "Install the executable")) - & command "md5" (info checkMD5Parser - (progDesc "Check if the remote and local MD5 hash match")) - & command "run" (info runOnParser - (progDesc "Run the executable")) - & command "onvm" (info onVmCommandParser - (progDesc "Commands used when running ON the vm (usually used internally only)")) - ) - -runOnParser :: Parser Command -runOnParser = RunOn - <$> azureOptionsParser - <*> sshOptionsParser - <*> targetParser - <*> strOption ( long "port" - & metavar "PORT" - & help "Port number of the CH instance" - ) - <*> strOption ( long "closure" - & metavar "PROC" - & help "Process to run on the CH instance" - ) - <*> switch ( long "background" - & help "Run the process in the background" - ) - -onVmRunParser :: Parser OnVmCommand -onVmRunParser = OnVmRun - <$> strOption ( long "host" - & metavar "IP" - & help "IP address" - ) - <*> strOption ( long "port" - & metavar "PORT" - & help "port number" - ) - <*> strOption ( long "cloud-service" - & metavar "CS" - & help "Cloud service name" - ) - <*> switch ( long "background" - & help "Run the process in the background" - ) - -onVmCommandParser :: Parser Command -onVmCommandParser = OnVmCommand <$> subparser - ( command "run" (info onVmRunParser - (progDesc "Run the executable")) - ) - - From 3d4ecde2d447bfbf052ed25edecdeadbef54410f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 21 Aug 2012 17:40:01 +0100 Subject: [PATCH 0189/2357] Improve docs --- .../Distributed/Process/Backend/Azure.hs | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index a5508119..4d640aba 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -177,10 +177,11 @@ -- -- [First Example: Echo] -- --- The @echo@ demo starts a new Cloud Haskell node, waits for input from the --- user on the local machine, sends this to the remote machine. The remote --- machine will echo this back; the local machine will wait for the echo, show --- the echo, and repeat. +-- When we run the @cloud-haskell-azure-echo@ demo on our local machine, it +-- starts a new Cloud Haskell node on the specified remote virtual machine. It +-- then repeatedly waits for input from the user on the local machine, sends +-- this to the remote virtual machine which will echo it back, and wait for and +-- show the echo. -- -- Before you can try it you will first need to copy the executable (for -- example, using scp, although the Azure backend also provides this natively @@ -233,16 +234,35 @@ -- > main = do -- > args <- getArgs -- > case args of --- > "onvm":args' -> onVmMain __remoteTable args' +-- > "onvm":args' -> +-- > -- Pass execution to 'onVmMain' if we are running on the VM +-- > -- ('callOnVM' will provide the right arguments) +-- > onVmMain __remoteTable args' +-- > -- > sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do +-- > -- Initialize the Azure backend -- > params <- defaultAzureParameters sid x509 pkey -- > let params' = params { azureSshUserName = user } -- > backend <- initializeBackend params' cloudService +-- > +-- > -- Find the specified virtual machine -- > Just vm <- findNamedVM backend virtualMachine +-- > +-- > -- Run the echo client proper -- > callOnVM backend vm port $ -- > ProcessPair ($(mkClosure 'echoRemote) ()) -- > echoLocal -- +-- The most important part of this code is the last three lines +-- +-- > callOnVM backend vm port $ +-- > ProcessPair ($(mkClosure 'echoRemote) ()) +-- > echoLocal +-- +-- 'callOnVM' creats a new Cloud Haskell node on the specified virtual machine, +-- then runs @echoRemote@ on the remote machine and @echoLocal@ on the local +-- machine. +-- -- [Second Example: Ping] -- -- The second example differs from the @echo@ demo in that it uses both @@ -263,7 +283,13 @@ -- > <> \ -- > <> -- --- Finally, we can run the ping client: +-- As before, when we execute this on our local machine, it starts a new Cloud +-- Haskell node on the specified remote virtual machine and then executes the +-- ping server. Unlike with the echo example, however, this command will +-- terminate once the Cloud Haskell node has been set up, leaving the ping +-- server running in the background. +-- +-- Once the ping server is running we can run the ping client: -- -- > cloud-haskell-azure-ping client \ -- > <> \ @@ -339,16 +365,26 @@ -- > main = do -- > args <- getArgs -- > case args of --- > "onvm":args' -> onVmMain __remoteTable args' +-- > "onvm":args' -> +-- > -- Pass execution to 'onVmMain' if we are running on the VM +-- > onVmMain __remoteTable args' +-- > -- > "list":sid:x509:pkey:_ -> do +-- > -- List all available cloud services +-- > -- (useful, but not strictly necessary for the example) -- > params <- defaultAzureParameters sid x509 pkey -- > css <- cloudServices (azureSetup params) -- > mapM_ print css +-- > -- > cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do +-- > -- Initialize the backend and find the right VM -- > params <- defaultAzureParameters sid x509 pkey -- > let params' = params { azureSshUserName = user } -- > backend <- initializeBackend params' cloudService -- > Just vm <- findNamedVM backend virtualMachine +-- > +-- > -- The same binary can behave as the client or the server, +-- > -- depending on the command line arguments -- > case cmd of -- > "server" -> spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) -- > "client" -> callOnVM backend vm port $ From fb9f9a986a4f3fec3b89800624da2e254eb48d0c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 22 Aug 2012 09:06:08 +0100 Subject: [PATCH 0190/2357] Add comments from the docs to the demos --- demos/Echo.hs | 11 ++++++++++- demos/Ping.hs | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/demos/Echo.hs b/demos/Echo.hs index 0a6289a0..0790fe71 100644 --- a/demos/Echo.hs +++ b/demos/Echo.hs @@ -29,12 +29,21 @@ main :: IO () main = do args <- getArgs case args of - "onvm":args' -> onVmMain __remoteTable args' + "onvm":args' -> + -- Pass execution to 'onVmMain' if we are running on the VM + -- ('callOnVM' will provide the right arguments) + onVmMain __remoteTable args' + sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do + -- Initialize the Azure backend params <- defaultAzureParameters sid x509 pkey let params' = params { azureSshUserName = user } backend <- initializeBackend params' cloudService + + -- Find the specified virtual machine Just vm <- findNamedVM backend virtualMachine + + -- Run the echo client proper callOnVM backend vm port $ ProcessPair ($(mkClosure 'echoRemote) ()) echoLocal diff --git a/demos/Ping.hs b/demos/Ping.hs index 2f115f31..0db4ebf2 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -55,16 +55,26 @@ main :: IO () main = do args <- getArgs case args of - "onvm":args' -> onVmMain __remoteTable args' + "onvm":args' -> + -- Pass execution to 'onVmMain' if we are running on the VM + onVmMain __remoteTable args' + "list":sid:x509:pkey:_ -> do + -- List all available cloud services + -- (useful, but not strictly necessary for the example) params <- defaultAzureParameters sid x509 pkey css <- cloudServices (azureSetup params) mapM_ print css + cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do + -- Initialize the backend and find the right VM params <- defaultAzureParameters sid x509 pkey let params' = params { azureSshUserName = user } backend <- initializeBackend params' cloudService Just vm <- findNamedVM backend virtualMachine + + -- The same binary can behave as the client or the server, + -- depending on the command line arguments case cmd of "server" -> spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) "client" -> callOnVM backend vm port $ From a828abbf6a1b7c79c0eccf8cf1ce57376cfbb993 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 22 Aug 2012 10:15:38 +0100 Subject: [PATCH 0191/2357] Have spawnOnVM return a PID --- demos/Ping.hs | 11 ++++-- .../Distributed/Process/Backend/Azure.hs | 37 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/demos/Ping.hs b/demos/Ping.hs index 0db4ebf2..798c7e55 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -76,7 +76,10 @@ main = do -- The same binary can behave as the client or the server, -- depending on the command line arguments case cmd of - "server" -> spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) - "client" -> callOnVM backend vm port $ - ProcessPair ($(mkClosure 'pingClientRemote) ()) - pingClientLocal + "server" -> do + pid <- spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) + putStrLn $ "Ping server started at " ++ show pid + "client" -> + callOnVM backend vm port $ + ProcessPair ($(mkClosure 'pingClientRemote) ()) + pingClientLocal diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 4d640aba..0f97419a 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -455,7 +455,7 @@ import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) import Data.Foldable (forM_) import Control.Applicative ((<$>), (<*>)) -import Control.Monad (void, unless) +import Control.Monad (void) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) import Control.Exception ( Exception @@ -510,6 +510,8 @@ import Control.Distributed.Process , RemoteTable , catch , unClosure + , ProcessId + , getSelfPid ) import Control.Distributed.Process.Serializable (Serializable) import qualified Control.Distributed.Process.Internal.Types as CH @@ -546,7 +548,7 @@ data Backend = Backend { -- | Create a new CH node and run the specified process. -- The CH node will shut down when the /remote/ process exists. @spawnOnVM@ -- returns as soon as the process has been spawned. - , spawnOnVM :: VirtualMachine -> String -> RemoteProcess () -> IO () + , spawnOnVM :: VirtualMachine -> String -> RemoteProcess () -> IO ProcessId } deriving (Typeable) -- | Azure connection parameters @@ -658,7 +660,7 @@ apiSpawnOnVM :: AzureParameters -> VirtualMachine -> String -> Closure (Backend -> Process ()) - -> IO () + -> IO ProcessId apiSpawnOnVM params cloudService vm port proc = withSSH2 params vm $ \s -> do -- TODO: reduce duplication with apiCallOnVM @@ -678,7 +680,9 @@ apiSpawnOnVM params cloudService vm port proc = SSH.writeAllChannel ch paramsEnc SSH.channelSendEOF ch SSH.readAllChannel ch - unless (status == 0) $ error (BSLC.unpack r) + if status == 0 + then return (decode r) + else error (BSLC.unpack r) where procEnc :: BSL.ByteString procEnc = encode proc @@ -822,15 +826,27 @@ onVmMain :: (RemoteTable -> RemoteTable) -> [String] -> IO () onVmMain rtable [host, port, cloudService, bg] = do hSetBinaryMode stdin True hSetBinaryMode stdout True - Just procEnc <- getWithLength stdin + Just rprocEnc <- getWithLength stdin Just paramsEnc <- getWithLength stdin backend <- initializeBackend (decode paramsEnc) cloudService - let proc = decode procEnc + let rproc = decode rprocEnc lprocMVar <- newEmptyMVar :: IO (MVar CH.LocalProcess) if read bg - then detach $ startCH proc lprocMVar backend runProcess (\_ -> return ()) + then + void . Posix.forkProcess $ do + -- We inherit the file descriptors from the parent, so the SSH + -- session will not be terminated until we close them + void Posix.createSession + startCH rproc lprocMVar backend + (\node proc -> runProcess node $ do + us <- getSelfPid + liftIO $ do + BSL.hPut stdout (encode us) + mapM_ hClose [stdin, stdout, stderr] + proc) + (\_ -> return ()) else do - startCH proc lprocMVar backend forkProcess (liftIO . remoteThrow) + startCH rproc lprocMVar backend forkProcess (liftIO . remoteThrow) lproc <- readMVar lprocMVar queueFromHandle stdin (CH.processQueue lproc) where @@ -875,11 +891,6 @@ queueFromHandle h q = do enqueue q $ CH.payloadToMessage (BSL.toChunks payload) queueFromHandle h q -detach :: IO () -> IO () -detach io = do - mapM_ hClose [stdin, stdout, stderr] - void . Posix.forkProcess $ void Posix.createSession >> io - -------------------------------------------------------------------------------- -- SSH utilities -- -------------------------------------------------------------------------------- From b89d682ba5c81ebe63233bfdc4108bab79060fd4 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 22 Aug 2012 11:21:14 +0100 Subject: [PATCH 0192/2357] Better error reporting --- .../Distributed/Process/Backend/Azure.hs | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 0f97419a..2589065d 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -413,7 +413,6 @@ module Control.Distributed.Process.Backend.Azure , localSend , localExpect , remoteSend - , remoteThrow ) where import Prelude hiding (catch) @@ -451,7 +450,6 @@ import qualified Data.ByteString.Lazy as BSL , hPut , hGet ) -import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack) import Data.Typeable (Typeable) import Data.Foldable (forM_) import Control.Applicative ((<$>), (<*>)) @@ -678,11 +676,17 @@ apiSpawnOnVM params cloudService vm port proc = SSH.writeAllChannel ch procEnc SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) SSH.writeAllChannel ch paramsEnc - SSH.channelSendEOF ch - SSH.readAllChannel ch + localExpect' ch + if status == 0 + then return r + else error "spawnOnVM: Non-zero exit status" + {- + --SSH.channelSendEOF ch + --SSH.readAllChannel ch if status == 0 then return (decode r) else error (BSLC.unpack r) + -} where procEnc :: BSL.ByteString procEnc = encode proc @@ -780,30 +784,34 @@ localSend x = LocalProcess $ do -- Note that unlike for the standard Cloud Haskell 'expect' it will result in a -- runtime error if the remote process sends a message of type other than @a@. localExpect :: Serializable a => LocalProcess a -localExpect = LocalProcess $ do - ch <- ask - liftIO $ do - isE <- readIntChannel ch - len <- readIntChannel ch - msg <- readSizeChannel ch len - if isE /= 0 - then error (decode msg) - else return (decode msg) +localExpect = LocalProcess $ ask >>= liftIO . localExpect' + +localExpect' :: Serializable a => SSH.Channel -> IO a +localExpect' ch = do + isE <- readIntChannel ch + len <- readIntChannel ch + msg <- readSizeChannel ch len + if isE /= 0 + then error (decode msg) + else return (decode msg) -- | Send a message from the remote process to the local process (see -- 'ProcessPair'). Note that the remote process can use the standard Cloud -- Haskell primitives to /receive/ messages from the local process. remoteSend :: Serializable a => a -> Process () -remoteSend = liftIO . remoteSend' 0 +remoteSend = liftIO . remoteSend' + +remoteSend' :: Serializable a => a -> IO () +remoteSend' = remoteSendFlagged 0 -- | If the remote process encounters an error it can use 'remoteThrow'. This -- will cause the exception to be raised (as a user-exception, not as the -- original type) in the local process (as well as in the remote process). remoteThrow :: Exception e => e -> IO () -remoteThrow e = remoteSend' 1 (show e) >> throwIO e +remoteThrow e = remoteSendFlagged 1 (show e) >> throwIO e -remoteSend' :: Serializable a => Int -> a -> IO () -remoteSend' flags x = do +remoteSendFlagged :: Serializable a => Int -> a -> IO () +remoteSendFlagged flags x = do let enc = encode x BSS.hPut stdout (encodeInt32 flags) BSS.hPut stdout (encodeInt32 (BSL.length enc)) @@ -841,12 +849,11 @@ onVmMain rtable [host, port, cloudService, bg] = do (\node proc -> runProcess node $ do us <- getSelfPid liftIO $ do - BSL.hPut stdout (encode us) + remoteSend' us mapM_ hClose [stdin, stdout, stderr] proc) - (\_ -> return ()) else do - startCH rproc lprocMVar backend forkProcess (liftIO . remoteThrow) + startCH rproc lprocMVar backend forkProcess lproc <- readMVar lprocMVar queueFromHandle stdin (CH.processQueue lproc) where @@ -854,9 +861,8 @@ onVmMain rtable [host, port, cloudService, bg] = do -> MVar CH.LocalProcess -> Backend -> (CH.LocalNode -> Process () -> IO a) - -> (SomeException -> Process ()) -> IO () - startCH rproc lprocMVar backend go exceptionHandler = do + startCH rproc lprocMVar backend go = do mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> remoteThrow err @@ -865,7 +871,8 @@ onVmMain rtable [host, port, cloudService, bg] = do void . go node $ do ask >>= liftIO . putMVar lprocMVar proc <- unClosure rproc :: Process (Backend -> Process ()) - catch (proc backend) exceptionHandler + catch (proc backend) + (liftIO . (remoteThrow :: SomeException -> IO ())) onVmMain _ _ = error "Invalid arguments passed on onVmMain" From d1b2b39e16b7b83b2019560e07c188a07f423a95 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 22 Aug 2012 12:00:13 +0100 Subject: [PATCH 0193/2357] Cleanup --- .../Distributed/Process/Backend/Azure.hs | 108 +++++++----------- 1 file changed, 44 insertions(+), 64 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 2589065d..9c3e9467 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -629,29 +629,7 @@ apiCallOnVM :: AzureParameters -> String -> ProcessPair a -> IO a -apiCallOnVM params cloudService vm port ppair = - withSSH2 params vm $ \s -> do - let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm" - ++ " " ++ vmIpAddress vm - ++ " " ++ port - ++ " " ++ cloudService - ++ " False" - ++ " 2>&1" - let paramsEnc = encode params - (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do - SSH.channelExecute ch exe - SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc)) - SSH.writeAllChannel ch rprocEnc - SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) - SSH.writeAllChannel ch paramsEnc - runLocalProcess (ppairLocal ppair) ch - if status == 0 - then return r - else error "callOnVM: Non-zero exit status" - where - rprocEnc :: BSL.ByteString - rprocEnc = encode (ppairRemote ppair) +apiCallOnVM = runOnVM False apiSpawnOnVM :: AzureParameters -> String @@ -659,37 +637,40 @@ apiSpawnOnVM :: AzureParameters -> String -> Closure (Backend -> Process ()) -> IO ProcessId -apiSpawnOnVM params cloudService vm port proc = - withSSH2 params vm $ \s -> do - -- TODO: reduce duplication with apiCallOnVM - let exe = "PATH=. " ++ azureSshRemotePath params - ++ " onvm" - ++ " " ++ vmIpAddress vm - ++ " " ++ port - ++ " " ++ cloudService - ++ " True" - ++ " 2>&1" - let paramsEnc = encode params - (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do - SSH.channelExecute ch exe - SSH.writeChannel ch (encodeInt32 (BSL.length procEnc)) - SSH.writeAllChannel ch procEnc - SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) - SSH.writeAllChannel ch paramsEnc - localExpect' ch - if status == 0 - then return r - else error "spawnOnVM: Non-zero exit status" - {- - --SSH.channelSendEOF ch - --SSH.readAllChannel ch - if status == 0 - then return (decode r) - else error (BSLC.unpack r) - -} - where - procEnc :: BSL.ByteString - procEnc = encode proc +apiSpawnOnVM params cloudService vm port rproc = + runOnVM True params cloudService vm port $ + ProcessPair rproc localExpect + +-- | Internal generalization of 'spawnOnVM' and 'callOnVM' +runOnVM :: Bool + -> AzureParameters + -> String + -> VirtualMachine + -> String + -> ProcessPair a + -> IO a +runOnVM bg params cloudService vm port ppair = + withSSH2 params vm $ \s -> do + -- TODO: reduce duplication with apiCallOnVM + let exe = "PATH=. " ++ azureSshRemotePath params + ++ " onvm" + ++ " " ++ vmIpAddress vm + ++ " " ++ port + ++ " " ++ cloudService + ++ " " ++ show bg + ++ " 2>&1" + let paramsEnc = encode params + let rprocEnc = encode (ppairRemote ppair) + (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do + SSH.channelExecute ch exe + SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc)) + SSH.writeAllChannel ch rprocEnc + SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) + SSH.writeAllChannel ch paramsEnc + runLocalProcess (ppairLocal ppair) ch + if status == 0 + then return r + else error "runOnVM: Non-zero exit status" -- This would a bug -- | Check the MD5 hash of the executable on the remote machine apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool @@ -784,16 +765,15 @@ localSend x = LocalProcess $ do -- Note that unlike for the standard Cloud Haskell 'expect' it will result in a -- runtime error if the remote process sends a message of type other than @a@. localExpect :: Serializable a => LocalProcess a -localExpect = LocalProcess $ ask >>= liftIO . localExpect' - -localExpect' :: Serializable a => SSH.Channel -> IO a -localExpect' ch = do - isE <- readIntChannel ch - len <- readIntChannel ch - msg <- readSizeChannel ch len - if isE /= 0 - then error (decode msg) - else return (decode msg) +localExpect = LocalProcess $ do + ch <- ask + liftIO $ do + isE <- readIntChannel ch + len <- readIntChannel ch + msg <- readSizeChannel ch len + if isE /= 0 + then error (decode msg) + else return (decode msg) -- | Send a message from the remote process to the local process (see -- 'ProcessPair'). Note that the remote process can use the standard Cloud From fc798ef92a250c2e65ec8c1921d46866fe652583 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 22 Aug 2012 13:16:42 +0100 Subject: [PATCH 0194/2357] Don't assume nodes are still alive in findSlaves --- .../Process/Backend/SimpleLocalnet.hs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index 1cd23555..4ba0f181 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -97,7 +97,7 @@ import Data.Foldable (forM_) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Exception (throw) -import Control.Monad (forever, forM) +import Control.Monad (forever, forM, replicateM) import Control.Monad.IO.Class (liftIO) import Control.Concurrent (forkIO, threadDelay, ThreadId) import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_) @@ -114,8 +114,12 @@ import Control.Distributed.Process , expect , nsendRemote , receiveWait + , match , matchIf , processNodeId + , monitorNode + , unmonitor + , NodeMonitorNotification(..) ) import qualified Control.Distributed.Process.Node as Node ( LocalNode @@ -286,12 +290,24 @@ findSlaves :: Backend -> Process [NodeId] findSlaves backend = do nodes <- liftIO $ findPeers backend 1000000 -- Fire of asynchronous requests for the slave controller - forM_ nodes $ \nid -> whereisRemoteAsync nid "slaveController" + refs <- forM nodes $ \nid -> do + whereisRemoteAsync nid "slaveController" + ref <- monitorNode nid + return (nid, ref) -- Wait for the replies - catMaybes <$> forM nodes (\_ -> + catMaybes <$> replicateM (length nodes) ( receiveWait [ matchIf (\(WhereIsReply label _) -> label == "slaveController") - (\(WhereIsReply _ mPid) -> return (processNodeId <$> mPid)) + (\(WhereIsReply _ mPid) -> + case mPid of + Nothing -> + return Nothing + Just pid -> do + let nid = processNodeId pid + Just ref = lookup nid refs + unmonitor ref + return (Just nid)) + , match (\(NodeMonitorNotification {}) -> return Nothing) ]) -- | Terminate all slaves From 7a7336ffd53e7cf89e36a6601d88dc1a7560ebc6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 22 Aug 2012 13:17:49 +0100 Subject: [PATCH 0195/2357] Bumb version to 0.2.0.5. --- ChangeLog | 4 ++++ distributed-process-simplelocalnet.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index bdec8533..546b8763 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-08-22 Edsko de Vries 0.2.0.5 + +* Don't assume slaves are still alive in findSlaves + 2012-08-09 Edsko de Vries 0.2.0.4 * Relax version bounds for distributed-process diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 848e7645..beb0d0c3 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.4 +Version: 0.2.0.5 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 From e43c2c3fbe62f96a482fd04e906d33f80dd5da66 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 24 Aug 2012 15:48:39 +0100 Subject: [PATCH 0196/2357] Use a BS.copy in Binary instance for addresses to avoid addresses hanging on to much larger chunks of bytestrings returned by the specific network implementation --- src/Network/Transport.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 1922dee4..bc987e29 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -23,10 +23,12 @@ module Network.Transport ( -- * Types ) where import Data.ByteString (ByteString) +import qualified Data.ByteString as BS (copy) import qualified Data.ByteString.Char8 as BSC (unpack) import Control.Exception (Exception) +import Control.Applicative ((<$>)) import Data.Typeable (Typeable) -import Data.Binary (Binary) +import Data.Binary (Binary(get, put)) -------------------------------------------------------------------------------- -- Main API -- @@ -116,7 +118,11 @@ data MulticastGroup = MulticastGroup { -- | EndPointAddress of an endpoint. newtype EndPointAddress = EndPointAddress { endPointAddressToByteString :: ByteString } - deriving (Eq, Ord, Typeable, Binary) + deriving (Eq, Ord, Typeable) + +instance Binary EndPointAddress where + put = put . endPointAddressToByteString + get = EndPointAddress . BS.copy <$> get instance Show EndPointAddress where show = BSC.unpack . endPointAddressToByteString From 48d1b4fd7923b83a8abd13b7b3179403f4ae258c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 24 Aug 2012 17:22:57 +0100 Subject: [PATCH 0197/2357] Start work on the updated N.T failure semantics --- ChangeLog | 4 ++++ network-transport.cabal | 2 +- src/Network/Transport.hs | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0fccd757..e050fd49 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +< Edsko de Vries 0.3.0 + +* Clarify disconnection + 2012-07-16 Edsko de Vries 0.2.0.2 * Base 4.6 compatible test suites diff --git a/network-transport.cabal b/network-transport.cabal index d1fa9edc..c728ea84 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,5 +1,5 @@ Name: network-transport -Version: 0.2.0.2 +Version: 0.3.0 Cabal-Version: >=1.6 Build-Type: Simple License: BSD3 diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index bc987e29..5c150167 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -230,8 +230,35 @@ data EventErrorCode = EventEndPointFailed -- | Transport-wide fatal error | EventTransportFailed - -- | Some incoming connections were closed abruptly. - -- If an endpoint address is specified, then all connections to and - -- from that endpoint are now lost - | EventConnectionLost (Maybe EndPointAddress) [ConnectionId] + -- | We lost connection to another endpoint + -- + -- Although "Network.Transport" provides multiple independent lightweight + -- connections between endpoints, those connections cannot /fail/ + -- independently: once one connection has failed, /all/ connections, in + -- both directions, must now be considered to have failed; they fail as a + -- "bundle" of connections, with only a single "bundle" of connections per + -- endpoint at any point in time. + -- + -- That is, suppose there are multiple connections in either direction + -- between A and B, and A receives a notification that it has lost + -- connection to B. Then A should not be able to send any further messages + -- to B on existing connections. + -- + -- Although B may not realize /immediately/ that its connection to A has + -- been broken, messages sent by B on existing connections should not be + -- delivered, and B must eventually get an EventConnectionLost message, + -- too. + -- + -- Moreover, this event must be posted before A has successfully + -- reconnected (in other words, if B notices a reconnection attempt from A, + -- it must post the EventConnectionLost before acknowledging the connection + -- from A) so that B will not receive events about new connections or + -- incoming messages from A without realizing that it got disconnected. + -- + -- If B attempts to establish another connection to A before it realized + -- that it got disconnected from A then it's okay for this connection + -- attempt to fail, and the EventConnectionLost to be posted at that point, + -- or for the EventConnectionLost to be posted and for the new connection + -- to be considered the first connection of the "new bundle". + | EventConnectionLost EndPointAddress deriving (Show, Typeable, Eq) From daa9b08a7223b2cf6fee084d05e37227a35e8353 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 24 Aug 2012 17:22:57 +0100 Subject: [PATCH 0198/2357] Start work on the updated N.T failure semantics --- ChangeLog | 4 ++++ network-transport-tcp.cabal | 6 +++--- src/Network/Transport/TCP.hs | 7 ++----- tests/TestTCP.hs | 22 +++++++++++----------- tests/TestTransport.hs | 8 ++++---- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/ChangeLog b/ChangeLog index ed027400..0e8f159b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +<> Edsko de Vries 0.3.0 + +* Implement new disconnection semantics + 2012-08-20 Edsko de Vries 0.2.0.3 * Allow for colons in hostnames (for IPv6) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 5c76b998..dd34c4aa 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -1,5 +1,5 @@ Name: network-transport-tcp -Version: 0.2.0.3 +Version: 0.3.0 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -22,7 +22,7 @@ Source-Repository head Library Build-Depends: base >= 4.3 && < 5, - network-transport >= 0.2 && < 0.3, + network-transport >= 0.3 && < 0.4, data-accessor >= 0.2 && < 0.3, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, @@ -37,7 +37,7 @@ Test-Suite TestTCP Type: exitcode-stdio-1.0 Main-Is: TestTCP.hs Build-Depends: base >= 4.3 && < 5, - network-transport >= 0.2 && < 0.3, + network-transport >= 0.3 && < 0.4, data-accessor >= 0.2 && < 0.3, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index f01f0bf7..dabb3f09 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -745,8 +745,7 @@ modifyRemoteState (ourEndPoint, theirEndPoint) match = handleIOException ex vst = do tryCloseSocket (remoteSocket vst) putMVar theirState (RemoteEndPointFailed ex) - let incoming = IntSet.elems $ vst ^. remoteIncoming - code = EventConnectionLost (Just $ remoteAddress theirEndPoint) incoming + let code = EventConnectionLost (remoteAddress theirEndPoint) err = TransportError code (show ex) writeChan (localChannel ourEndPoint) $ ErrorEvent err @@ -1085,9 +1084,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do - let code = EventConnectionLost - (Just $ remoteAddress theirEndPoint) - (IntSet.elems $ vst ^. remoteIncoming) + let code = EventConnectionLost (remoteAddress theirEndPoint) writeChan ourChannel . ErrorEvent $ TransportError code (show err) forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) return (RemoteEndPointFailed err) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 83aaa89e..57981cf5 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -98,11 +98,11 @@ testEarlyDisconnect nextPort = do -- TEST 1: they connect to us, then drop the connection do - ConnectionOpened cid _ addr <- receive endpoint + ConnectionOpened _ _ addr <- receive endpoint True <- return $ addr == theirAddr - ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid']) _) <- receive endpoint - True <- return $ addr' == theirAddr && cid' == cid + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint + True <- return $ addr' == theirAddr return () @@ -124,8 +124,8 @@ testEarlyDisconnect nextPort = do Received cid' ["pong"] <- receive endpoint True <- return $ cid == cid' - ErrorEvent (TransportError (EventConnectionLost (Just addr') [cid'']) _) <- receive endpoint - True <- return $ addr' == theirAddr && cid'' == cid + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint + True <- return $ addr' == theirAddr return () @@ -235,7 +235,7 @@ testEarlyCloseSocket nextPort = do ConnectionClosed cid'' <- receive endpoint True <- return $ cid'' == cid - ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint True <- return $ addr' == theirAddr return () @@ -634,7 +634,7 @@ testReconnect nextPort = do -- But a send will fail because the server has closed the connection again Left (TransportError SendFailed _) <- send conn1 ["ping"] - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint -- But a subsequent call to connect should reestablish the connection Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -695,7 +695,7 @@ testUnidirectionalError nextPort = do -- But when we send we find the error Left (TransportError SendFailed _) <- send conn1 ["ping"] - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint -- A call to connect should now re-establish the connection Right conn2 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -709,7 +709,7 @@ testUnidirectionalError nextPort = do -- We now find the error when we attempt to close the connection Nothing <- timeout 500000 $ receive endpoint close conn2 - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint Right conn3 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints send conn3 ["ping"] takeMVar serverGotPing @@ -721,7 +721,7 @@ testUnidirectionalError nextPort = do -- Now we notice the problem when we try to connect Nothing <- timeout 500000 $ receive endpoint Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint Right conn4 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints send conn4 ["ping"] takeMVar serverGotPing @@ -746,7 +746,7 @@ testInvalidCloseConnection nextPort = do -- At this point the client sends an invalid request, so we terminate the -- connection - ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint putMVar serverDone () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs index e06ad7ff..f1381bbf 100644 --- a/tests/TestTransport.hs +++ b/tests/TestTransport.hs @@ -606,7 +606,7 @@ testCloseEndPoint transport _ = do send conn ["pong"] ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' - ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint ; True <- return $ addr' == theirAddr + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint ; True <- return $ addr' == theirAddr Left (TransportError SendFailed _) <- send conn ["pong2"] @@ -694,7 +694,7 @@ testCloseTransport newTransport = do evs <- replicateM 3 $ receive endpoint let expected = [ ConnectionClosed cid1 , ConnectionClosed cid2 - , ErrorEvent (TransportError (EventConnectionLost (Just theirAddr2) []) "") + , ErrorEvent (TransportError (EventConnectionLost theirAddr2) "") ] True <- return $ any (== expected) (permutations evs) @@ -813,8 +813,8 @@ testSendException newTransport = do -- This will have been as a failure to send by endpoint1, which will -- therefore have closed the socket. In turn this will have caused endpoint2 -- to report that the connection was lost - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint1 - ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint1 + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint2 -- A new connection will re-establish the connection Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints From b7d5f5cb46437df2351559a7d556c66eb3ed5274 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 24 Aug 2012 17:22:57 +0100 Subject: [PATCH 0199/2357] Start work on the updated N.T failure semantics --- ChangeLog | 4 ++++ distributed-process-simplelocalnet.cabal | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 546b8763..0197da7d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +<> Edsko de Vries 0.2.0.6 + +* Use new version of network-transport + 2012-08-22 Edsko de Vries 0.2.0.5 * Don't assume slaves are still alive in findSlaves diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index beb0d0c3..879957f9 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.5 +Version: 0.2.0.6 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -32,8 +32,8 @@ Library binary >= 0.5 && < 0.6, containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, - network-transport >= 0.2 && < 0.3, - network-transport-tcp >= 0.2 && < 0.3, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, distributed-process >= 0.2 && < 0.4 Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast @@ -54,8 +54,8 @@ Test-Suite TestSimpleLocalnet binary >= 0.5 && < 0.6, containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, - network-transport >= 0.2 && < 0.3, - network-transport-tcp >= 0.2 && < 0.3, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, distributed-process >= 0.2 && < 0.4 Extensions: RankNTypes, DeriveDataTypeable From 8475049d74ff864629156b2ec56ceb39081db5e6 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 27 Aug 2012 16:39:05 +0100 Subject: [PATCH 0200/2357] Clarify docs --- src/Network/Transport.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 5c150167..04d427db 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -240,9 +240,9 @@ data EventErrorCode = -- endpoint at any point in time. -- -- That is, suppose there are multiple connections in either direction - -- between A and B, and A receives a notification that it has lost - -- connection to B. Then A should not be able to send any further messages - -- to B on existing connections. + -- between endpoints A and B, and A receives a notification that it has + -- lost contact with B. Then A must not be able to send any further + -- messages to B on existing connections. -- -- Although B may not realize /immediately/ that its connection to A has -- been broken, messages sent by B on existing connections should not be From a6114e538702e0c89f90b7e4b0a83b5c8857d8f2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Sep 2012 11:10:56 +0100 Subject: [PATCH 0201/2357] Add protocol check to localExpect --- src/Control/Distributed/Process/Backend/Azure.hs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 9c3e9467..5aa9f53d 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -453,7 +453,7 @@ import qualified Data.ByteString.Lazy as BSL import Data.Typeable (Typeable) import Data.Foldable (forM_) import Control.Applicative ((<$>), (<*>)) -import Control.Monad (void) +import Control.Monad (void, when) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) import Control.Exception ( Exception @@ -764,12 +764,18 @@ localSend x = LocalProcess $ do -- | Wait for a message from the remote process (see 'ProcessPair'). -- Note that unlike for the standard Cloud Haskell 'expect' it will result in a -- runtime error if the remote process sends a message of type other than @a@. +-- +-- Since it is relatively easy for the remote process to mess up the +-- communication protocol (for instance, by doing a putStr) we ask for the +-- length twice, as some sort of sanity check. localExpect :: Serializable a => LocalProcess a localExpect = LocalProcess $ do ch <- ask liftIO $ do isE <- readIntChannel ch len <- readIntChannel ch + lenAgain <- readIntChannel ch + when (len /= lenAgain) $ throwIO (userError "Protocol violation") msg <- readSizeChannel ch len if isE /= 0 then error (decode msg) @@ -794,6 +800,8 @@ remoteSendFlagged :: Serializable a => Int -> a -> IO () remoteSendFlagged flags x = do let enc = encode x BSS.hPut stdout (encodeInt32 flags) + -- See 'localExpect' for why we send the length twice + BSS.hPut stdout (encodeInt32 (BSL.length enc)) BSS.hPut stdout (encodeInt32 (BSL.length enc)) BSL.hPut stdout enc hFlush stdout From a4c2728a12d43eb6c6c5b54949d1ff7e19db7646 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Sep 2012 18:17:30 +0100 Subject: [PATCH 0202/2357] Move the transport tests to a separate package This paves the way for reusing these tests in other transports --- LICENSE | 30 + Setup.hs | 2 + network-transport-tests.cabal | 35 + src/Network/Transport/Tests.hs | 964 +++++++++++++++++++++++ src/Network/Transport/Tests/Auxiliary.hs | 112 +++ src/Network/Transport/Tests/Traced.hs | 200 +++++ 6 files changed, 1343 insertions(+) create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 network-transport-tests.cabal create mode 100644 src/Network/Transport/Tests.hs create mode 100644 src/Network/Transport/Tests/Auxiliary.hs create mode 100644 src/Network/Transport/Tests/Traced.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a956d0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Edsko de Vries + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Edsko de Vries nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/network-transport-tests.cabal b/network-transport-tests.cabal new file mode 100644 index 00000000..0f3d2053 --- /dev/null +++ b/network-transport-tests.cabal @@ -0,0 +1,35 @@ +name: network-transport-tests +version: 0.1.0.0 +synopsis: Unit tests for Network.Transport implementations +-- description: +homepage: http://github.com/haskell-distributed/distributed-process +license: BSD3 +license-file: LICENSE +author: Edsko de Vries +maintainer: edsko@well-typed.com +copyright: Well-Typed LLP +category: Network +build-type: Simple +cabal-version: >=1.8 + +library + exposed-modules: Network.Transport.Tests, + Network.Transport.Tests.Auxiliary, + Network.Transport.Tests.Traced + -- other-modules: + build-depends: base ==4.5.*, + network-transport >= 0.3 && < 0.4, + containers >= 0.4 && < 0.6, + bytestring >= 0.9 && < 0.11, + random >= 1.0 && < 1.1, + mtl >= 2.1 && < 2.2, + ansi-terminal >= 0.5 && < 0.6 + hs-source-dirs: src + ghc-options: -Wall -fno-warn-unused-do-bind + extensions: CPP, + ExistentialQuantification, + FlexibleInstances, + DeriveDataTypeable, + RankNTypes, + OverloadedStrings, + OverlappingInstances diff --git a/src/Network/Transport/Tests.hs b/src/Network/Transport/Tests.hs new file mode 100644 index 00000000..45773c42 --- /dev/null +++ b/src/Network/Transport/Tests.hs @@ -0,0 +1,964 @@ +{-# LANGUAGE RebindableSyntax #-} +module Network.Transport.Tests where + +import Prelude hiding + ( (>>=) + , return + , fail + , (>>) +#if ! MIN_VERSION_base(4,6,0) + , catch +#endif + ) +import Control.Concurrent (forkIO, killThread, yield) +import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) +import Control.Exception (evaluate, throw, throwIO, bracket) +import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) +import Control.Monad.Error () +import Control.Applicative ((<$>)) +import Network.Transport +import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) +import Network.Transport.Util (spawn) +import System.Random (randomIO) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack) +import Data.Map (Map) +import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) +import Data.String (fromString) +import Data.List (permutations) +import Network.Transport.Tests.Auxiliary (forkTry, runTests, trySome, randomThreadDelay) +import Network.Transport.Tests.Traced + +-- | Server that echoes messages straight back to the origin endpoint. +echoServer :: EndPoint -> IO () +echoServer endpoint = do + go Map.empty + where + go :: Map ConnectionId Connection -> IO () + go cs = do + event <- receive endpoint + case event of + ConnectionOpened cid rel addr -> do + tlog $ "Opened new connection " ++ show cid + Right conn <- connect endpoint addr rel defaultConnectHints + go (Map.insert cid conn cs) + Received cid payload -> do + send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload + go cs + ConnectionClosed cid -> do + tlog $ "Close connection " ++ show cid + close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) + go (Map.delete cid cs) + ReceivedMulticast _ _ -> + -- Ignore + go cs + ErrorEvent _ -> + putStrLn $ "Echo server received error event: " ++ show event + EndPointClosed -> + return () + +-- | Ping client used in a few tests +ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () +ping endpoint server numPings msg = do + -- Open connection to the server + tlog "Connect to echo server" + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Wait for the server to open reply connection + tlog "Wait for ConnectionOpened message" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings and wait for reply + tlog "Send ping and wait for reply" + replicateM_ numPings $ do + send conn [msg] + Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg + return () + + -- Close the connection + tlog "Close the connection" + close conn + + -- Wait for the server to close its connection to us + tlog "Wait for ConnectionClosed message" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + -- Done + tlog "Ping client done" + +-- | Basic ping test +testPingPong :: Transport -> Int -> IO () +testPingPong transport numPings = do + tlog "Starting ping pong test" + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + tlog "Ping client" + Right endpoint <- newEndPoint transport + ping endpoint server numPings "ping" + putMVar result () + + takeMVar result + +-- | Test that endpoints don't get confused +testEndPoints :: Transport -> Int -> IO () +testEndPoints transport numPings = do + server <- spawn transport echoServer + dones <- replicateM 2 newEmptyMVar + + forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do + let name' :: ByteString + name' = pack [name] + Right endpoint <- newEndPoint transport + tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) + ping endpoint server numPings name' + putMVar done () + + forM_ dones takeMVar + +-- Test that connections don't get confused +testConnections :: Transport -> Int -> IO () +testConnections transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ replicateM_ numPings $ send conn1 ["pingA"] + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ numPings $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (2 * numPings) + + takeMVar result + +-- | Test that closing one connection does not close the other +testCloseOneConnection :: Transport -> Int -> IO () +testCloseOneConnection transport numPings = do + server <- spawn transport echoServer + result <- newEmptyMVar + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + + -- Open two connections to the server + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv1 _ _ <- receive endpoint + + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + ConnectionOpened serv2 _ _ <- receive endpoint + + -- One thread to send "pingA" on the first connection + forkTry $ do + replicateM_ numPings $ send conn1 ["pingA"] + close conn1 + + -- One thread to send "pingB" on the second connection + forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] + + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do + event <- receive endpoint + case event of + Received cid [payload] -> do + when (cid == serv1 && payload /= "pingA") $ error "Wrong message" + when (cid == serv2 && payload /= "pingB") $ error "Wrong message" + verifyResponse (n - 1) + _ -> + verifyResponse n + verifyResponse (3 * numPings) + + takeMVar result + +-- | Test that if A connects to B and B connects to A, B can still send to A after +-- A closes its connection to B (for instance, in the TCP transport, the socket pair +-- connecting A and B should not yet be closed). +testCloseOneDirection :: Transport -> Int -> IO () +testCloseOneDirection transport numPings = do + addrA <- newEmptyMVar + addrB <- newEmptyMVar + doneA <- newEmptyMVar + doneB <- newEmptyMVar + + -- A + forkTry $ do + tlog "A" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrA (address endpoint) + + -- Connect to B + tlog "Connect to B" + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + -- Wait for B to connect to us + tlog "Wait for B" + ConnectionOpened cid _ _ <- receive endpoint + + -- Send pings to B + tlog "Send pings to B" + replicateM_ numPings $ send conn ["ping"] + + -- Close our connection to B + tlog "Close connection" + close conn + + -- Wait for B's pongs + tlog "Wait for pongs from B" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for B to close it's connection to us + tlog "Wait for B to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Done + tlog "Done" + putMVar doneA () + + -- B + forkTry $ do + tlog "B" + Right endpoint <- newEndPoint transport + tlog (show (address endpoint)) + putMVar addrB (address endpoint) + + -- Wait for A to connect + tlog "Wait for A to connect" + ConnectionOpened cid _ _ <- receive endpoint + + -- Connect to A + tlog "Connect to A" + Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + -- Wait for A's pings + tlog "Wait for pings from A" + replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () + + -- Wait for A to close it's connection to us + tlog "Wait for A to close connection" + ConnectionClosed cid' <- receive endpoint + guard (cid == cid') + + -- Send pongs to A + tlog "Send pongs to A" + replicateM_ numPings $ send conn ["pong"] + + -- Close our connection to A + tlog "Close connection to A" + close conn + + -- Done + tlog "Done" + putMVar doneB () + + mapM_ takeMVar [doneA, doneB] + +-- | Collect events and order them by connection ID +collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] +collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty + where + -- TODO: for more serious use of this function we'd need to make these arguments strict + go (Just 0) open closed = finish open closed + go n open closed = do + mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint + case mEvent of + Left _ -> finish open closed + Right event -> do + let n' = (\x -> x - 1) <$> n + case event of + ConnectionOpened cid _ _ -> + go n' (Map.insert cid [] open) closed + ConnectionClosed cid -> + let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in + go n' (Map.delete cid open) (Map.insert cid list closed) + Received cid msg -> + go n' (Map.adjust (msg :) cid open) closed + ReceivedMulticast _ _ -> + fail "Unexpected multicast" + ErrorEvent _ -> + fail "Unexpected error" + EndPointClosed -> + fail "Unexpected endpoint closure" + + finish open closed = + if Map.null open + then return . Map.toList . Map.map reverse $ closed + else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) + +-- | Open connection, close it, then reopen it +-- (In the TCP transport this means the socket will be closed, then reopened) +-- +-- Note that B cannot expect to receive all of A's messages on the first connection +-- before receiving the messages on the second connection. What might (and sometimes +-- does) happen is that finishes sending all of its messages on the first connection +-- (in the TCP transport, the first socket pair) while B is behind on reading _from_ +-- this connection (socket pair) -- the messages are "in transit" on the network +-- (these tests are done on localhost, so there are in some OS buffer). Then when +-- A opens the second connection (socket pair) B will spawn a new thread for this +-- connection, and hence might start interleaving messages from the first and second +-- connection. +-- +-- This is correct behaviour, however: the transport API guarantees reliability and +-- ordering _per connection_, but not _across_ connections. +testCloseReopen :: Transport -> Int -> IO () +testCloseReopen transport numPings = do + addrB <- newEmptyMVar + doneB <- newEmptyMVar + + let numRepeats = 2 :: Int + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + + forM_ [1 .. numRepeats] $ \i -> do + tlog "A connecting" + -- Connect to B + Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + tlog "A pinging" + -- Say hi + forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] + + tlog "A closing" + -- Disconnect again + close conn + + tlog "A finishing" + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar addrB (address endpoint) + + eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing + + forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do + forM_ (zip [1 .. numPings] events) $ \(j, event) -> do + guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) + + putMVar doneB () + + takeMVar doneB + +-- | Test lots of parallel connection attempts +testParallelConnects :: Transport -> Int -> IO () +testParallelConnects transport numPings = do + server <- spawn transport echoServer + done <- newEmptyMVar + + Right endpoint <- newEndPoint transport + + -- Spawn lots of clients + forM_ [1 .. numPings] $ \i -> forkTry $ do + Right conn <- connect endpoint server ReliableOrdered defaultConnectHints + send conn [pack $ "ping" ++ show i] + send conn [pack $ "ping" ++ show i] + close conn + + forkTry $ do + eventss <- collect endpoint (Just (numPings * 4)) Nothing + -- Check that no pings got sent to the wrong connection + forM_ eventss $ \(_, [[ping1], [ping2]]) -> + guard (ping1 == ping2) + putMVar done () + + takeMVar done + +-- | Test that sending on a closed connection gives an error +testSendAfterClose :: Transport -> Int -> IO () +testSendAfterClose transport numRepeats = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + -- We request two lightweight connections + replicateM numRepeats $ do + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Close the second, but leave the first open; then output on the second + -- connection (i.e., on a closed connection while there is still another + -- connection open) + close conn2 + Left (TransportError SendClosed _) <- send conn2 ["ping2"] + + -- Now close the first connection, and output on it (i.e., output while + -- there are no lightweight connection at all anymore) + close conn1 + Left (TransportError SendClosed _) <- send conn2 ["ping2"] + + return () + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that closing the same connection twice has no effect +testCloseTwice :: Transport -> Int -> IO () +testCloseTwice transport numRepeats = do + server <- spawn transport echoServer + clientDone <- newEmptyMVar + + forkTry $ do + Right endpoint <- newEndPoint transport + + replicateM numRepeats $ do + -- We request two lightweight connections + Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints + + -- Close the second one twice + close conn2 + close conn2 + + -- Then send a message on the first and close that twice too + send conn1 ["ping"] + close conn1 + + -- Verify expected response from the echo server + ConnectionOpened cid1 _ _ <- receive endpoint + ConnectionOpened cid2 _ _ <- receive endpoint + ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 + Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 + ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 + + return () + + putMVar clientDone () + + takeMVar clientDone + +-- | Test that we can connect an endpoint to itself +testConnectToSelf :: Transport -> Int -> IO () +testConnectToSelf transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + + tlog "Talk to myself" + + -- One thread to write to the endpoint + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn ["ping"] + + tlog $ "Closing connection" + close conn + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + tlog "Waiting for ConnectionOpened" + ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint + + tlog "Waiting for Received" + replicateM_ numPings $ do + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + return () + + tlog "Waiting for ConnectionClosed" + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we can connect an endpoint to itself multiple times +testConnectToSelfTwice :: Transport -> Int -> IO () +testConnectToSelfTwice transport numPings = do + done <- newEmptyMVar + Right endpoint <- newEndPoint transport + + tlog "Creating self-connection" + Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints + + tlog "Talk to myself" + + -- One thread to write to the endpoint using the first connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn1 ["pingA"] + + tlog $ "Closing connection" + close conn1 + + -- One thread to write to the endpoint using the second connection + forkTry $ do + tlog $ "writing" + + tlog $ "Sending ping" + replicateM_ numPings $ send conn2 ["pingB"] + + tlog $ "Closing connection" + close conn2 + + -- And one thread to read + forkTry $ do + tlog $ "reading" + + [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing + True <- return $ events1 == replicate numPings ["pingA"] + True <- return $ events2 == replicate numPings ["pingB"] + + tlog "Done" + putMVar done () + + takeMVar done + +-- | Test that we self-connections no longer work once we close our endpoint +-- or our transport +testCloseSelf :: IO (Either String Transport) -> IO () +testCloseSelf newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints + + -- Close the conneciton and try to send + close conn1 + Left (TransportError SendClosed _) <- send conn1 ["ping"] + + -- Close the first endpoint. We should not be able to use the first + -- connection anymore, or open more self connections, but the self connection + -- to the second endpoint should still be fine + closeEndPoint endpoint1 + Left (TransportError SendFailed _) <- send conn2 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints + Right () <- send conn3 ["ping"] + + -- Close the transport; now the second should no longer work + closeTransport transport + Left (TransportError SendFailed _) <- send conn3 ["ping"] + Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints + + return () + +-- | Test various aspects of 'closeEndPoint' +testCloseEndPoint :: Transport -> Int -> IO () +testCloseEndPoint transport _ = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- First test (see client) + do + theirAddr <- readMVar clientAddr1 + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + return () + + -- Second test + do + theirAddr <- readMVar clientAddr2 + + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' + + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn ["pong"] + + ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint ; True <- return $ addr' == theirAddr + + Left (TransportError SendFailed _) <- send conn ["pong2"] + + return () + + putMVar serverDone () + + -- Client + forkTry $ do + theirAddr <- readMVar serverAddr + + -- First test: close endpoint with one outgoing but no incoming connections + do + Right endpoint <- newEndPoint transport + putMVar clientAddr1 (address endpoint) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + closeEndPoint endpoint + EndPointClosed <- receive endpoint + return () + + -- Second test: close endpoint with one outgoing and one incoming connection + do + Right endpoint <- newEndPoint transport + putMVar clientAddr2 (address endpoint) + + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' + + -- Close the endpoint + closeEndPoint endpoint + EndPointClosed <- receive endpoint + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect + Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + + return () + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- Test closeTransport +-- +-- This tests many of the same things that testEndPoint does, and some more +testCloseTransport :: IO (Either String Transport) -> IO () +testCloseTransport newTransport = do + serverDone <- newEmptyMVar + clientDone <- newEmptyMVar + clientAddr1 <- newEmptyMVar + clientAddr2 <- newEmptyMVar + serverAddr <- newEmptyMVar + + -- Server + forkTry $ do + Right transport <- newTransport + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + -- Client sets up first endpoint + theirAddr1 <- readMVar clientAddr1 + ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 + + -- Client sets up second endpoint + theirAddr2 <- readMVar clientAddr2 + + ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 + Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 + + Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints + send conn ["pong"] + + -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) + evs <- replicateM 3 $ receive endpoint + let expected = [ ConnectionClosed cid1 + , ConnectionClosed cid2 + , ErrorEvent (TransportError (EventConnectionLost theirAddr2) "") + ] + True <- return $ any (== expected) (permutations evs) + + -- An attempt to send to the endpoint should now fail + Left (TransportError SendFailed _) <- send conn ["pong2"] + + putMVar serverDone () + + -- Client + forkTry $ do + Right transport <- newTransport + theirAddr <- readMVar serverAddr + + -- Set up endpoint with one outgoing but no incoming connections + Right endpoint1 <- newEndPoint transport + putMVar clientAddr1 (address endpoint1) + + -- Connect to the server, then close the endpoint without disconnecting explicitly + Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + + -- Set up an endpoint with one outgoing and out incoming connection + Right endpoint2 <- newEndPoint transport + putMVar clientAddr2 (address endpoint2) + + Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints + send conn ["ping"] + + -- Reply from the server + ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr + Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' + + -- Now shut down the entire transport + closeTransport transport + + -- Both endpoints should report that they have been closed + EndPointClosed <- receive endpoint1 + EndPointClosed <- receive endpoint2 + + -- Attempt to send should fail with connection closed + Left (TransportError SendFailed _) <- send conn ["ping2"] + + -- An attempt to close the already closed connection should just return + () <- close conn + + -- And so should an attempt to connect on either endpoint + Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints + Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints + + -- And finally, so should an attempt to create a new endpoint + Left (TransportError NewEndPointFailed _) <- newEndPoint transport + + putMVar clientDone () + + mapM_ takeMVar [serverDone, clientDone] + +-- | Remote node attempts to connect to a closed local endpoint +testConnectClosedEndPoint :: Transport -> IO () +testConnectClosedEndPoint transport = do + serverAddr <- newEmptyMVar + serverClosed <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Server + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + closeEndPoint endpoint + putMVar serverClosed () + + -- Client + forkTry $ do + Right endpoint <- newEndPoint transport + readMVar serverClosed + + Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints + + putMVar clientDone () + + takeMVar clientDone + +-- | We should receive an exception when doing a 'receive' after we have been +-- notified that an endpoint has been closed +testExceptionOnReceive :: IO (Either String Transport) -> IO () +testExceptionOnReceive newTransport = do + Right transport <- newTransport + + -- Test one: when we close an endpoint specifically + Right endpoint1 <- newEndPoint transport + closeEndPoint endpoint1 + EndPointClosed <- receive endpoint1 + Left _ <- trySome (receive endpoint1 >>= evaluate) + + -- Test two: when we close the entire transport + Right endpoint2 <- newEndPoint transport + closeTransport transport + EndPointClosed <- receive endpoint2 + Left _ <- trySome (receive endpoint2 >>= evaluate) + + return () + +-- | Test what happens when the argument to 'send' is an exceptional value +testSendException :: IO (Either String Transport) -> IO () +testSendException newTransport = do + Right transport <- newTransport + Right endpoint1 <- newEndPoint transport + Right endpoint2 <- newEndPoint transport + + -- Connect endpoint1 to endpoint2 + Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + ConnectionOpened _ _ _ <- receive endpoint2 + + -- Send an exceptional value + Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") + + -- This will have been as a failure to send by endpoint1, which will + -- therefore have closed the socket. In turn this will have caused endpoint2 + -- to report that the connection was lost + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint1 + ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint2 + + -- A new connection will re-establish the connection + Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints + send conn2 ["ping"] + close conn2 + + ConnectionOpened _ _ _ <- receive endpoint2 + Received _ ["ping"] <- receive endpoint2 + ConnectionClosed _ <- receive endpoint2 + + return () + +-- | If threads get killed while executing a 'connect', 'send', or 'close', this +-- should not affect other threads. +-- +-- The intention of this test is to see what happens when a asynchronous +-- exception happes _while executing a send_. This is exceedingly difficult to +-- guarantee, however. Hence we run a large number of tests and insert random +-- thread delays -- and even then it might not happen. Moreover, it will only +-- happen when we run on multiple cores. +testKill :: IO (Either String Transport) -> Int -> IO () +testKill newTransport numThreads = do + Right transport1 <- newTransport + Right transport2 <- newTransport + Right endpoint1 <- newEndPoint transport1 + Right endpoint2 <- newEndPoint transport2 + + threads <- replicateM numThreads . forkIO $ do + randomThreadDelay 100 + bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) + -- Note that we should not insert a randomThreadDelay into the + -- exception handler itself as this means that the exception handler + -- could be interrupted and we might not close + (\(Right conn) -> close conn) + (\(Right conn) -> do randomThreadDelay 100 + Right () <- send conn ["ping"] + randomThreadDelay 100) + + numAlive <- newMVar (0 :: Int) + + -- Kill half of those threads + forkIO . forM_ threads $ \tid -> do + shouldKill <- randomIO + if shouldKill + then randomThreadDelay 600 >> killThread tid + else modifyMVar_ numAlive (return . (+ 1)) + + -- Since it is impossible to predict when the kill exactly happens, we don't + -- know how many connects were opened and how many pings were sent. But we + -- should not have any open connections (if we do, collect will throw an + -- error) and we should have at least the number of pings equal to the number + -- of threads we did *not* kill + eventss <- collect endpoint2 Nothing (Just 1000000) + let actualPings = sum . map (length . snd) $ eventss + expectedPings <- takeMVar numAlive + unless (actualPings >= expectedPings) $ + throwIO (userError "Missing pings") + +-- print (actualPings, expectedPings) + + +-- | Set up conditions with a high likelyhood of "crossing" (for transports +-- that multiplex lightweight connections across heavyweight connections) +testCrossing :: Transport -> Int -> IO () +testCrossing transport numRepeats = do + [aAddr, bAddr] <- replicateM 2 newEmptyMVar + [aDone, bDone] <- replicateM 2 newEmptyMVar + [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar + go <- newEmptyMVar + + let hints = defaultConnectHints { + connectTimeout = Just 5000000 + } + + -- A + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar aAddr (address endpoint) + theirAddress <- readMVar bAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + -- Because we are creating lots of connections, it's possible that + -- connect times out (for instance, in the TCP transport, + -- Network.Socket.connect may time out). We shouldn't regard this as an + -- error in the Transport, though. + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar aTimeout () + Left (TransportError ConnectFailed _) -> readMVar bTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err + putMVar aDone () + + -- B + forkTry $ do + Right endpoint <- newEndPoint transport + putMVar bAddr (address endpoint) + theirAddress <- readMVar aAddr + + replicateM_ numRepeats $ do + takeMVar go >> yield + connectResult <- connect endpoint theirAddress ReliableOrdered hints + case connectResult of + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar bTimeout () + Left (TransportError ConnectFailed _) -> readMVar aTimeout + Left err -> throwIO . userError $ "testCrossed: " ++ show err + putMVar bDone () + + -- Driver + forM_ [1 .. numRepeats] $ \_i -> do + -- putStrLn $ "Round " ++ show _i + tryTakeMVar aTimeout + tryTakeMVar bTimeout + putMVar go () + putMVar go () + takeMVar aDone + takeMVar bDone + +-- Transport tests +testTransport :: IO (Either String Transport) -> IO () +testTransport newTransport = do + Right transport <- newTransport + runTests + [ ("PingPong", testPingPong transport numPings) + , ("EndPoints", testEndPoints transport numPings) + , ("Connections", testConnections transport numPings) + , ("CloseOneConnection", testCloseOneConnection transport numPings) + , ("CloseOneDirection", testCloseOneDirection transport numPings) + , ("CloseReopen", testCloseReopen transport numPings) + , ("ParallelConnects", testParallelConnects transport numPings) + , ("SendAfterClose", testSendAfterClose transport 100) + , ("Crossing", testCrossing transport 10) + , ("CloseTwice", testCloseTwice transport 100) + , ("ConnectToSelf", testConnectToSelf transport numPings) + , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) + , ("CloseSelf", testCloseSelf newTransport) + , ("CloseEndPoint", testCloseEndPoint transport numPings) + , ("CloseTransport", testCloseTransport newTransport) + , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) + , ("ExceptionOnReceive", testExceptionOnReceive newTransport) + , ("SendException", testSendException newTransport) + , ("Kill", testKill newTransport 1000) + ] + where + numPings = 10000 :: Int diff --git a/src/Network/Transport/Tests/Auxiliary.hs b/src/Network/Transport/Tests/Auxiliary.hs new file mode 100644 index 00000000..792c5214 --- /dev/null +++ b/src/Network/Transport/Tests/Auxiliary.hs @@ -0,0 +1,112 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Network.Transport.Tests.Auxiliary + ( -- Running tests + runTest + , runTests + -- Writing tests + , forkTry + , trySome + , randomThreadDelay + ) where + +#if ! MIN_VERSION_base(4,6,0) +import Prelude hiding (catch) +#endif + +import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) +import Control.Concurrent.Chan (Chan) +import Control.Monad (liftM2, unless) +import Control.Exception (SomeException, try, catch) +import System.Timeout (timeout) +import System.IO (stdout, hFlush) +import System.Console.ANSI ( SGR(SetColor, Reset) + , Color(Red, Green) + , ConsoleLayer(Foreground) + , ColorIntensity(Vivid) + , setSGR + ) +import System.Random (randomIO) +import Network.Transport +import Network.Transport.Tests.Traced (Traceable(..), traceShow) + +-- | Like fork, but throw exceptions in the child thread to the parent +forkTry :: IO () -> IO ThreadId +forkTry p = do + tid <- myThreadId + forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) + +-- | Like try, but specialized to SomeException +trySome :: IO a -> IO (Either SomeException a) +trySome = try + +-- | Run the given test, catching timeouts and exceptions +runTest :: String -> IO () -> IO Bool +runTest description test = do + putStr $ "Running " ++ show description ++ ": " + hFlush stdout + done <- try . timeout 60000000 $ test -- 60 seconds + case done of + Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" + Right Nothing -> failed $ "(timeout)" + Right (Just ()) -> ok + where + failed :: String -> IO Bool + failed err = do + setSGR [SetColor Foreground Vivid Red] + putStr "failed " + setSGR [Reset] + putStrLn err + return False + + ok :: IO Bool + ok = do + setSGR [SetColor Foreground Vivid Green] + putStrLn "ok" + setSGR [Reset] + return True + +-- | Run a bunch of tests and throw an exception if any fails +runTests :: [(String, IO ())] -> IO () +runTests tests = do + success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests + unless success $ fail "Some tests failed" + +-- | Random thread delay between 0 and the specified max +randomThreadDelay :: Int -> IO () +randomThreadDelay maxDelay = do + delay <- randomIO :: IO Int + threadDelay (delay `mod` maxDelay) + +-------------------------------------------------------------------------------- +-- traceShow instances -- +-------------------------------------------------------------------------------- + +instance Traceable EndPoint where + trace = const Nothing + +instance Traceable Transport where + trace = const Nothing + +instance Traceable Connection where + trace = const Nothing + +instance Traceable Event where + trace = traceShow + +instance Show err => Traceable (TransportError err) where + trace = traceShow + +instance Traceable EndPointAddress where + trace = traceShow . endPointAddressToByteString + +instance Traceable SomeException where + trace = traceShow + +instance Traceable ThreadId where + trace = const Nothing + +instance Traceable (Chan a) where + trace = const Nothing + +instance Traceable Float where + trace = traceShow diff --git a/src/Network/Transport/Tests/Traced.hs b/src/Network/Transport/Tests/Traced.hs new file mode 100644 index 00000000..f0d8d834 --- /dev/null +++ b/src/Network/Transport/Tests/Traced.hs @@ -0,0 +1,200 @@ +-- | Add tracing to the IO monad (see examples). +-- +-- [Usage] +-- +-- > {-# LANGUAGE RebindableSyntax #-} +-- > import Prelude hiding (catch, (>>=), (>>), return, fail) +-- > import Traced +-- +-- [Example] +-- +-- > test1 :: IO Int +-- > test1 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > putStrLn "Hello world" +-- > Right y <- return (Left 2 :: Either Int Int) +-- > return (x + y) +-- +-- outputs +-- +-- > Hello world +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) +-- > Trace: +-- > 0 Left 2 +-- > 1 Left 1 +-- +-- [Guards] +-- +-- Use the following idiom instead of using 'Control.Monad.guard': +-- +-- > test2 :: IO Int +-- > test2 = do +-- > Left x <- return (Left 1 :: Either Int Int) +-- > True <- return (x == 3) +-- > return x +-- +-- The advantage of this idiom is that it gives you line number information when the guard fails: +-- +-- > *Traced> test2 +-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) +-- > Trace: +-- > 0 Left 1 +module Network.Transport.Tests.Traced + ( MonadS(..) + , return + , (>>=) + , (>>) + , fail + , ifThenElse + , Showable(..) + , Traceable(..) + , traceShow + ) where + +import Prelude hiding + ( (>>=) + , return + , fail + , (>>) +#if ! MIN_VERSION_base(4,6,0) + , catch +#endif + ) +import qualified Prelude +import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) +import Control.Applicative ((<$>)) +import Data.Typeable (Typeable) +import Data.Maybe (catMaybes) +import Data.ByteString (ByteString) +import Data.Int (Int32) +import Control.Concurrent.MVar (MVar) + +-------------------------------------------------------------------------------- +-- MonadS class -- +-------------------------------------------------------------------------------- + +-- | Like 'Monad' but bind is only defined for 'Trace'able instances +class MonadS m where + returnS :: a -> m a + bindS :: Traceable a => m a -> (a -> m b) -> m b + failS :: String -> m a + seqS :: m a -> m b -> m b + +-- | Redefinition of 'Prelude.>>=' +(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b +(>>=) = bindS + +-- | Redefinition of 'Prelude.>>' +(>>) :: MonadS m => m a -> m b -> m b +(>>) = seqS + +-- | Redefinition of 'Prelude.return' +return :: MonadS m => a -> m a +return = returnS + +-- | Redefinition of 'Prelude.fail' +fail :: MonadS m => String -> m a +fail = failS + +-------------------------------------------------------------------------------- +-- Trace typeclass (for adding elements to a trace -- +-------------------------------------------------------------------------------- + +data Showable = forall a. Show a => Showable a + +instance Show Showable where + show (Showable x) = show x + +mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable +mapShowable f (Showable x) = f x + +traceShow :: Show a => a -> Maybe Showable +traceShow = Just . Showable + +class Traceable a where + trace :: a -> Maybe Showable + +instance (Traceable a, Traceable b) => Traceable (Either a b) where + trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x + trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y + +instance (Traceable a, Traceable b) => Traceable (a, b) where + trace (x, y) = case (trace x, trace y) of + (Nothing, Nothing) -> Nothing + (Just t1, Nothing) -> traceShow t1 + (Nothing, Just t2) -> traceShow t2 + (Just t1, Just t2) -> traceShow (t1, t2) + +instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where + trace (x, y, z) = case (trace x, trace y, trace z) of + (Nothing, Nothing, Nothing) -> Nothing + (Just t1, Nothing, Nothing) -> traceShow t1 + (Nothing, Just t2, Nothing) -> traceShow t2 + (Just t1, Just t2, Nothing) -> traceShow (t1, t2) + (Nothing, Nothing, Just t3) -> traceShow t3 + (Just t1, Nothing, Just t3) -> traceShow (t1, t3) + (Nothing, Just t2, Just t3) -> traceShow (t2, t3) + (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) + +instance Traceable a => Traceable (Maybe a) where + trace Nothing = traceShow (Nothing :: Maybe ()) + trace (Just x) = mapShowable (Showable . Just) <$> trace x + +instance Traceable a => Traceable [a] where + trace = traceShow . catMaybes . map trace + +instance Traceable () where + trace = const Nothing + +instance Traceable Int where + trace = traceShow + +instance Traceable Int32 where + trace = traceShow + +instance Traceable Bool where + trace = const Nothing + +instance Traceable ByteString where + trace = traceShow + +instance Traceable (MVar a) where + trace = const Nothing + +instance Traceable [Char] where + trace = traceShow + +instance Traceable IOException where + trace = traceShow + +-------------------------------------------------------------------------------- +-- IO instance for MonadS -- +-------------------------------------------------------------------------------- + +data TracedException = TracedException [String] SomeException + deriving Typeable + +instance Exception TracedException + +-- | Add tracing to 'IO' (see examples) +instance MonadS IO where + returnS = Prelude.return + bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) + failS = Prelude.fail + seqS = (Prelude.>>) + +instance Show TracedException where + show (TracedException ts ex) = + show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) + +traceHandlers :: Traceable a => a -> [Handler b] +traceHandlers a = case trace a of + Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] + Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex + , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) + ] + +-- | Definition of 'ifThenElse' for use with RebindableSyntax +ifThenElse :: Bool -> a -> a -> a +ifThenElse True x _ = x +ifThenElse False _ y = y From ab64a7a201a396daebcb69df7e4a86d95e276dd9 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Sep 2012 18:17:30 +0100 Subject: [PATCH 0203/2357] Move the transport tests to a separate package This paves the way for reusing these tests in other transports --- network-transport-tcp.cabal | 30 +- tests/TestAuxiliary.hs | 111 ----- tests/TestTCP.hs | 6 +- tests/TestTransport.hs | 964 ------------------------------------ tests/Traced.hs | 199 -------- 5 files changed, 12 insertions(+), 1298 deletions(-) delete mode 100644 tests/TestAuxiliary.hs delete mode 100644 tests/TestTransport.hs delete mode 100644 tests/Traced.hs diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index dd34c4aa..7516be69 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -10,8 +10,8 @@ Maintainer: edsko@well-typed.com, dcoutts@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com -Synopsis: TCP instantation of Network.Transport -Description: TCP instantation of Network.Transport +Synopsis: TCP instantiation of Network.Transport +Description: TCP instantiation of Network.Transport Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Network @@ -37,23 +37,11 @@ Test-Suite TestTCP Type: exitcode-stdio-1.0 Main-Is: TestTCP.hs Build-Depends: base >= 4.3 && < 5, - network-transport >= 0.3 && < 0.4, - data-accessor >= 0.2 && < 0.3, - containers >= 0.4 && < 0.6, - bytestring >= 0.9 && < 0.11, + network-transport-tests >= 0.1 && < 0.2, network >= 2.3 && < 2.4, - random >= 1.0 && < 1.1, - ansi-terminal >= 0.5 && < 0.6, - mtl >= 2.0 && < 2.2 - Other-modules: TestAuxiliary, - TestTransport, - Traced - ghc-options: -Wall -fno-warn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N - Extensions: ExistentialQuantification, - FlexibleInstances, - DeriveDataTypeable, - RankNTypes, - OverlappingInstances, - OverloadedStrings, - CPP - HS-Source-Dirs: tests src + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4 + ghc-options: -threaded -rtsopts -with-rtsopts=-N + HS-Source-Dirs: tests + Extensions: CPP, + OverloadedStrings diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs deleted file mode 100644 index f76bf819..00000000 --- a/tests/TestAuxiliary.hs +++ /dev/null @@ -1,111 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -module TestAuxiliary ( -- Running tests - runTest - , runTests - -- Writing tests - , forkTry - , trySome - , randomThreadDelay - ) where - -#if ! MIN_VERSION_base(4,6,0) -import Prelude hiding (catch) -#endif - -import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) -import Control.Concurrent.Chan (Chan) -import Control.Monad (liftM2, unless) -import Control.Exception (SomeException, try, catch) -import System.Timeout (timeout) -import System.IO (stdout, hFlush) -import System.Console.ANSI ( SGR(SetColor, Reset) - , Color(Red, Green) - , ConsoleLayer(Foreground) - , ColorIntensity(Vivid) - , setSGR - ) -import System.Random (randomIO) -import Network.Transport -import Traced (Traceable(..), traceShow) - --- | Like fork, but throw exceptions in the child thread to the parent -forkTry :: IO () -> IO ThreadId -forkTry p = do - tid <- myThreadId - forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) - --- | Like try, but specialized to SomeException -trySome :: IO a -> IO (Either SomeException a) -trySome = try - --- | Run the given test, catching timeouts and exceptions -runTest :: String -> IO () -> IO Bool -runTest description test = do - putStr $ "Running " ++ show description ++ ": " - hFlush stdout - done <- try . timeout 60000000 $ test -- 60 seconds - case done of - Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" - Right Nothing -> failed $ "(timeout)" - Right (Just ()) -> ok - where - failed :: String -> IO Bool - failed err = do - setSGR [SetColor Foreground Vivid Red] - putStr "failed " - setSGR [Reset] - putStrLn err - return False - - ok :: IO Bool - ok = do - setSGR [SetColor Foreground Vivid Green] - putStrLn "ok" - setSGR [Reset] - return True - --- | Run a bunch of tests and throw an exception if any fails -runTests :: [(String, IO ())] -> IO () -runTests tests = do - success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests - unless success $ fail "Some tests failed" - --- | Random thread delay between 0 and the specified max -randomThreadDelay :: Int -> IO () -randomThreadDelay maxDelay = do - delay <- randomIO :: IO Int - threadDelay (delay `mod` maxDelay) - --------------------------------------------------------------------------------- --- traceShow instances -- --------------------------------------------------------------------------------- - -instance Traceable EndPoint where - trace = const Nothing - -instance Traceable Transport where - trace = const Nothing - -instance Traceable Connection where - trace = const Nothing - -instance Traceable Event where - trace = traceShow - -instance Show err => Traceable (TransportError err) where - trace = traceShow - -instance Traceable EndPointAddress where - trace = traceShow . endPointAddressToByteString - -instance Traceable SomeException where - trace = traceShow - -instance Traceable ThreadId where - trace = const Nothing - -instance Traceable (Chan a) where - trace = const Nothing - -instance Traceable Float where - trace = traceShow diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 57981cf5..58f60a94 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -11,8 +11,6 @@ import Prelude hiding , catch #endif ) -import TestTransport (testTransport) -import TestAuxiliary (forkTry, runTests) import Network.Transport import Network.Transport.TCP ( createTransport , createTransportExposeInternals @@ -54,10 +52,12 @@ import qualified Network.Socket as N ( sClose ) import Network.Socket.ByteString (sendMany) import Data.String (fromString) -import Traced import GHC.IO.Exception (ioe_errno) import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) import System.Timeout (timeout) +import Network.Transport.Tests (testTransport) +import Network.Transport.Tests.Auxiliary (forkTry, runTests) +import Network.Transport.Tests.Traced instance Traceable ControlHeader where trace = traceShow diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs deleted file mode 100644 index f1381bbf..00000000 --- a/tests/TestTransport.hs +++ /dev/null @@ -1,964 +0,0 @@ -{-# LANGUAGE RebindableSyntax #-} -module TestTransport where - -import Prelude hiding - ( (>>=) - , return - , fail - , (>>) -#if ! MIN_VERSION_base(4,6,0) - , catch -#endif - ) -import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) -import Control.Concurrent (forkIO, killThread, yield) -import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) -import Control.Exception (evaluate, throw, throwIO, bracket) -import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) -import Control.Monad.Error () -import Control.Applicative ((<$>)) -import Network.Transport -import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) -import Network.Transport.Util (spawn) -import System.Random (randomIO) -import Data.ByteString (ByteString) -import Data.ByteString.Char8 (pack) -import Data.Map (Map) -import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) -import Data.String (fromString) -import Data.List (permutations) -import Traced - --- | Server that echoes messages straight back to the origin endpoint. -echoServer :: EndPoint -> IO () -echoServer endpoint = do - go Map.empty - where - go :: Map ConnectionId Connection -> IO () - go cs = do - event <- receive endpoint - case event of - ConnectionOpened cid rel addr -> do - tlog $ "Opened new connection " ++ show cid - Right conn <- connect endpoint addr rel defaultConnectHints - go (Map.insert cid conn cs) - Received cid payload -> do - send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload - go cs - ConnectionClosed cid -> do - tlog $ "Close connection " ++ show cid - close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) - go (Map.delete cid cs) - ReceivedMulticast _ _ -> - -- Ignore - go cs - ErrorEvent _ -> - putStrLn $ "Echo server received error event: " ++ show event - EndPointClosed -> - return () - --- | Ping client used in a few tests -ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () -ping endpoint server numPings msg = do - -- Open connection to the server - tlog "Connect to echo server" - Right conn <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Wait for the server to open reply connection - tlog "Wait for ConnectionOpened message" - ConnectionOpened cid _ _ <- receive endpoint - - -- Send pings and wait for reply - tlog "Send ping and wait for reply" - replicateM_ numPings $ do - send conn [msg] - Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg - return () - - -- Close the connection - tlog "Close the connection" - close conn - - -- Wait for the server to close its connection to us - tlog "Wait for ConnectionClosed message" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - - -- Done - tlog "Ping client done" - --- | Basic ping test -testPingPong :: Transport -> Int -> IO () -testPingPong transport numPings = do - tlog "Starting ping pong test" - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - tlog "Ping client" - Right endpoint <- newEndPoint transport - ping endpoint server numPings "ping" - putMVar result () - - takeMVar result - --- | Test that endpoints don't get confused -testEndPoints :: Transport -> Int -> IO () -testEndPoints transport numPings = do - server <- spawn transport echoServer - dones <- replicateM 2 newEmptyMVar - - forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do - let name' :: ByteString - name' = pack [name] - Right endpoint <- newEndPoint transport - tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) - ping endpoint server numPings name' - putMVar done () - - forM_ dones takeMVar - --- Test that connections don't get confused -testConnections :: Transport -> Int -> IO () -testConnections transport numPings = do - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - - -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv1 _ _ <- receive endpoint - - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv2 _ _ <- receive endpoint - - -- One thread to send "pingA" on the first connection - forkTry $ replicateM_ numPings $ send conn1 ["pingA"] - - -- One thread to send "pingB" on the second connection - forkTry $ replicateM_ numPings $ send conn2 ["pingB"] - - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do - event <- receive endpoint - case event of - Received cid [payload] -> do - when (cid == serv1 && payload /= "pingA") $ error "Wrong message" - when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n - verifyResponse (2 * numPings) - - takeMVar result - --- | Test that closing one connection does not close the other -testCloseOneConnection :: Transport -> Int -> IO () -testCloseOneConnection transport numPings = do - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - - -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv1 _ _ <- receive endpoint - - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv2 _ _ <- receive endpoint - - -- One thread to send "pingA" on the first connection - forkTry $ do - replicateM_ numPings $ send conn1 ["pingA"] - close conn1 - - -- One thread to send "pingB" on the second connection - forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] - - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do - event <- receive endpoint - case event of - Received cid [payload] -> do - when (cid == serv1 && payload /= "pingA") $ error "Wrong message" - when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n - verifyResponse (3 * numPings) - - takeMVar result - --- | Test that if A connects to B and B connects to A, B can still send to A after --- A closes its connection to B (for instance, in the TCP transport, the socket pair --- connecting A and B should not yet be closed). -testCloseOneDirection :: Transport -> Int -> IO () -testCloseOneDirection transport numPings = do - addrA <- newEmptyMVar - addrB <- newEmptyMVar - doneA <- newEmptyMVar - doneB <- newEmptyMVar - - -- A - forkTry $ do - tlog "A" - Right endpoint <- newEndPoint transport - tlog (show (address endpoint)) - putMVar addrA (address endpoint) - - -- Connect to B - tlog "Connect to B" - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - -- Wait for B to connect to us - tlog "Wait for B" - ConnectionOpened cid _ _ <- receive endpoint - - -- Send pings to B - tlog "Send pings to B" - replicateM_ numPings $ send conn ["ping"] - - -- Close our connection to B - tlog "Close connection" - close conn - - -- Wait for B's pongs - tlog "Wait for pongs from B" - replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () - - -- Wait for B to close it's connection to us - tlog "Wait for B to close connection" - ConnectionClosed cid' <- receive endpoint - guard (cid == cid') - - -- Done - tlog "Done" - putMVar doneA () - - -- B - forkTry $ do - tlog "B" - Right endpoint <- newEndPoint transport - tlog (show (address endpoint)) - putMVar addrB (address endpoint) - - -- Wait for A to connect - tlog "Wait for A to connect" - ConnectionOpened cid _ _ <- receive endpoint - - -- Connect to A - tlog "Connect to A" - Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - -- Wait for A's pings - tlog "Wait for pings from A" - replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () - - -- Wait for A to close it's connection to us - tlog "Wait for A to close connection" - ConnectionClosed cid' <- receive endpoint - guard (cid == cid') - - -- Send pongs to A - tlog "Send pongs to A" - replicateM_ numPings $ send conn ["pong"] - - -- Close our connection to A - tlog "Close connection to A" - close conn - - -- Done - tlog "Done" - putMVar doneB () - - mapM_ takeMVar [doneA, doneB] - --- | Collect events and order them by connection ID -collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] -collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty - where - -- TODO: for more serious use of this function we'd need to make these arguments strict - go (Just 0) open closed = finish open closed - go n open closed = do - mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint - case mEvent of - Left _ -> finish open closed - Right event -> do - let n' = (\x -> x - 1) <$> n - case event of - ConnectionOpened cid _ _ -> - go n' (Map.insert cid [] open) closed - ConnectionClosed cid -> - let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in - go n' (Map.delete cid open) (Map.insert cid list closed) - Received cid msg -> - go n' (Map.adjust (msg :) cid open) closed - ReceivedMulticast _ _ -> - fail "Unexpected multicast" - ErrorEvent _ -> - fail "Unexpected error" - EndPointClosed -> - fail "Unexpected endpoint closure" - - finish open closed = - if Map.null open - then return . Map.toList . Map.map reverse $ closed - else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) - --- | Open connection, close it, then reopen it --- (In the TCP transport this means the socket will be closed, then reopened) --- --- Note that B cannot expect to receive all of A's messages on the first connection --- before receiving the messages on the second connection. What might (and sometimes --- does) happen is that finishes sending all of its messages on the first connection --- (in the TCP transport, the first socket pair) while B is behind on reading _from_ --- this connection (socket pair) -- the messages are "in transit" on the network --- (these tests are done on localhost, so there are in some OS buffer). Then when --- A opens the second connection (socket pair) B will spawn a new thread for this --- connection, and hence might start interleaving messages from the first and second --- connection. --- --- This is correct behaviour, however: the transport API guarantees reliability and --- ordering _per connection_, but not _across_ connections. -testCloseReopen :: Transport -> Int -> IO () -testCloseReopen transport numPings = do - addrB <- newEmptyMVar - doneB <- newEmptyMVar - - let numRepeats = 2 :: Int - - -- A - forkTry $ do - Right endpoint <- newEndPoint transport - - forM_ [1 .. numRepeats] $ \i -> do - tlog "A connecting" - -- Connect to B - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - tlog "A pinging" - -- Say hi - forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] - - tlog "A closing" - -- Disconnect again - close conn - - tlog "A finishing" - - -- B - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar addrB (address endpoint) - - eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing - - forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do - forM_ (zip [1 .. numPings] events) $ \(j, event) -> do - guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) - - putMVar doneB () - - takeMVar doneB - --- | Test lots of parallel connection attempts -testParallelConnects :: Transport -> Int -> IO () -testParallelConnects transport numPings = do - server <- spawn transport echoServer - done <- newEmptyMVar - - Right endpoint <- newEndPoint transport - - -- Spawn lots of clients - forM_ [1 .. numPings] $ \i -> forkTry $ do - Right conn <- connect endpoint server ReliableOrdered defaultConnectHints - send conn [pack $ "ping" ++ show i] - send conn [pack $ "ping" ++ show i] - close conn - - forkTry $ do - eventss <- collect endpoint (Just (numPings * 4)) Nothing - -- Check that no pings got sent to the wrong connection - forM_ eventss $ \(_, [[ping1], [ping2]]) -> - guard (ping1 == ping2) - putMVar done () - - takeMVar done - --- | Test that sending on a closed connection gives an error -testSendAfterClose :: Transport -> Int -> IO () -testSendAfterClose transport numRepeats = do - server <- spawn transport echoServer - clientDone <- newEmptyMVar - - forkTry $ do - Right endpoint <- newEndPoint transport - - -- We request two lightweight connections - replicateM numRepeats $ do - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Close the second, but leave the first open; then output on the second - -- connection (i.e., on a closed connection while there is still another - -- connection open) - close conn2 - Left (TransportError SendClosed _) <- send conn2 ["ping2"] - - -- Now close the first connection, and output on it (i.e., output while - -- there are no lightweight connection at all anymore) - close conn1 - Left (TransportError SendClosed _) <- send conn2 ["ping2"] - - return () - - putMVar clientDone () - - takeMVar clientDone - --- | Test that closing the same connection twice has no effect -testCloseTwice :: Transport -> Int -> IO () -testCloseTwice transport numRepeats = do - server <- spawn transport echoServer - clientDone <- newEmptyMVar - - forkTry $ do - Right endpoint <- newEndPoint transport - - replicateM numRepeats $ do - -- We request two lightweight connections - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Close the second one twice - close conn2 - close conn2 - - -- Then send a message on the first and close that twice too - send conn1 ["ping"] - close conn1 - - -- Verify expected response from the echo server - ConnectionOpened cid1 _ _ <- receive endpoint - ConnectionOpened cid2 _ _ <- receive endpoint - ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 - Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 - ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 - - return () - - putMVar clientDone () - - takeMVar clientDone - --- | Test that we can connect an endpoint to itself -testConnectToSelf :: Transport -> Int -> IO () -testConnectToSelf transport numPings = do - done <- newEmptyMVar - Right endpoint <- newEndPoint transport - - tlog "Creating self-connection" - Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - - tlog "Talk to myself" - - -- One thread to write to the endpoint - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn ["ping"] - - tlog $ "Closing connection" - close conn - - -- And one thread to read - forkTry $ do - tlog $ "reading" - - tlog "Waiting for ConnectionOpened" - ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint - - tlog "Waiting for Received" - replicateM_ numPings $ do - Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - return () - - tlog "Waiting for ConnectionClosed" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - - tlog "Done" - putMVar done () - - takeMVar done - --- | Test that we can connect an endpoint to itself multiple times -testConnectToSelfTwice :: Transport -> Int -> IO () -testConnectToSelfTwice transport numPings = do - done <- newEmptyMVar - Right endpoint <- newEndPoint transport - - tlog "Creating self-connection" - Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - - tlog "Talk to myself" - - -- One thread to write to the endpoint using the first connection - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn1 ["pingA"] - - tlog $ "Closing connection" - close conn1 - - -- One thread to write to the endpoint using the second connection - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn2 ["pingB"] - - tlog $ "Closing connection" - close conn2 - - -- And one thread to read - forkTry $ do - tlog $ "reading" - - [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing - True <- return $ events1 == replicate numPings ["pingA"] - True <- return $ events2 == replicate numPings ["pingB"] - - tlog "Done" - putMVar done () - - takeMVar done - --- | Test that we self-connections no longer work once we close our endpoint --- or our transport -testCloseSelf :: IO (Either String Transport) -> IO () -testCloseSelf newTransport = do - Right transport <- newTransport - Right endpoint1 <- newEndPoint transport - Right endpoint2 <- newEndPoint transport - Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - - -- Close the conneciton and try to send - close conn1 - Left (TransportError SendClosed _) <- send conn1 ["ping"] - - -- Close the first endpoint. We should not be able to use the first - -- connection anymore, or open more self connections, but the self connection - -- to the second endpoint should still be fine - closeEndPoint endpoint1 - Left (TransportError SendFailed _) <- send conn2 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right () <- send conn3 ["ping"] - - -- Close the transport; now the second should no longer work - closeTransport transport - Left (TransportError SendFailed _) <- send conn3 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - - return () - --- | Test various aspects of 'closeEndPoint' -testCloseEndPoint :: Transport -> Int -> IO () -testCloseEndPoint transport _ = do - serverDone <- newEmptyMVar - clientDone <- newEmptyMVar - clientAddr1 <- newEmptyMVar - clientAddr2 <- newEmptyMVar - serverAddr <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- First test (see client) - do - theirAddr <- readMVar clientAddr1 - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - return () - - -- Second test - do - theirAddr <- readMVar clientAddr2 - - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn ["pong"] - - ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' - ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint ; True <- return $ addr' == theirAddr - - Left (TransportError SendFailed _) <- send conn ["pong2"] - - return () - - putMVar serverDone () - - -- Client - forkTry $ do - theirAddr <- readMVar serverAddr - - -- First test: close endpoint with one outgoing but no incoming connections - do - Right endpoint <- newEndPoint transport - putMVar clientAddr1 (address endpoint) - - -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - closeEndPoint endpoint - EndPointClosed <- receive endpoint - return () - - -- Second test: close endpoint with one outgoing and one incoming connection - do - Right endpoint <- newEndPoint transport - putMVar clientAddr2 (address endpoint) - - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn ["ping"] - - -- Reply from the server - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' - - -- Close the endpoint - closeEndPoint endpoint - EndPointClosed <- receive endpoint - - -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- An attempt to close the already closed connection should just return - () <- close conn - - -- And so should an attempt to connect - Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - return () - - putMVar clientDone () - - mapM_ takeMVar [serverDone, clientDone] - --- Test closeTransport --- --- This tests many of the same things that testEndPoint does, and some more -testCloseTransport :: IO (Either String Transport) -> IO () -testCloseTransport newTransport = do - serverDone <- newEmptyMVar - clientDone <- newEmptyMVar - clientAddr1 <- newEmptyMVar - clientAddr2 <- newEmptyMVar - serverAddr <- newEmptyMVar - - -- Server - forkTry $ do - Right transport <- newTransport - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- Client sets up first endpoint - theirAddr1 <- readMVar clientAddr1 - ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 - - -- Client sets up second endpoint - theirAddr2 <- readMVar clientAddr2 - - ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 - Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 - - Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints - send conn ["pong"] - - -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) - evs <- replicateM 3 $ receive endpoint - let expected = [ ConnectionClosed cid1 - , ConnectionClosed cid2 - , ErrorEvent (TransportError (EventConnectionLost theirAddr2) "") - ] - True <- return $ any (== expected) (permutations evs) - - -- An attempt to send to the endpoint should now fail - Left (TransportError SendFailed _) <- send conn ["pong2"] - - putMVar serverDone () - - -- Client - forkTry $ do - Right transport <- newTransport - theirAddr <- readMVar serverAddr - - -- Set up endpoint with one outgoing but no incoming connections - Right endpoint1 <- newEndPoint transport - putMVar clientAddr1 (address endpoint1) - - -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints - - -- Set up an endpoint with one outgoing and out incoming connection - Right endpoint2 <- newEndPoint transport - putMVar clientAddr2 (address endpoint2) - - Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints - send conn ["ping"] - - -- Reply from the server - ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr - Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' - - -- Now shut down the entire transport - closeTransport transport - - -- Both endpoints should report that they have been closed - EndPointClosed <- receive endpoint1 - EndPointClosed <- receive endpoint2 - - -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- An attempt to close the already closed connection should just return - () <- close conn - - -- And so should an attempt to connect on either endpoint - Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints - Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints - - -- And finally, so should an attempt to create a new endpoint - Left (TransportError NewEndPointFailed _) <- newEndPoint transport - - putMVar clientDone () - - mapM_ takeMVar [serverDone, clientDone] - --- | Remote node attempts to connect to a closed local endpoint -testConnectClosedEndPoint :: Transport -> IO () -testConnectClosedEndPoint transport = do - serverAddr <- newEmptyMVar - serverClosed <- newEmptyMVar - clientDone <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - closeEndPoint endpoint - putMVar serverClosed () - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - readMVar serverClosed - - Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - putMVar clientDone () - - takeMVar clientDone - --- | We should receive an exception when doing a 'receive' after we have been --- notified that an endpoint has been closed -testExceptionOnReceive :: IO (Either String Transport) -> IO () -testExceptionOnReceive newTransport = do - Right transport <- newTransport - - -- Test one: when we close an endpoint specifically - Right endpoint1 <- newEndPoint transport - closeEndPoint endpoint1 - EndPointClosed <- receive endpoint1 - Left _ <- trySome (receive endpoint1 >>= evaluate) - - -- Test two: when we close the entire transport - Right endpoint2 <- newEndPoint transport - closeTransport transport - EndPointClosed <- receive endpoint2 - Left _ <- trySome (receive endpoint2 >>= evaluate) - - return () - --- | Test what happens when the argument to 'send' is an exceptional value -testSendException :: IO (Either String Transport) -> IO () -testSendException newTransport = do - Right transport <- newTransport - Right endpoint1 <- newEndPoint transport - Right endpoint2 <- newEndPoint transport - - -- Connect endpoint1 to endpoint2 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - ConnectionOpened _ _ _ <- receive endpoint2 - - -- Send an exceptional value - Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") - - -- This will have been as a failure to send by endpoint1, which will - -- therefore have closed the socket. In turn this will have caused endpoint2 - -- to report that the connection was lost - ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint1 - ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint2 - - -- A new connection will re-establish the connection - Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - send conn2 ["ping"] - close conn2 - - ConnectionOpened _ _ _ <- receive endpoint2 - Received _ ["ping"] <- receive endpoint2 - ConnectionClosed _ <- receive endpoint2 - - return () - --- | If threads get killed while executing a 'connect', 'send', or 'close', this --- should not affect other threads. --- --- The intention of this test is to see what happens when a asynchronous --- exception happes _while executing a send_. This is exceedingly difficult to --- guarantee, however. Hence we run a large number of tests and insert random --- thread delays -- and even then it might not happen. Moreover, it will only --- happen when we run on multiple cores. -testKill :: IO (Either String Transport) -> Int -> IO () -testKill newTransport numThreads = do - Right transport1 <- newTransport - Right transport2 <- newTransport - Right endpoint1 <- newEndPoint transport1 - Right endpoint2 <- newEndPoint transport2 - - threads <- replicateM numThreads . forkIO $ do - randomThreadDelay 100 - bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) - -- Note that we should not insert a randomThreadDelay into the - -- exception handler itself as this means that the exception handler - -- could be interrupted and we might not close - (\(Right conn) -> close conn) - (\(Right conn) -> do randomThreadDelay 100 - Right () <- send conn ["ping"] - randomThreadDelay 100) - - numAlive <- newMVar (0 :: Int) - - -- Kill half of those threads - forkIO . forM_ threads $ \tid -> do - shouldKill <- randomIO - if shouldKill - then randomThreadDelay 600 >> killThread tid - else modifyMVar_ numAlive (return . (+ 1)) - - -- Since it is impossible to predict when the kill exactly happens, we don't - -- know how many connects were opened and how many pings were sent. But we - -- should not have any open connections (if we do, collect will throw an - -- error) and we should have at least the number of pings equal to the number - -- of threads we did *not* kill - eventss <- collect endpoint2 Nothing (Just 1000000) - let actualPings = sum . map (length . snd) $ eventss - expectedPings <- takeMVar numAlive - unless (actualPings >= expectedPings) $ - throwIO (userError "Missing pings") - --- print (actualPings, expectedPings) - - --- | Set up conditions with a high likelyhood of "crossing" (for transports --- that multiplex lightweight connections across heavyweight connections) -testCrossing :: Transport -> Int -> IO () -testCrossing transport numRepeats = do - [aAddr, bAddr] <- replicateM 2 newEmptyMVar - [aDone, bDone] <- replicateM 2 newEmptyMVar - [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar - go <- newEmptyMVar - - let hints = defaultConnectHints { - connectTimeout = Just 5000000 - } - - -- A - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar aAddr (address endpoint) - theirAddress <- readMVar bAddr - - replicateM_ numRepeats $ do - takeMVar go >> yield - -- Because we are creating lots of connections, it's possible that - -- connect times out (for instance, in the TCP transport, - -- Network.Socket.connect may time out). We shouldn't regard this as an - -- error in the Transport, though. - connectResult <- connect endpoint theirAddress ReliableOrdered hints - case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar aTimeout () - Left (TransportError ConnectFailed _) -> readMVar bTimeout - Left err -> throwIO . userError $ "testCrossed: " ++ show err - putMVar aDone () - - -- B - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar bAddr (address endpoint) - theirAddress <- readMVar aAddr - - replicateM_ numRepeats $ do - takeMVar go >> yield - connectResult <- connect endpoint theirAddress ReliableOrdered hints - case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar bTimeout () - Left (TransportError ConnectFailed _) -> readMVar aTimeout - Left err -> throwIO . userError $ "testCrossed: " ++ show err - putMVar bDone () - - -- Driver - forM_ [1 .. numRepeats] $ \_i -> do - -- putStrLn $ "Round " ++ show _i - tryTakeMVar aTimeout - tryTakeMVar bTimeout - putMVar go () - putMVar go () - takeMVar aDone - takeMVar bDone - --- Transport tests -testTransport :: IO (Either String Transport) -> IO () -testTransport newTransport = do - Right transport <- newTransport - runTests - [ ("PingPong", testPingPong transport numPings) - , ("EndPoints", testEndPoints transport numPings) - , ("Connections", testConnections transport numPings) - , ("CloseOneConnection", testCloseOneConnection transport numPings) - , ("CloseOneDirection", testCloseOneDirection transport numPings) - , ("CloseReopen", testCloseReopen transport numPings) - , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport 100) - , ("Crossing", testCrossing transport 10) - , ("CloseTwice", testCloseTwice transport 100) - , ("ConnectToSelf", testConnectToSelf transport numPings) - , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) - , ("CloseSelf", testCloseSelf newTransport) - , ("CloseEndPoint", testCloseEndPoint transport numPings) - , ("CloseTransport", testCloseTransport newTransport) - , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) - , ("ExceptionOnReceive", testExceptionOnReceive newTransport) - , ("SendException", testSendException newTransport) - , ("Kill", testKill newTransport 1000) - ] - where - numPings = 10000 :: Int diff --git a/tests/Traced.hs b/tests/Traced.hs deleted file mode 100644 index d001c7a2..00000000 --- a/tests/Traced.hs +++ /dev/null @@ -1,199 +0,0 @@ --- | Add tracing to the IO monad (see examples). --- --- [Usage] --- --- > {-# LANGUAGE RebindableSyntax #-} --- > import Prelude hiding (catch, (>>=), (>>), return, fail) --- > import Traced --- --- [Example] --- --- > test1 :: IO Int --- > test1 = do --- > Left x <- return (Left 1 :: Either Int Int) --- > putStrLn "Hello world" --- > Right y <- return (Left 2 :: Either Int Int) --- > return (x + y) --- --- outputs --- --- > Hello world --- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) --- > Trace: --- > 0 Left 2 --- > 1 Left 1 --- --- [Guards] --- --- Use the following idiom instead of using 'Control.Monad.guard': --- --- > test2 :: IO Int --- > test2 = do --- > Left x <- return (Left 1 :: Either Int Int) --- > True <- return (x == 3) --- > return x --- --- The advantage of this idiom is that it gives you line number information when the guard fails: --- --- > *Traced> test2 --- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) --- > Trace: --- > 0 Left 1 -module Traced ( MonadS(..) - , return - , (>>=) - , (>>) - , fail - , ifThenElse - , Showable(..) - , Traceable(..) - , traceShow - ) where - -import Prelude hiding - ( (>>=) - , return - , fail - , (>>) -#if ! MIN_VERSION_base(4,6,0) - , catch -#endif - ) -import qualified Prelude -import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) -import Control.Applicative ((<$>)) -import Data.Typeable (Typeable) -import Data.Maybe (catMaybes) -import Data.ByteString (ByteString) -import Data.Int (Int32) -import Control.Concurrent.MVar (MVar) - --------------------------------------------------------------------------------- --- MonadS class -- --------------------------------------------------------------------------------- - --- | Like 'Monad' but bind is only defined for 'Trace'able instances -class MonadS m where - returnS :: a -> m a - bindS :: Traceable a => m a -> (a -> m b) -> m b - failS :: String -> m a - seqS :: m a -> m b -> m b - --- | Redefinition of 'Prelude.>>=' -(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b -(>>=) = bindS - --- | Redefinition of 'Prelude.>>' -(>>) :: MonadS m => m a -> m b -> m b -(>>) = seqS - --- | Redefinition of 'Prelude.return' -return :: MonadS m => a -> m a -return = returnS - --- | Redefinition of 'Prelude.fail' -fail :: MonadS m => String -> m a -fail = failS - --------------------------------------------------------------------------------- --- Trace typeclass (for adding elements to a trace -- --------------------------------------------------------------------------------- - -data Showable = forall a. Show a => Showable a - -instance Show Showable where - show (Showable x) = show x - -mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable -mapShowable f (Showable x) = f x - -traceShow :: Show a => a -> Maybe Showable -traceShow = Just . Showable - -class Traceable a where - trace :: a -> Maybe Showable - -instance (Traceable a, Traceable b) => Traceable (Either a b) where - trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x - trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y - -instance (Traceable a, Traceable b) => Traceable (a, b) where - trace (x, y) = case (trace x, trace y) of - (Nothing, Nothing) -> Nothing - (Just t1, Nothing) -> traceShow t1 - (Nothing, Just t2) -> traceShow t2 - (Just t1, Just t2) -> traceShow (t1, t2) - -instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where - trace (x, y, z) = case (trace x, trace y, trace z) of - (Nothing, Nothing, Nothing) -> Nothing - (Just t1, Nothing, Nothing) -> traceShow t1 - (Nothing, Just t2, Nothing) -> traceShow t2 - (Just t1, Just t2, Nothing) -> traceShow (t1, t2) - (Nothing, Nothing, Just t3) -> traceShow t3 - (Just t1, Nothing, Just t3) -> traceShow (t1, t3) - (Nothing, Just t2, Just t3) -> traceShow (t2, t3) - (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) - -instance Traceable a => Traceable (Maybe a) where - trace Nothing = traceShow (Nothing :: Maybe ()) - trace (Just x) = mapShowable (Showable . Just) <$> trace x - -instance Traceable a => Traceable [a] where - trace = traceShow . catMaybes . map trace - -instance Traceable () where - trace = const Nothing - -instance Traceable Int where - trace = traceShow - -instance Traceable Int32 where - trace = traceShow - -instance Traceable Bool where - trace = const Nothing - -instance Traceable ByteString where - trace = traceShow - -instance Traceable (MVar a) where - trace = const Nothing - -instance Traceable [Char] where - trace = traceShow - -instance Traceable IOException where - trace = traceShow - --------------------------------------------------------------------------------- --- IO instance for MonadS -- --------------------------------------------------------------------------------- - -data TracedException = TracedException [String] SomeException - deriving Typeable - -instance Exception TracedException - --- | Add tracing to 'IO' (see examples) -instance MonadS IO where - returnS = Prelude.return - bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) - failS = Prelude.fail - seqS = (Prelude.>>) - -instance Show TracedException where - show (TracedException ts ex) = - show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) - -traceHandlers :: Traceable a => a -> [Handler b] -traceHandlers a = case trace a of - Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] - Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex - , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) - ] - --- | Definition of 'ifThenElse' for use with RebindableSyntax -ifThenElse :: Bool -> a -> a -> a -ifThenElse True x _ = x -ifThenElse False _ y = y From 8546c200b89cee2b6edb048086782f13d033b7f7 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 4 Sep 2012 08:46:07 +0100 Subject: [PATCH 0204/2357] Use N.T.Tests and update to work with N.T 0.3 --- network-transport-tests.cabal | 1 + src/Network/Transport/Tests/Multicast.hs | 72 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/Network/Transport/Tests/Multicast.hs diff --git a/network-transport-tests.cabal b/network-transport-tests.cabal index 0f3d2053..2bafd0b8 100644 --- a/network-transport-tests.cabal +++ b/network-transport-tests.cabal @@ -14,6 +14,7 @@ cabal-version: >=1.8 library exposed-modules: Network.Transport.Tests, + Network.Transport.Tests.Multicast, Network.Transport.Tests.Auxiliary, Network.Transport.Tests.Traced -- other-modules: diff --git a/src/Network/Transport/Tests/Multicast.hs b/src/Network/Transport/Tests/Multicast.hs new file mode 100644 index 00000000..f68a2cb6 --- /dev/null +++ b/src/Network/Transport/Tests/Multicast.hs @@ -0,0 +1,72 @@ +module Network.Transport.Tests.Multicast where + +import Network.Transport +import Control.Monad (replicateM, replicateM_, forM_, when) +import Control.Concurrent (forkIO) +import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar, readMVar) +import Data.ByteString (ByteString) +import Data.List (elemIndex) +import Network.Transport.Tests.Auxiliary (runTests) + +-- | Node for the "No confusion" test +noConfusionNode :: Transport -- ^ Transport + -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to + -> [MVar ()] -- ^ I'm ready : others ready + -> Int -- ^ number of pings + -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') + -> MVar () -- ^ I'm done + -> IO () +noConfusionNode transport groups ready numPings msgs done = do + -- Create a new endpoint + Right endpoint <- newEndPoint transport + + -- Create a new multicast group and broadcast its address + Right myGroup <- newMulticastGroup endpoint + putMVar (head groups) (multicastAddress myGroup) + + -- Subscribe to the given multicast groups + addrs <- mapM readMVar (tail groups) + forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr + multicastSubscribe group + + -- Indicate that we're ready and wait for everybody else to be ready + putMVar (head ready) () + mapM_ readMVar (tail ready) + + -- Send messages.. + forkIO . replicateM_ numPings $ multicastSend myGroup [head msgs] + + -- ..while checking that the messages we receive are the right ones + replicateM_ (2 * numPings) $ do + event <- receive endpoint + case event of + ReceivedMulticast addr [msg] -> + let mix = addr `elemIndex` addrs in + case mix of + Nothing -> error "Message from unexpected source" + Just ix -> when (msgs !! (ix + 1) /= msg) $ error "Unexpected message" + _ -> + error "Unexpected event" + + -- Success + putMVar done () + +-- | Test that distinct multicast groups are not confused +testNoConfusion :: Transport -> Int -> IO () +testNoConfusion transport numPings = do + [group1, group2, group3] <- replicateM 3 newEmptyMVar + [readyA, readyB, readyC] <- replicateM 3 newEmptyMVar + [doneA, doneB, doneC] <- replicateM 3 newEmptyMVar + let [msgA, msgB, msgC] = ["A says hi", "B says hi", "C says hi"] + + forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA + forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB + forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC + + mapM_ takeMVar [doneA, doneB, doneC] + +-- | Test multicast +testMulticast :: Transport -> IO () +testMulticast transport = + runTests + [ ("NoConfusion", testNoConfusion transport 10000) ] From 6b849f0a8545537f441650f3a17d34e69fe09643 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 4 Sep 2012 08:46:07 +0100 Subject: [PATCH 0205/2357] Use N.T.Tests and update to work with N.T 0.3 --- network-transport-inmemory.cabal | 40 +- tests/TestAuxiliary.hs | 108 ---- tests/TestInMemory.hs | 2 +- tests/TestMulticast.hs | 72 --- tests/TestMulticastInMemory.hs | 2 +- tests/TestTransport.hs | 956 ------------------------------- tests/Traced.hs | 191 ------ 7 files changed, 12 insertions(+), 1359 deletions(-) delete mode 100644 tests/TestAuxiliary.hs delete mode 100644 tests/TestMulticast.hs delete mode 100644 tests/TestTransport.hs delete mode 100644 tests/Traced.hs diff --git a/network-transport-inmemory.cabal b/network-transport-inmemory.cabal index a1b6e282..f4960144 100644 --- a/network-transport-inmemory.cabal +++ b/network-transport-inmemory.cabal @@ -1,5 +1,5 @@ Name: network-transport-inmemory -Version: 0.2.0 +Version: 0.3.0 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -10,14 +10,14 @@ Maintainer: edsko@well-typed.com, dcoutts@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com -Synopsis: In-memory instantation of Network.Transport -Description: In-memory instantation of Network.Transport +Synopsis: In-memory instantiation of Network.Transport +Description: In-memory instantiation of Network.Transport Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Network Library Build-Depends: base >= 4.3 && < 5, - network-transport >= 0.2 && < 0.3, + network-transport >= 0.3 && < 0.4, data-accessor >= 0.2 && < 0.3, bytestring >= 0.9 && < 0.10, containers >= 0.4 && < 0.5 @@ -28,37 +28,17 @@ Library Test-Suite TestMulticastInMemory Type: exitcode-stdio-1.0 Build-Depends: base >= 4.3 && < 5, - network-transport >= 0.2 && < 0.3, - data-accessor >= 0.2 && < 0.3, - bytestring >= 0.9 && < 0.10, - containers >= 0.4 && < 0.5, - random >= 1.0 && < 1.1, - ansi-terminal >= 0.5 && < 0.6 + network-transport-inmemory >= 0.3 && < 0.4, + network-transport-tests >= 0.1 && < 0.2 Main-Is: TestMulticastInMemory.hs ghc-options: -Wall -fno-warn-unused-do-bind - Extensions: ExistentialQuantification, - FlexibleInstances, - DeriveDataTypeable, - RankNTypes, - OverloadedStrings - HS-Source-Dirs: tests src + HS-Source-Dirs: tests Test-Suite TestInMemory Type: exitcode-stdio-1.0 Build-Depends: base >= 4.3 && < 5, - network-transport >= 0.2 && < 0.3, - data-accessor >= 0.2 && < 0.3, - bytestring >= 0.9 && < 0.10, - containers >= 0.4 && < 0.5, - random >= 1.0 && < 1.1, - ansi-terminal >= 0.5 && < 0.6, - mtl >= 2.0 && < 2.2 + network-transport-inmemory >= 0.3 && < 0.4, + network-transport-tests >= 0.1 && < 0.2 Main-Is: TestInMemory.hs ghc-options: -Wall -fno-warn-unused-do-bind - Extensions: ExistentialQuantification, - FlexibleInstances, - DeriveDataTypeable, - RankNTypes, - OverloadedStrings, - OverlappingInstances - HS-Source-Dirs: tests src + HS-Source-Dirs: tests diff --git a/tests/TestAuxiliary.hs b/tests/TestAuxiliary.hs deleted file mode 100644 index d912ee6e..00000000 --- a/tests/TestAuxiliary.hs +++ /dev/null @@ -1,108 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} -module TestAuxiliary ( -- Running tests - runTest - , runTests - -- Writing tests - , forkTry - , trySome - , randomThreadDelay - ) where - -import Prelude hiding (catch) -import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo, threadDelay) -import Control.Concurrent.Chan (Chan) -import Control.Monad (liftM2, unless) -import Control.Exception (SomeException, try, catch) -import System.Timeout (timeout) -import System.IO (stdout, hFlush) -import System.Console.ANSI ( SGR(SetColor, Reset) - , Color(Red, Green) - , ConsoleLayer(Foreground) - , ColorIntensity(Vivid) - , setSGR - ) -import System.Random (randomIO) -import Network.Transport -import Traced (Traceable(..), traceShow) - --- | Like fork, but throw exceptions in the child thread to the parent -forkTry :: IO () -> IO ThreadId -forkTry p = do - tid <- myThreadId - forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) - --- | Like try, but specialized to SomeException -trySome :: IO a -> IO (Either SomeException a) -trySome = try - --- | Run the given test, catching timeouts and exceptions -runTest :: String -> IO () -> IO Bool -runTest description test = do - putStr $ "Running " ++ show description ++ ": " - hFlush stdout - done <- try . timeout 60000000 $ test -- 60 seconds - case done of - Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" - Right Nothing -> failed $ "(timeout)" - Right (Just ()) -> ok - where - failed :: String -> IO Bool - failed err = do - setSGR [SetColor Foreground Vivid Red] - putStr "failed " - setSGR [Reset] - putStrLn err - return False - - ok :: IO Bool - ok = do - setSGR [SetColor Foreground Vivid Green] - putStrLn "ok" - setSGR [Reset] - return True - --- | Run a bunch of tests and throw an exception if any fails -runTests :: [(String, IO ())] -> IO () -runTests tests = do - success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests - unless success $ fail "Some tests failed" - --- | Random thread delay between 0 and the specified max -randomThreadDelay :: Int -> IO () -randomThreadDelay maxDelay = do - delay <- randomIO :: IO Int - threadDelay (delay `mod` maxDelay) - --------------------------------------------------------------------------------- --- traceShow instances -- --------------------------------------------------------------------------------- - -instance Traceable EndPoint where - trace = const Nothing - -instance Traceable Transport where - trace = const Nothing - -instance Traceable Connection where - trace = const Nothing - -instance Traceable Event where - trace = traceShow - -instance Show err => Traceable (TransportError err) where - trace = traceShow - -instance Traceable EndPointAddress where - trace = traceShow . endPointAddressToByteString - -instance Traceable SomeException where - trace = traceShow - -instance Traceable ThreadId where - trace = const Nothing - -instance Traceable (Chan a) where - trace = const Nothing - -instance Traceable Float where - trace = traceShow diff --git a/tests/TestInMemory.hs b/tests/TestInMemory.hs index f7b6f70f..64c274d0 100644 --- a/tests/TestInMemory.hs +++ b/tests/TestInMemory.hs @@ -1,6 +1,6 @@ module Main where -import TestTransport +import Network.Transport.Tests import Network.Transport.Chan import Control.Applicative ((<$>)) diff --git a/tests/TestMulticast.hs b/tests/TestMulticast.hs deleted file mode 100644 index 43eb526d..00000000 --- a/tests/TestMulticast.hs +++ /dev/null @@ -1,72 +0,0 @@ -module TestMulticast where - -import Network.Transport -import TestAuxiliary (runTests) -import Control.Monad (replicateM, replicateM_, forM_, when) -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar, readMVar) -import Data.ByteString (ByteString) -import Data.List (elemIndex) - --- | Node for the "No confusion" test -noConfusionNode :: Transport -- ^ Transport - -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to - -> [MVar ()] -- ^ I'm ready : others ready - -> Int -- ^ number of pings - -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') - -> MVar () -- ^ I'm done - -> IO () -noConfusionNode transport groups ready numPings msgs done = do - -- Create a new endpoint - Right endpoint <- newEndPoint transport - - -- Create a new multicast group and broadcast its address - Right myGroup <- newMulticastGroup endpoint - putMVar (head groups) (multicastAddress myGroup) - - -- Subscribe to the given multicast groups - addrs <- mapM readMVar (tail groups) - forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr - multicastSubscribe group - - -- Indicate that we're ready and wait for everybody else to be ready - putMVar (head ready) () - mapM_ readMVar (tail ready) - - -- Send messages.. - forkIO . replicateM_ numPings $ multicastSend myGroup [head msgs] - - -- ..while checking that the messages we receive are the right ones - replicateM_ (2 * numPings) $ do - event <- receive endpoint - case event of - ReceivedMulticast addr [msg] -> - let mix = addr `elemIndex` addrs in - case mix of - Nothing -> error "Message from unexpected source" - Just ix -> when (msgs !! (ix + 1) /= msg) $ error "Unexpected message" - _ -> - error "Unexpected event" - - -- Success - putMVar done () - --- | Test that distinct multicast groups are not confused -testNoConfusion :: Transport -> Int -> IO () -testNoConfusion transport numPings = do - [group1, group2, group3] <- replicateM 3 newEmptyMVar - [readyA, readyB, readyC] <- replicateM 3 newEmptyMVar - [doneA, doneB, doneC] <- replicateM 3 newEmptyMVar - let [msgA, msgB, msgC] = ["A says hi", "B says hi", "C says hi"] - - forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA - forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB - forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC - - mapM_ takeMVar [doneA, doneB, doneC] - --- | Test multicast -testMulticast :: Transport -> IO () -testMulticast transport = - runTests - [ ("NoConfusion", testNoConfusion transport 10000) ] diff --git a/tests/TestMulticastInMemory.hs b/tests/TestMulticastInMemory.hs index 8494af64..097ec711 100644 --- a/tests/TestMulticastInMemory.hs +++ b/tests/TestMulticastInMemory.hs @@ -1,6 +1,6 @@ module Main where -import TestMulticast +import Network.Transport.Tests.Multicast import Network.Transport.Chan (createTransport) main :: IO () diff --git a/tests/TestTransport.hs b/tests/TestTransport.hs deleted file mode 100644 index e528e327..00000000 --- a/tests/TestTransport.hs +++ /dev/null @@ -1,956 +0,0 @@ -{-# LANGUAGE RebindableSyntax #-} -module TestTransport where - -import Prelude hiding (catch, (>>=), (>>), return, fail) -import TestAuxiliary (forkTry, runTests, trySome, randomThreadDelay) -import Control.Concurrent (forkIO, killThread, yield) -import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar, readMVar, tryTakeMVar, modifyMVar_, newMVar) -import Control.Exception (evaluate, throw, throwIO, bracket) -import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) -import Control.Monad.Error () -import Control.Applicative ((<$>)) -import Network.Transport -import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) -import Network.Transport.Util (spawn) -import System.Random (randomIO) -import Data.ByteString (ByteString) -import Data.ByteString.Char8 (pack) -import Data.Map (Map) -import qualified Data.Map as Map (empty, insert, delete, findWithDefault, adjust, null, toList, map) -import Data.String (fromString) -import Data.List (permutations) -import Traced - --- | Server that echoes messages straight back to the origin endpoint. -echoServer :: EndPoint -> IO () -echoServer endpoint = do - go Map.empty - where - go :: Map ConnectionId Connection -> IO () - go cs = do - event <- receive endpoint - case event of - ConnectionOpened cid rel addr -> do - tlog $ "Opened new connection " ++ show cid - Right conn <- connect endpoint addr rel defaultConnectHints - go (Map.insert cid conn cs) - Received cid payload -> do - send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload - go cs - ConnectionClosed cid -> do - tlog $ "Close connection " ++ show cid - close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) - go (Map.delete cid cs) - ReceivedMulticast _ _ -> - -- Ignore - go cs - ErrorEvent _ -> - putStrLn $ "Echo server received error event: " ++ show event - EndPointClosed -> - return () - --- | Ping client used in a few tests -ping :: EndPoint -> EndPointAddress -> Int -> ByteString -> IO () -ping endpoint server numPings msg = do - -- Open connection to the server - tlog "Connect to echo server" - Right conn <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Wait for the server to open reply connection - tlog "Wait for ConnectionOpened message" - ConnectionOpened cid _ _ <- receive endpoint - - -- Send pings and wait for reply - tlog "Send ping and wait for reply" - replicateM_ numPings $ do - send conn [msg] - Received cid' [reply] <- receive endpoint ; True <- return $ cid == cid' && reply == msg - return () - - -- Close the connection - tlog "Close the connection" - close conn - - -- Wait for the server to close its connection to us - tlog "Wait for ConnectionClosed message" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - - -- Done - tlog "Ping client done" - --- | Basic ping test -testPingPong :: Transport -> Int -> IO () -testPingPong transport numPings = do - tlog "Starting ping pong test" - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - tlog "Ping client" - Right endpoint <- newEndPoint transport - ping endpoint server numPings "ping" - putMVar result () - - takeMVar result - --- | Test that endpoints don't get confused -testEndPoints :: Transport -> Int -> IO () -testEndPoints transport numPings = do - server <- spawn transport echoServer - dones <- replicateM 2 newEmptyMVar - - forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do - let name' :: ByteString - name' = pack [name] - Right endpoint <- newEndPoint transport - tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) - ping endpoint server numPings name' - putMVar done () - - forM_ dones takeMVar - --- Test that connections don't get confused -testConnections :: Transport -> Int -> IO () -testConnections transport numPings = do - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - - -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv1 _ _ <- receive endpoint - - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv2 _ _ <- receive endpoint - - -- One thread to send "pingA" on the first connection - forkTry $ replicateM_ numPings $ send conn1 ["pingA"] - - -- One thread to send "pingB" on the second connection - forkTry $ replicateM_ numPings $ send conn2 ["pingB"] - - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do - event <- receive endpoint - case event of - Received cid [payload] -> do - when (cid == serv1 && payload /= "pingA") $ error "Wrong message" - when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n - verifyResponse (2 * numPings) - - takeMVar result - --- | Test that closing one connection does not close the other -testCloseOneConnection :: Transport -> Int -> IO () -testCloseOneConnection transport numPings = do - server <- spawn transport echoServer - result <- newEmptyMVar - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - - -- Open two connections to the server - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv1 _ _ <- receive endpoint - - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - ConnectionOpened serv2 _ _ <- receive endpoint - - -- One thread to send "pingA" on the first connection - forkTry $ do - replicateM_ numPings $ send conn1 ["pingA"] - close conn1 - - -- One thread to send "pingB" on the second connection - forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] - - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do - event <- receive endpoint - case event of - Received cid [payload] -> do - when (cid == serv1 && payload /= "pingA") $ error "Wrong message" - when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n - verifyResponse (3 * numPings) - - takeMVar result - --- | Test that if A connects to B and B connects to A, B can still send to A after --- A closes its connection to B (for instance, in the TCP transport, the socket pair --- connecting A and B should not yet be closed). -testCloseOneDirection :: Transport -> Int -> IO () -testCloseOneDirection transport numPings = do - addrA <- newEmptyMVar - addrB <- newEmptyMVar - doneA <- newEmptyMVar - doneB <- newEmptyMVar - - -- A - forkTry $ do - tlog "A" - Right endpoint <- newEndPoint transport - tlog (show (address endpoint)) - putMVar addrA (address endpoint) - - -- Connect to B - tlog "Connect to B" - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - -- Wait for B to connect to us - tlog "Wait for B" - ConnectionOpened cid _ _ <- receive endpoint - - -- Send pings to B - tlog "Send pings to B" - replicateM_ numPings $ send conn ["ping"] - - -- Close our connection to B - tlog "Close connection" - close conn - - -- Wait for B's pongs - tlog "Wait for pongs from B" - replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () - - -- Wait for B to close it's connection to us - tlog "Wait for B to close connection" - ConnectionClosed cid' <- receive endpoint - guard (cid == cid') - - -- Done - tlog "Done" - putMVar doneA () - - -- B - forkTry $ do - tlog "B" - Right endpoint <- newEndPoint transport - tlog (show (address endpoint)) - putMVar addrB (address endpoint) - - -- Wait for A to connect - tlog "Wait for A to connect" - ConnectionOpened cid _ _ <- receive endpoint - - -- Connect to A - tlog "Connect to A" - Right conn <- readMVar addrA >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - -- Wait for A's pings - tlog "Wait for pings from A" - replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () - - -- Wait for A to close it's connection to us - tlog "Wait for A to close connection" - ConnectionClosed cid' <- receive endpoint - guard (cid == cid') - - -- Send pongs to A - tlog "Send pongs to A" - replicateM_ numPings $ send conn ["pong"] - - -- Close our connection to A - tlog "Close connection to A" - close conn - - -- Done - tlog "Done" - putMVar doneB () - - mapM_ takeMVar [doneA, doneB] - --- | Collect events and order them by connection ID -collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString]])] -collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty - where - -- TODO: for more serious use of this function we'd need to make these arguments strict - go (Just 0) open closed = finish open closed - go n open closed = do - mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint - case mEvent of - Left _ -> finish open closed - Right event -> do - let n' = (\x -> x - 1) <$> n - case event of - ConnectionOpened cid _ _ -> - go n' (Map.insert cid [] open) closed - ConnectionClosed cid -> - let list = Map.findWithDefault (error "Invalid ConnectionClosed") cid open in - go n' (Map.delete cid open) (Map.insert cid list closed) - Received cid msg -> - go n' (Map.adjust (msg :) cid open) closed - ReceivedMulticast _ _ -> - fail "Unexpected multicast" - ErrorEvent _ -> - fail "Unexpected error" - EndPointClosed -> - fail "Unexpected endpoint closure" - - finish open closed = - if Map.null open - then return . Map.toList . Map.map reverse $ closed - else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) - --- | Open connection, close it, then reopen it --- (In the TCP transport this means the socket will be closed, then reopened) --- --- Note that B cannot expect to receive all of A's messages on the first connection --- before receiving the messages on the second connection. What might (and sometimes --- does) happen is that finishes sending all of its messages on the first connection --- (in the TCP transport, the first socket pair) while B is behind on reading _from_ --- this connection (socket pair) -- the messages are "in transit" on the network --- (these tests are done on localhost, so there are in some OS buffer). Then when --- A opens the second connection (socket pair) B will spawn a new thread for this --- connection, and hence might start interleaving messages from the first and second --- connection. --- --- This is correct behaviour, however: the transport API guarantees reliability and --- ordering _per connection_, but not _across_ connections. -testCloseReopen :: Transport -> Int -> IO () -testCloseReopen transport numPings = do - addrB <- newEmptyMVar - doneB <- newEmptyMVar - - let numRepeats = 2 :: Int - - -- A - forkTry $ do - Right endpoint <- newEndPoint transport - - forM_ [1 .. numRepeats] $ \i -> do - tlog "A connecting" - -- Connect to B - Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - tlog "A pinging" - -- Say hi - forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] - - tlog "A closing" - -- Disconnect again - close conn - - tlog "A finishing" - - -- B - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar addrB (address endpoint) - - eventss <- collect endpoint (Just (numRepeats * (numPings + 2))) Nothing - - forM_ (zip [1 .. numRepeats] eventss) $ \(i, (_, events)) -> do - forM_ (zip [1 .. numPings] events) $ \(j, event) -> do - guard (event == [pack $ "ping" ++ show i ++ "/" ++ show j]) - - putMVar doneB () - - takeMVar doneB - --- | Test lots of parallel connection attempts -testParallelConnects :: Transport -> Int -> IO () -testParallelConnects transport numPings = do - server <- spawn transport echoServer - done <- newEmptyMVar - - Right endpoint <- newEndPoint transport - - -- Spawn lots of clients - forM_ [1 .. numPings] $ \i -> forkTry $ do - Right conn <- connect endpoint server ReliableOrdered defaultConnectHints - send conn [pack $ "ping" ++ show i] - send conn [pack $ "ping" ++ show i] - close conn - - forkTry $ do - eventss <- collect endpoint (Just (numPings * 4)) Nothing - -- Check that no pings got sent to the wrong connection - forM_ eventss $ \(_, [[ping1], [ping2]]) -> - guard (ping1 == ping2) - putMVar done () - - takeMVar done - --- | Test that sending on a closed connection gives an error -testSendAfterClose :: Transport -> Int -> IO () -testSendAfterClose transport numRepeats = do - server <- spawn transport echoServer - clientDone <- newEmptyMVar - - forkTry $ do - Right endpoint <- newEndPoint transport - - -- We request two lightweight connections - replicateM numRepeats $ do - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Close the second, but leave the first open; then output on the second - -- connection (i.e., on a closed connection while there is still another - -- connection open) - close conn2 - Left (TransportError SendClosed _) <- send conn2 ["ping2"] - - -- Now close the first connection, and output on it (i.e., output while - -- there are no lightweight connection at all anymore) - close conn1 - Left (TransportError SendClosed _) <- send conn2 ["ping2"] - - return () - - putMVar clientDone () - - takeMVar clientDone - --- | Test that closing the same connection twice has no effect -testCloseTwice :: Transport -> Int -> IO () -testCloseTwice transport numRepeats = do - server <- spawn transport echoServer - clientDone <- newEmptyMVar - - forkTry $ do - Right endpoint <- newEndPoint transport - - replicateM numRepeats $ do - -- We request two lightweight connections - Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - - -- Close the second one twice - close conn2 - close conn2 - - -- Then send a message on the first and close that twice too - send conn1 ["ping"] - close conn1 - - -- Verify expected response from the echo server - ConnectionOpened cid1 _ _ <- receive endpoint - ConnectionOpened cid2 _ _ <- receive endpoint - ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 - Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 - ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 - - return () - - putMVar clientDone () - - takeMVar clientDone - --- | Test that we can connect an endpoint to itself -testConnectToSelf :: Transport -> Int -> IO () -testConnectToSelf transport numPings = do - done <- newEmptyMVar - Right endpoint <- newEndPoint transport - - tlog "Creating self-connection" - Right conn <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - - tlog "Talk to myself" - - -- One thread to write to the endpoint - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn ["ping"] - - tlog $ "Closing connection" - close conn - - -- And one thread to read - forkTry $ do - tlog $ "reading" - - tlog "Waiting for ConnectionOpened" - ConnectionOpened cid _ addr <- receive endpoint ; True <- return $ addr == address endpoint - - tlog "Waiting for Received" - replicateM_ numPings $ do - Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - return () - - tlog "Waiting for ConnectionClosed" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - - tlog "Done" - putMVar done () - - takeMVar done - --- | Test that we can connect an endpoint to itself multiple times -testConnectToSelfTwice :: Transport -> Int -> IO () -testConnectToSelfTwice transport numPings = do - done <- newEmptyMVar - Right endpoint <- newEndPoint transport - - tlog "Creating self-connection" - Right conn1 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint (address endpoint) ReliableOrdered defaultConnectHints - - tlog "Talk to myself" - - -- One thread to write to the endpoint using the first connection - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn1 ["pingA"] - - tlog $ "Closing connection" - close conn1 - - -- One thread to write to the endpoint using the second connection - forkTry $ do - tlog $ "writing" - - tlog $ "Sending ping" - replicateM_ numPings $ send conn2 ["pingB"] - - tlog $ "Closing connection" - close conn2 - - -- And one thread to read - forkTry $ do - tlog $ "reading" - - [(_, events1), (_, events2)] <- collect endpoint (Just (2 * (numPings + 2))) Nothing - True <- return $ events1 == replicate numPings ["pingA"] - True <- return $ events2 == replicate numPings ["pingB"] - - tlog "Done" - putMVar done () - - takeMVar done - --- | Test that we self-connections no longer work once we close our endpoint --- or our transport -testCloseSelf :: IO (Either String Transport) -> IO () -testCloseSelf newTransport = do - Right transport <- newTransport - Right endpoint1 <- newEndPoint transport - Right endpoint2 <- newEndPoint transport - Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - - -- Close the conneciton and try to send - close conn1 - Left (TransportError SendClosed _) <- send conn1 ["ping"] - - -- Close the first endpoint. We should not be able to use the first - -- connection anymore, or open more self connections, but the self connection - -- to the second endpoint should still be fine - closeEndPoint endpoint1 - Left (TransportError SendFailed _) <- send conn2 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints - Right () <- send conn3 ["ping"] - - -- Close the transport; now the second should no longer work - closeTransport transport - Left (TransportError SendFailed _) <- send conn3 ["ping"] - Left (TransportError ConnectFailed _) <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - - return () - --- | Test various aspects of 'closeEndPoint' -testCloseEndPoint :: Transport -> Int -> IO () -testCloseEndPoint transport _ = do - serverDone <- newEmptyMVar - clientDone <- newEmptyMVar - clientAddr1 <- newEmptyMVar - clientAddr2 <- newEmptyMVar - serverAddr <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- First test (see client) - do - theirAddr <- readMVar clientAddr1 - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' - return () - - -- Second test - do - theirAddr <- readMVar clientAddr2 - - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' - - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn ["pong"] - - ConnectionClosed cid'' <- receive endpoint ; True <- return $ cid == cid'' - ErrorEvent (TransportError (EventConnectionLost (Just addr') []) _) <- receive endpoint ; True <- return $ addr' == theirAddr - - Left (TransportError SendFailed _) <- send conn ["pong2"] - - return () - - putMVar serverDone () - - -- Client - forkTry $ do - theirAddr <- readMVar serverAddr - - -- First test: close endpoint with one outgoing but no incoming connections - do - Right endpoint <- newEndPoint transport - putMVar clientAddr1 (address endpoint) - - -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - closeEndPoint endpoint - EndPointClosed <- receive endpoint - return () - - -- Second test: close endpoint with one outgoing and one incoming connection - do - Right endpoint <- newEndPoint transport - putMVar clientAddr2 (address endpoint) - - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - send conn ["ping"] - - -- Reply from the server - ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr - Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' - - -- Close the endpoint - closeEndPoint endpoint - EndPointClosed <- receive endpoint - - -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- An attempt to close the already closed connection should just return - () <- close conn - - -- And so should an attempt to connect - Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - - return () - - putMVar clientDone () - - mapM_ takeMVar [serverDone, clientDone] - --- Test closeTransport --- --- This tests many of the same things that testEndPoint does, and some more -testCloseTransport :: IO (Either String Transport) -> IO () -testCloseTransport newTransport = do - serverDone <- newEmptyMVar - clientDone <- newEmptyMVar - clientAddr1 <- newEmptyMVar - clientAddr2 <- newEmptyMVar - serverAddr <- newEmptyMVar - - -- Server - forkTry $ do - Right transport <- newTransport - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - -- Client sets up first endpoint - theirAddr1 <- readMVar clientAddr1 - ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 - - -- Client sets up second endpoint - theirAddr2 <- readMVar clientAddr2 - - ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 - Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 - - Right conn <- connect endpoint theirAddr2 ReliableOrdered defaultConnectHints - send conn ["pong"] - - -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) - evs <- replicateM 3 $ receive endpoint - let expected = [ ConnectionClosed cid1 - , ConnectionClosed cid2 - , ErrorEvent (TransportError (EventConnectionLost (Just theirAddr2) []) "") - ] - True <- return $ any (== expected) (permutations evs) - - -- An attempt to send to the endpoint should now fail - Left (TransportError SendFailed _) <- send conn ["pong2"] - - putMVar serverDone () - - -- Client - forkTry $ do - Right transport <- newTransport - theirAddr <- readMVar serverAddr - - -- Set up endpoint with one outgoing but no incoming connections - Right endpoint1 <- newEndPoint transport - putMVar clientAddr1 (address endpoint1) - - -- Connect to the server, then close the endpoint without disconnecting explicitly - Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints - - -- Set up an endpoint with one outgoing and out incoming connection - Right endpoint2 <- newEndPoint transport - putMVar clientAddr2 (address endpoint2) - - Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints - send conn ["ping"] - - -- Reply from the server - ConnectionOpened cid ReliableOrdered addr <- receive endpoint2 ; True <- return $ addr == theirAddr - Received cid' ["pong"] <- receive endpoint2 ; True <- return $ cid == cid' - - -- Now shut down the entire transport - closeTransport transport - - -- Both endpoints should report that they have been closed - EndPointClosed <- receive endpoint1 - EndPointClosed <- receive endpoint2 - - -- Attempt to send should fail with connection closed - Left (TransportError SendFailed _) <- send conn ["ping2"] - - -- An attempt to close the already closed connection should just return - () <- close conn - - -- And so should an attempt to connect on either endpoint - Left (TransportError ConnectFailed _) <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints - Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints - - -- And finally, so should an attempt to create a new endpoint - Left (TransportError NewEndPointFailed _) <- newEndPoint transport - - putMVar clientDone () - - mapM_ takeMVar [serverDone, clientDone] - --- | Remote node attempts to connect to a closed local endpoint -testConnectClosedEndPoint :: Transport -> IO () -testConnectClosedEndPoint transport = do - serverAddr <- newEmptyMVar - serverClosed <- newEmptyMVar - clientDone <- newEmptyMVar - - -- Server - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) - - closeEndPoint endpoint - putMVar serverClosed () - - -- Client - forkTry $ do - Right endpoint <- newEndPoint transport - readMVar serverClosed - - Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - putMVar clientDone () - - takeMVar clientDone - --- | We should receive an exception when doing a 'receive' after we have been --- notified that an endpoint has been closed -testExceptionOnReceive :: IO (Either String Transport) -> IO () -testExceptionOnReceive newTransport = do - Right transport <- newTransport - - -- Test one: when we close an endpoint specifically - Right endpoint1 <- newEndPoint transport - closeEndPoint endpoint1 - EndPointClosed <- receive endpoint1 - Left _ <- trySome (receive endpoint1 >>= evaluate) - - -- Test two: when we close the entire transport - Right endpoint2 <- newEndPoint transport - closeTransport transport - EndPointClosed <- receive endpoint2 - Left _ <- trySome (receive endpoint2 >>= evaluate) - - return () - --- | Test what happens when the argument to 'send' is an exceptional value -testSendException :: IO (Either String Transport) -> IO () -testSendException newTransport = do - Right transport <- newTransport - Right endpoint1 <- newEndPoint transport - Right endpoint2 <- newEndPoint transport - - -- Connect endpoint1 to endpoint2 - Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - ConnectionOpened _ _ _ <- receive endpoint2 - - -- Send an exceptional value - Left (TransportError SendFailed _) <- send conn (throw $ userError "uhoh") - - -- This will have been as a failure to send by endpoint1, which will - -- therefore have closed the socket. In turn this will have caused endpoint2 - -- to report that the connection was lost - ErrorEvent (TransportError (EventConnectionLost _ []) _) <- receive endpoint1 - ErrorEvent (TransportError (EventConnectionLost _ [_]) _) <- receive endpoint2 - - -- A new connection will re-establish the connection - Right conn2 <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints - send conn2 ["ping"] - close conn2 - - ConnectionOpened _ _ _ <- receive endpoint2 - Received _ ["ping"] <- receive endpoint2 - ConnectionClosed _ <- receive endpoint2 - - return () - --- | If threads get killed while executing a 'connect', 'send', or 'close', this --- should not affect other threads. --- --- The intention of this test is to see what happens when a asynchronous --- exception happes _while executing a send_. This is exceedingly difficult to --- guarantee, however. Hence we run a large number of tests and insert random --- thread delays -- and even then it might not happen. Moreover, it will only --- happen when we run on multiple cores. -testKill :: IO (Either String Transport) -> Int -> IO () -testKill newTransport numThreads = do - Right transport1 <- newTransport - Right transport2 <- newTransport - Right endpoint1 <- newEndPoint transport1 - Right endpoint2 <- newEndPoint transport2 - - threads <- replicateM numThreads . forkIO $ do - randomThreadDelay 100 - bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) - -- Note that we should not insert a randomThreadDelay into the - -- exception handler itself as this means that the exception handler - -- could be interrupted and we might not close - (\(Right conn) -> close conn) - (\(Right conn) -> do randomThreadDelay 100 - Right () <- send conn ["ping"] - randomThreadDelay 100) - - numAlive <- newMVar (0 :: Int) - - -- Kill half of those threads - forkIO . forM_ threads $ \tid -> do - shouldKill <- randomIO - if shouldKill - then randomThreadDelay 600 >> killThread tid - else modifyMVar_ numAlive (return . (+ 1)) - - -- Since it is impossible to predict when the kill exactly happens, we don't - -- know how many connects were opened and how many pings were sent. But we - -- should not have any open connections (if we do, collect will throw an - -- error) and we should have at least the number of pings equal to the number - -- of threads we did *not* kill - eventss <- collect endpoint2 Nothing (Just 1000000) - let actualPings = sum . map (length . snd) $ eventss - expectedPings <- takeMVar numAlive - unless (actualPings >= expectedPings) $ - throwIO (userError "Missing pings") - --- print (actualPings, expectedPings) - - --- | Set up conditions with a high likelyhood of "crossing" (for transports --- that multiplex lightweight connections across heavyweight connections) -testCrossing :: Transport -> Int -> IO () -testCrossing transport numRepeats = do - [aAddr, bAddr] <- replicateM 2 newEmptyMVar - [aDone, bDone] <- replicateM 2 newEmptyMVar - [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar - go <- newEmptyMVar - - let hints = defaultConnectHints { - connectTimeout = Just 5000000 - } - - -- A - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar aAddr (address endpoint) - theirAddress <- readMVar bAddr - - replicateM_ numRepeats $ do - takeMVar go >> yield - -- Because we are creating lots of connections, it's possible that - -- connect times out (for instance, in the TCP transport, - -- Network.Socket.connect may time out). We shouldn't regard this as an - -- error in the Transport, though. - connectResult <- connect endpoint theirAddress ReliableOrdered hints - case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar aTimeout () - Left (TransportError ConnectFailed _) -> readMVar bTimeout - Left err -> throwIO . userError $ "testCrossed: " ++ show err - putMVar aDone () - - -- B - forkTry $ do - Right endpoint <- newEndPoint transport - putMVar bAddr (address endpoint) - theirAddress <- readMVar aAddr - - replicateM_ numRepeats $ do - takeMVar go >> yield - connectResult <- connect endpoint theirAddress ReliableOrdered hints - case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar bTimeout () - Left (TransportError ConnectFailed _) -> readMVar aTimeout - Left err -> throwIO . userError $ "testCrossed: " ++ show err - putMVar bDone () - - -- Driver - forM_ [1 .. numRepeats] $ \_i -> do - -- putStrLn $ "Round " ++ show _i - tryTakeMVar aTimeout - tryTakeMVar bTimeout - putMVar go () - putMVar go () - takeMVar aDone - takeMVar bDone - --- Transport tests -testTransport :: IO (Either String Transport) -> IO () -testTransport newTransport = do - Right transport <- newTransport - runTests - [ ("PingPong", testPingPong transport numPings) - , ("EndPoints", testEndPoints transport numPings) - , ("Connections", testConnections transport numPings) - , ("CloseOneConnection", testCloseOneConnection transport numPings) - , ("CloseOneDirection", testCloseOneDirection transport numPings) - , ("CloseReopen", testCloseReopen transport numPings) - , ("ParallelConnects", testParallelConnects transport numPings) - , ("SendAfterClose", testSendAfterClose transport 1000) - , ("Crossing", testCrossing transport 100) - , ("CloseTwice", testCloseTwice transport 100) - , ("ConnectToSelf", testConnectToSelf transport numPings) - , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) - , ("CloseSelf", testCloseSelf newTransport) - , ("CloseEndPoint", testCloseEndPoint transport numPings) - , ("CloseTransport", testCloseTransport newTransport) - , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) - , ("ExceptionOnReceive", testExceptionOnReceive newTransport) - , ("SendException", testSendException newTransport) - , ("Kill", testKill newTransport 10000) - ] - where - numPings = 10000 :: Int diff --git a/tests/Traced.hs b/tests/Traced.hs deleted file mode 100644 index a7735efa..00000000 --- a/tests/Traced.hs +++ /dev/null @@ -1,191 +0,0 @@ --- | Add tracing to the IO monad (see examples). --- --- [Usage] --- --- > {-# LANGUAGE RebindableSyntax #-} --- > import Prelude hiding (catch, (>>=), (>>), return, fail) --- > import Traced --- --- [Example] --- --- > test1 :: IO Int --- > test1 = do --- > Left x <- return (Left 1 :: Either Int Int) --- > putStrLn "Hello world" --- > Right y <- return (Left 2 :: Either Int Int) --- > return (x + y) --- --- outputs --- --- > Hello world --- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) --- > Trace: --- > 0 Left 2 --- > 1 Left 1 --- --- [Guards] --- --- Use the following idiom instead of using 'Control.Monad.guard': --- --- > test2 :: IO Int --- > test2 = do --- > Left x <- return (Left 1 :: Either Int Int) --- > True <- return (x == 3) --- > return x --- --- The advantage of this idiom is that it gives you line number information when the guard fails: --- --- > *Traced> test2 --- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) --- > Trace: --- > 0 Left 1 -module Traced ( MonadS(..) - , return - , (>>=) - , (>>) - , fail - , ifThenElse - , Showable(..) - , Traceable(..) - , traceShow - ) where - -import Prelude hiding ((>>=), return, fail, catch, (>>)) -import qualified Prelude -import Control.Exception (catches, Handler(..), SomeException, throwIO, Exception(..), IOException) -import Control.Applicative ((<$>)) -import Data.Typeable (Typeable) -import Data.Maybe (catMaybes) -import Data.ByteString (ByteString) -import Data.Int (Int32) -import Control.Concurrent.MVar (MVar) - --------------------------------------------------------------------------------- --- MonadS class -- --------------------------------------------------------------------------------- - --- | Like 'Monad' but bind is only defined for 'Trace'able instances -class MonadS m where - returnS :: a -> m a - bindS :: Traceable a => m a -> (a -> m b) -> m b - failS :: String -> m a - seqS :: m a -> m b -> m b - --- | Redefinition of 'Prelude.>>=' -(>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b -(>>=) = bindS - --- | Redefinition of 'Prelude.>>' -(>>) :: MonadS m => m a -> m b -> m b -(>>) = seqS - --- | Redefinition of 'Prelude.return' -return :: MonadS m => a -> m a -return = returnS - --- | Redefinition of 'Prelude.fail' -fail :: MonadS m => String -> m a -fail = failS - --------------------------------------------------------------------------------- --- Trace typeclass (for adding elements to a trace -- --------------------------------------------------------------------------------- - -data Showable = forall a. Show a => Showable a - -instance Show Showable where - show (Showable x) = show x - -mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable -mapShowable f (Showable x) = f x - -traceShow :: Show a => a -> Maybe Showable -traceShow = Just . Showable - -class Traceable a where - trace :: a -> Maybe Showable - -instance (Traceable a, Traceable b) => Traceable (Either a b) where - trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x - trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y - -instance (Traceable a, Traceable b) => Traceable (a, b) where - trace (x, y) = case (trace x, trace y) of - (Nothing, Nothing) -> Nothing - (Just t1, Nothing) -> traceShow t1 - (Nothing, Just t2) -> traceShow t2 - (Just t1, Just t2) -> traceShow (t1, t2) - -instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where - trace (x, y, z) = case (trace x, trace y, trace z) of - (Nothing, Nothing, Nothing) -> Nothing - (Just t1, Nothing, Nothing) -> traceShow t1 - (Nothing, Just t2, Nothing) -> traceShow t2 - (Just t1, Just t2, Nothing) -> traceShow (t1, t2) - (Nothing, Nothing, Just t3) -> traceShow t3 - (Just t1, Nothing, Just t3) -> traceShow (t1, t3) - (Nothing, Just t2, Just t3) -> traceShow (t2, t3) - (Just t1, Just t2, Just t3) -> traceShow (t1, t2, t3) - -instance Traceable a => Traceable (Maybe a) where - trace Nothing = traceShow (Nothing :: Maybe ()) - trace (Just x) = mapShowable (Showable . Just) <$> trace x - -instance Traceable a => Traceable [a] where - trace = traceShow . catMaybes . map trace - -instance Traceable () where - trace = const Nothing - -instance Traceable Int where - trace = traceShow - -instance Traceable Int32 where - trace = traceShow - -instance Traceable Bool where - trace = const Nothing - -instance Traceable ByteString where - trace = traceShow - -instance Traceable (MVar a) where - trace = const Nothing - -instance Traceable [Char] where - trace = traceShow - -instance Traceable IOException where - trace = traceShow - --------------------------------------------------------------------------------- --- IO instance for MonadS -- --------------------------------------------------------------------------------- - -data TracedException = TracedException [String] SomeException - deriving Typeable - -instance Exception TracedException - --- | Add tracing to 'IO' (see examples) -instance MonadS IO where - returnS = Prelude.return - bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) - failS = Prelude.fail - seqS = (Prelude.>>) - -instance Show TracedException where - show (TracedException ts ex) = - show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) - -traceHandlers :: Traceable a => a -> [Handler b] -traceHandlers a = case trace a of - Nothing -> [ Handler $ \ex -> throwIO (ex :: SomeException) ] - Just t -> [ Handler $ \(TracedException ts ex) -> throwIO $ TracedException (show t : ts) ex - , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) - ] - --- | Definition of 'ifThenElse' for use with RebindableSyntax -ifThenElse :: Bool -> a -> a -> a -ifThenElse True x _ = x -ifThenElse False _ y = y From 0db7137d368310a995d549e3a9f80bcadce72b3e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 4 Sep 2012 10:19:14 +0100 Subject: [PATCH 0206/2357] Fix potential deadlock in testCrossing --- src/Network/Transport/Tests.hs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Network/Transport/Tests.hs b/src/Network/Transport/Tests.hs index 45773c42..1a6141dd 100644 --- a/src/Network/Transport/Tests.hs +++ b/src/Network/Transport/Tests.hs @@ -882,8 +882,8 @@ testCrossing :: Transport -> Int -> IO () testCrossing transport numRepeats = do [aAddr, bAddr] <- replicateM 2 newEmptyMVar [aDone, bDone] <- replicateM 2 newEmptyMVar + [aGo, bGo] <- replicateM 2 newEmptyMVar [aTimeout, bTimeout] <- replicateM 2 newEmptyMVar - go <- newEmptyMVar let hints = defaultConnectHints { connectTimeout = Just 5000000 @@ -896,7 +896,7 @@ testCrossing transport numRepeats = do theirAddress <- readMVar bAddr replicateM_ numRepeats $ do - takeMVar go >> yield + takeMVar aGo >> yield -- Because we are creating lots of connections, it's possible that -- connect times out (for instance, in the TCP transport, -- Network.Socket.connect may time out). We shouldn't regard this as an @@ -914,9 +914,9 @@ testCrossing transport numRepeats = do Right endpoint <- newEndPoint transport putMVar bAddr (address endpoint) theirAddress <- readMVar aAddr - + replicateM_ numRepeats $ do - takeMVar go >> yield + takeMVar bGo >> yield connectResult <- connect endpoint theirAddress ReliableOrdered hints case connectResult of Right conn -> close conn @@ -930,8 +930,10 @@ testCrossing transport numRepeats = do -- putStrLn $ "Round " ++ show _i tryTakeMVar aTimeout tryTakeMVar bTimeout - putMVar go () - putMVar go () + b <- randomIO + if b then do putMVar aGo () ; putMVar bGo () + else do putMVar bGo () ; putMVar aGo () + yield takeMVar aDone takeMVar bDone From 5e18ece1fda9840afe3dd8b903eb8d97841cccdd Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 5 Sep 2012 12:13:12 +0100 Subject: [PATCH 0207/2357] Various improvements to static/closure/TH support --- demos/Conway.hs | 51 ------- demos/Fib.hs | 140 ++++++++++++++++++ distributed-process-azure.cabal | 25 ++-- .../Distributed/Process/Backend/Azure.hs | 64 +++++++- 4 files changed, 217 insertions(+), 63 deletions(-) delete mode 100644 demos/Conway.hs create mode 100644 demos/Fib.hs diff --git a/demos/Conway.hs b/demos/Conway.hs deleted file mode 100644 index f885b938..00000000 --- a/demos/Conway.hs +++ /dev/null @@ -1,51 +0,0 @@ -{-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-} -import System.Environment (getArgs) -import Data.Data (Typeable, Data) -import Data.Binary (Binary(get, put)) -import Data.Binary.Generic (getGeneric, putGeneric) -import Control.Monad (forever) -import Control.Monad.IO.Class (liftIO) -import Control.Distributed.Process - ( Process - , expect - ) -import Control.Distributed.Process.Closure - ( remotable - , mkClosure - ) -import Control.Distributed.Process.Backend.Azure - -data ControllerMsg = - ControllerExit - deriving (Typeable, Data) - -instance Binary ControllerMsg where - get = getGeneric - put = putGeneric - -conwayStart :: () -> Backend -> Process () -conwayStart () backend = do - vms <- liftIO $ findVMs backend - remoteSend (show vms) - -remotable ['conwayStart] - -echo :: LocalProcess () -echo = forever $ do - msg <- localExpect - liftIO $ putStrLn msg - -main :: IO () -main = do - args <- getArgs - case args of - "onvm":args' -> onVmMain __remoteTable args' - cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do - params <- defaultAzureParameters sid x509 pkey - let params' = params { azureSshUserName = user } - backend <- initializeBackend params' cloudService - Just vm <- findNamedVM backend virtualMachine - case cmd of - "start" -> callOnVM backend vm port $ - ProcessPair ($(mkClosure 'conwayStart) ()) - echo diff --git a/demos/Fib.hs b/demos/Fib.hs new file mode 100644 index 00000000..92adb21c --- /dev/null +++ b/demos/Fib.hs @@ -0,0 +1,140 @@ +{-# LANGUAGE TemplateHaskell #-} +import System.Environment (getArgs) +import System.Random (randomIO) +import Data.Binary (encode) +import Data.ByteString.Lazy (ByteString) +import Control.Monad (forM) +import Control.Monad.IO.Class (liftIO) +import Control.Distributed.Process + ( Process + , NodeId + , SendPort + , newChan + , sendChan + , Closure + , spawn + , receiveChan + , spawnLocal + ) +import Control.Distributed.Process.Closure + ( SerializableDict(SerializableDict) + , staticDecode + ) +import Control.Distributed.Process.Backend.Azure +import Control.Distributed.Static + ( Static + , staticLabel + , closure + , RemoteTable + , registerStatic + , staticCompose + ) +import Data.Rank1Dynamic (toDynamic) +import Control.Distributed.Process.Closure (remotable, mkClosure) +import qualified Language.Haskell.TH as TH + +randomElement :: [a] -> IO a +randomElement xs = do + ix <- randomIO + return (xs !! (ix `mod` length xs)) + +f :: [TH.Name] -> TH.Q [TH.Dec] +f = remotable + +remotableDec :: [TH.Dec] -> TH.Q [TH.Dec] +remotableDec = undefined + +remotableDec [ [d| f :: Int |] ] + +main :: IO () +main = undefined + +-------------------------------------------------------------------------------- +-- Auxiliary -- +-------------------------------------------------------------------------------- + + + +{- + +-------------------------------------------------------------------------------- +-- Distributed Fibonacci -- +-------------------------------------------------------------------------------- + +dfib :: ([NodeId], Integer, SendPort Integer) -> Process () +dfib (_, 0, reply) = sendChan reply 0 +dfib (_, 1, reply) = sendChan reply 1 +dfib (nids, n, reply) = do + nid1 <- liftIO $ randomElement nids + nid2 <- liftIO $ randomElement nids + (sport, rport) <- newChan + spawn nid1 $ dfibClosure nids (n - 2) sport + spawn nid2 $ dfibClosure nids (n - 1) sport + n1 <- receiveChan rport + n2 <- receiveChan rport + sendChan reply $ n1 + n2 + +remoteFib :: ([NodeId], Integer) -> Backend -> Process () +remoteFib (nids, n) _backend = do + (sport, rport) <- newChan + spawnLocal $ dfib (nids, n, sport) + fib_n <- receiveChan rport + remoteSend fib_n + +printResult :: LocalProcess () +printResult = do + result <- localExpect :: LocalProcess Integer + liftIO $ print result + +main :: IO () +main = do + args <- getArgs + case args of + "onvm":args' -> onVmMain __remoteTable args' + [sid, x509, pkey, user, cloudService, n] -> do + params <- defaultAzureParameters sid x509 pkey + let params' = params { azureSshUserName = user } + backend <- initializeBackend params' cloudService + vms <- findVMs backend + nids <- forM vms $ \vm -> spawnNodeOnVM backend vm "8080" + callOnVM backend (head vms) "8081" $ + ProcessPair (remoteFibClosure nids (read n)) printResult + _ -> + error "Invalid command line arguments" + +-------------------------------------------------------------------------------- +-- Static plumping -- +-------------------------------------------------------------------------------- + +dfibStatic :: Static (([NodeId], Integer, SendPort Integer) -> Process ()) +dfibStatic = staticLabel "dfib" + +dfibDict :: Static (SerializableDict ([NodeId], Integer, SendPort Integer)) +dfibDict = staticLabel "dfibDict" + +dfibClosure :: [NodeId] -> Integer -> SendPort Integer -> Closure (Process ()) +dfibClosure nids n reply = closure decoder (encode (nids, n, reply)) + where + decoder :: Static (ByteString -> Process ()) + decoder = dfibStatic `staticCompose` staticDecode dfibDict + +remoteFibStatic :: Static (([NodeId], Integer) -> Backend -> Process ()) +remoteFibStatic = staticLabel "remoteFib" + +remoteFibDict :: Static (SerializableDict ([NodeId], Integer)) +remoteFibDict = staticLabel "remoteFibDict" + +remoteFibClosure :: [NodeId] -> Integer -> RemoteProcess () +remoteFibClosure nids n = closure decoder (encode (nids, n)) + where + decoder :: Static (ByteString -> Backend -> Process ()) + decoder = remoteFibStatic `staticCompose` staticDecode remoteFibDict + +__remoteTable :: RemoteTable -> RemoteTable +__remoteTable = + registerStatic "dfib" (toDynamic dfib) + . registerStatic "dfibDict" (toDynamic (SerializableDict :: SerializableDict ([NodeId], Integer, SendPort Integer))) + . registerStatic "remoteFib" (toDynamic remoteFib) + . registerStatic "remoteFibDict" (toDynamic (SerializableDict :: SerializableDict ([NodeId], Integer))) + +-} diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index e87d7bf8..d6d5ee04 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -30,15 +30,17 @@ Library libssh2 >= 0.2 && < 0.3, pureMD5 >= 2.1 && < 2.2, bytestring >= 0.9 && < 0.11, - distributed-process >= 0.2.3 && < 0.3, + distributed-process >= 0.3.2 && < 0.4, binary >= 0.5 && < 0.6, - network-transport-tcp >= 0.2 && < 0.3, + network-transport-tcp >= 0.3 && < 0.4, optparse-applicative >= 0.2 && < 0.4, transformers >= 0.3 && < 0.4, certificate >= 1.2.4 && < 1.3, unix >= 2.5 && < 2.6, - network-transport >= 0.2 && < 0.3, - mtl >= 2.1 && < 2.2 + network-transport >= 0.3 && < 0.4, + mtl >= 2.1 && < 2.2, + rank1dynamic >= 0.1 && < 0.2, + distributed-static >= 0.2 && < 0.3 Exposed-modules: Control.Distributed.Process.Backend.Azure Extensions: ViewPatterns, RankNTypes, @@ -53,7 +55,7 @@ Executable cloud-haskell-azure-echo if flag(build-demos) Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, - distributed-process >= 0.2 && < 0.3, + distributed-process >= 0.3.2 && < 0.4, transformers >= 0.3 && < 0.4 else buildable: False @@ -64,7 +66,7 @@ Executable cloud-haskell-azure-ping if flag(build-demos) Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, - distributed-process >= 0.2 && < 0.3, + distributed-process >= 0.3.2 && < 0.4, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6, @@ -75,16 +77,19 @@ Executable cloud-haskell-azure-ping Main-Is: demos/Ping.hs ghc-options: -Wall -Executable cloud-haskell-azure-conway +Executable cloud-haskell-azure-fib if flag(build-demos) Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, - distributed-process >= 0.2 && < 0.3, + distributed-process >= 0.3.2 && < 0.4, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6, - binary-generic >= 0.2 && < 0.3 + binary-generic >= 0.2 && < 0.3, + rank1dynamic >= 0.1 && < 0.2, + distributed-static >= 0.2 && < 0.3, + random >= 1.0 && < 1.1 else buildable: False - Main-Is: demos/Conway.hs + Main-Is: demos/Fib.hs ghc-options: -Wall diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 5aa9f53d..c087fd1d 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -413,6 +413,9 @@ module Control.Distributed.Process.Backend.Azure , localSend , localExpect , remoteSend + -- * High-level API + , spawnNodeOnVM + , terminateNode ) where import Prelude hiding (catch) @@ -431,7 +434,7 @@ import System.IO ) import qualified System.Posix.Process as Posix (forkProcess, createSession) import Data.Maybe (listToMaybe) -import Data.Binary (Binary(get, put), encode, decode) +import Data.Binary (Binary(get, put), encode, decode, getWord8, putWord8) import Data.Digest.Pure.MD5 (md5, MD5Digest) import qualified Data.ByteString as BSS ( ByteString @@ -510,6 +513,11 @@ import Control.Distributed.Process , unClosure , ProcessId , getSelfPid + , NodeId + , processNodeId + , register + , expect + , nsendRemote ) import Control.Distributed.Process.Serializable (Serializable) import qualified Control.Distributed.Process.Internal.Types as CH @@ -530,6 +538,15 @@ import Control.Distributed.Process.Internal.CQueue (CQueue, enqueue) import Network.Transport.TCP (createTransport, defaultTCPParameters) import Network.Transport.Internal (encodeInt32, decodeInt32, prependLength) +-- Static +import Control.Distributed.Static + ( Static + , registerStatic + , staticClosure + , staticLabel + ) +import Data.Rank1Dynamic (toDynamic) + -- | Azure backend data Backend = Backend { -- | Find virtual machines @@ -855,7 +872,7 @@ onVmMain rtable [host, port, cloudService, bg] = do case mTransport of Left err -> remoteThrow err Right transport -> do - node <- newLocalNode transport (rtable initRemoteTable) + node <- newLocalNode transport (rtable . __remoteTable $ initRemoteTable) void . go node $ do ask >>= liftIO . putMVar lprocMVar proc <- unClosure rproc :: Process (Backend -> Process ()) @@ -902,3 +919,46 @@ readSizeChannel ch = go [] readIntChannel :: SSH.Channel -> IO Int readIntChannel ch = decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4 + +-------------------------------------------------------------------------------- +-- High-level API -- +-------------------------------------------------------------------------------- + +data ServiceProcessMsg = + ServiceProcessTerminate + deriving Typeable + +instance Binary ServiceProcessMsg where + put ServiceProcessTerminate = putWord8 0 + get = do + header <- getWord8 + case header of + 0 -> return ServiceProcessTerminate + _ -> fail "ServiceProcessMsg.get" + +serviceProcess :: Backend -> Process () +serviceProcess _backend = do + us <- getSelfPid + register "serviceProcess" us + go + where + go = do + msg <- expect + case msg of + ServiceProcessTerminate -> + return () + +serviceProcessStatic :: Static (Backend -> Process ()) +serviceProcessStatic = staticLabel "serviceProcess" + +-- | Start a new Cloud Haskell node on the given virtual machine +spawnNodeOnVM :: Backend -> VirtualMachine -> String -> IO NodeId +spawnNodeOnVM backend vm port = + processNodeId <$> spawnOnVM backend vm port (staticClosure serviceProcessStatic) + +-- | Terminate a node started with 'spawnNodeOnVM' +terminateNode :: NodeId -> Process () +terminateNode nid = nsendRemote nid "serviceProcess" ServiceProcessTerminate + +__remoteTable :: RemoteTable -> RemoteTable +__remoteTable = registerStatic "serviceProcess" (toDynamic serviceProcess) From 31ebae7a2f5e4b19b7fcfa4a4a336c2af917cd5f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 5 Sep 2012 13:53:08 +0100 Subject: [PATCH 0208/2357] Distributed Fib on Azure working! --- demos/Fib.hs | 118 ++++++++++++--------------------------------------- 1 file changed, 26 insertions(+), 92 deletions(-) diff --git a/demos/Fib.hs b/demos/Fib.hs index 92adb21c..c03e748a 100644 --- a/demos/Fib.hs +++ b/demos/Fib.hs @@ -1,8 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} import System.Environment (getArgs) import System.Random (randomIO) -import Data.Binary (encode) -import Data.ByteString.Lazy (ByteString) import Control.Monad (forM) import Control.Monad.IO.Class (liftIO) import Control.Distributed.Process @@ -11,76 +9,48 @@ import Control.Distributed.Process , SendPort , newChan , sendChan - , Closure , spawn , receiveChan , spawnLocal ) -import Control.Distributed.Process.Closure - ( SerializableDict(SerializableDict) - , staticDecode - ) import Control.Distributed.Process.Backend.Azure -import Control.Distributed.Static - ( Static - , staticLabel - , closure - , RemoteTable - , registerStatic - , staticCompose +import Control.Distributed.Process.Closure + ( remotable + , remotableDecl + , mkClosure ) -import Data.Rank1Dynamic (toDynamic) -import Control.Distributed.Process.Closure (remotable, mkClosure) -import qualified Language.Haskell.TH as TH randomElement :: [a] -> IO a randomElement xs = do ix <- randomIO return (xs !! (ix `mod` length xs)) -f :: [TH.Name] -> TH.Q [TH.Dec] -f = remotable - -remotableDec :: [TH.Dec] -> TH.Q [TH.Dec] -remotableDec = undefined - -remotableDec [ [d| f :: Int |] ] - -main :: IO () -main = undefined - --------------------------------------------------------------------------------- --- Auxiliary -- --------------------------------------------------------------------------------- - - - -{- - --------------------------------------------------------------------------------- --- Distributed Fibonacci -- --------------------------------------------------------------------------------- - -dfib :: ([NodeId], Integer, SendPort Integer) -> Process () -dfib (_, 0, reply) = sendChan reply 0 -dfib (_, 1, reply) = sendChan reply 1 -dfib (nids, n, reply) = do - nid1 <- liftIO $ randomElement nids - nid2 <- liftIO $ randomElement nids - (sport, rport) <- newChan - spawn nid1 $ dfibClosure nids (n - 2) sport - spawn nid2 $ dfibClosure nids (n - 1) sport - n1 <- receiveChan rport - n2 <- receiveChan rport - sendChan reply $ n1 + n2 +remotableDecl [ + [d| dfib :: ([NodeId], SendPort Integer, Integer) -> Process () ; + dfib (_, reply, 0) = sendChan reply 0 + dfib (_, reply, 1) = sendChan reply 1 + dfib (nids, reply, n) = do + nid1 <- liftIO $ randomElement nids + nid2 <- liftIO $ randomElement nids + (sport, rport) <- newChan + _ <- spawn nid1 $ $(mkClosure 'dfib) (nids, sport, n - 2) + _ <- spawn nid2 $ $(mkClosure 'dfib) (nids, sport, n - 1) + n1 <- receiveChan rport + n2 <- receiveChan rport + sendChan reply $ n1 + n2 + |] + ] remoteFib :: ([NodeId], Integer) -> Backend -> Process () remoteFib (nids, n) _backend = do (sport, rport) <- newChan - spawnLocal $ dfib (nids, n, sport) + _ <- spawnLocal $ dfib (nids, sport, n) fib_n <- receiveChan rport + mapM_ terminateNode nids remoteSend fib_n +remotable ['remoteFib] + printResult :: LocalProcess () printResult = do result <- localExpect :: LocalProcess Integer @@ -90,7 +60,7 @@ main :: IO () main = do args <- getArgs case args of - "onvm":args' -> onVmMain __remoteTable args' + "onvm":args' -> onVmMain (__remoteTable . __remoteTableDecl) args' [sid, x509, pkey, user, cloudService, n] -> do params <- defaultAzureParameters sid x509 pkey let params' = params { azureSshUserName = user } @@ -98,43 +68,7 @@ main = do vms <- findVMs backend nids <- forM vms $ \vm -> spawnNodeOnVM backend vm "8080" callOnVM backend (head vms) "8081" $ - ProcessPair (remoteFibClosure nids (read n)) printResult + ProcessPair ($(mkClosure 'remoteFib) (nids, read n :: Integer)) + printResult _ -> error "Invalid command line arguments" - --------------------------------------------------------------------------------- --- Static plumping -- --------------------------------------------------------------------------------- - -dfibStatic :: Static (([NodeId], Integer, SendPort Integer) -> Process ()) -dfibStatic = staticLabel "dfib" - -dfibDict :: Static (SerializableDict ([NodeId], Integer, SendPort Integer)) -dfibDict = staticLabel "dfibDict" - -dfibClosure :: [NodeId] -> Integer -> SendPort Integer -> Closure (Process ()) -dfibClosure nids n reply = closure decoder (encode (nids, n, reply)) - where - decoder :: Static (ByteString -> Process ()) - decoder = dfibStatic `staticCompose` staticDecode dfibDict - -remoteFibStatic :: Static (([NodeId], Integer) -> Backend -> Process ()) -remoteFibStatic = staticLabel "remoteFib" - -remoteFibDict :: Static (SerializableDict ([NodeId], Integer)) -remoteFibDict = staticLabel "remoteFibDict" - -remoteFibClosure :: [NodeId] -> Integer -> RemoteProcess () -remoteFibClosure nids n = closure decoder (encode (nids, n)) - where - decoder :: Static (ByteString -> Backend -> Process ()) - decoder = remoteFibStatic `staticCompose` staticDecode remoteFibDict - -__remoteTable :: RemoteTable -> RemoteTable -__remoteTable = - registerStatic "dfib" (toDynamic dfib) - . registerStatic "dfibDict" (toDynamic (SerializableDict :: SerializableDict ([NodeId], Integer, SendPort Integer))) - . registerStatic "remoteFib" (toDynamic remoteFib) - . registerStatic "remoteFibDict" (toDynamic (SerializableDict :: SerializableDict ([NodeId], Integer))) - --} From 5d20f4cf53fcebd9cb679b9a4f73fc6703246c4f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 10:28:45 +0100 Subject: [PATCH 0209/2357] Layout --- src/Network/Transport/TCP.hs | 161 ++++++++++++++------------ src/Network/Transport/TCP/Internal.hs | 48 ++++---- 2 files changed, 109 insertions(+), 100 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index dabb3f09..06261bf4 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -10,23 +10,24 @@ -- Applications that use the TCP transport should use -- 'Network.Socket.withSocketsDo' in their main function for Windows -- compatibility (see "Network.Socket"). -module Network.Transport.TCP ( -- * Main API - createTransport - , TCPParameters(..) - , defaultTCPParameters - -- * Internals (exposed for unit tests) - , createTransportExposeInternals - , TransportInternals(..) - , EndPointId - , encodeEndPointAddress - , decodeEndPointAddress - , ControlHeader(..) - , ConnectionRequestResponse(..) - , firstNonReservedConnectionId - , socketToEndPoint - -- * Design notes - -- $design - ) where +module Network.Transport.TCP + ( -- * Main API + createTransport + , TCPParameters(..) + , defaultTCPParameters + -- * Internals (exposed for unit tests) + , createTransportExposeInternals + , TransportInternals(..) + , EndPointId + , encodeEndPointAddress + , decodeEndPointAddress + , ControlHeader(..) + , ConnectionRequestResponse(..) + , firstNonReservedConnectionId + , socketToEndPoint + -- * Design notes + -- $design + ) where import Prelude hiding ( mapM_ @@ -36,64 +37,69 @@ import Prelude hiding ) import Network.Transport -import Network.Transport.TCP.Internal ( forkServer - , recvWithLength - , recvInt32 - , tryCloseSocket - ) -import Network.Transport.Internal ( encodeInt32 - , decodeInt32 - , prependLength - , mapIOException - , tryIO - , tryToEnum - , void - , timeoutMaybe - , asyncWhenCancelled - ) -import qualified Network.Socket as N ( HostName - , ServiceName - , Socket - , getAddrInfo - , socket - , addrFamily - , addrAddress - , SocketType(Stream) - , defaultProtocol - , setSocketOption - , SocketOption(ReuseAddr) - , connect - , sOMAXCONN - , AddrInfo - ) +import Network.Transport.TCP.Internal + ( forkServer + , recvWithLength + , recvInt32 + , tryCloseSocket + ) +import Network.Transport.Internal + ( encodeInt32 + , decodeInt32 + , prependLength + , mapIOException + , tryIO + , tryToEnum + , void + , timeoutMaybe + , asyncWhenCancelled + ) +import qualified Network.Socket as N + ( HostName + , ServiceName + , Socket + , getAddrInfo + , socket + , addrFamily + , addrAddress + , SocketType(Stream) + , defaultProtocol + , setSocketOption + , SocketOption(ReuseAddr) + , connect + , sOMAXCONN + , AddrInfo + ) import Network.Socket.ByteString (sendMany) import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) -import Control.Concurrent.MVar ( MVar - , newMVar - , modifyMVar - , modifyMVar_ - , readMVar - , takeMVar - , putMVar - , newEmptyMVar - , withMVar - ) +import Control.Concurrent.MVar + ( MVar + , newMVar + , modifyMVar + , modifyMVar_ + , readMVar + , takeMVar + , putMVar + , newEmptyMVar + , withMVar + ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) import Control.Monad (when, unless) -import Control.Exception ( IOException - , SomeException - , AsyncException - , handle - , throw - , throwIO - , try - , bracketOnError - , mask - , onException - , fromException - ) +import Control.Exception + ( IOException + , SomeException + , AsyncException + , handle + , throw + , throwIO + , try + , bracketOnError + , mask + , onException + , fromException + ) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (concat) @@ -102,14 +108,15 @@ import Data.Int (Int32) import Data.IntMap (IntMap) import qualified Data.IntMap as IntMap (empty) import Data.IntSet (IntSet) -import qualified Data.IntSet as IntSet ( empty - , insert - , elems - , singleton - , null - , delete - , member - ) +import qualified Data.IntSet as IntSet + ( empty + , insert + , elems + , singleton + , null + , delete + , member + ) import Data.Map (Map) import qualified Data.Map as Map (empty) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs index b7301f13..d084285e 100644 --- a/src/Network/Transport/TCP/Internal.hs +++ b/src/Network/Transport/TCP/Internal.hs @@ -1,33 +1,35 @@ -- | Utility functions for TCP sockets -module Network.Transport.TCP.Internal ( forkServer - , recvWithLength - , recvExact - , recvInt32 - , tryCloseSocket - ) where +module Network.Transport.TCP.Internal + ( forkServer + , recvWithLength + , recvExact + , recvInt32 + , tryCloseSocket + ) where #if ! MIN_VERSION_base(4,6,0) import Prelude hiding (catch) #endif import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) -import qualified Network.Socket as N ( HostName - , ServiceName - , Socket - , SocketType(Stream) - , SocketOption(ReuseAddr) - , getAddrInfo - , defaultHints - , socket - , bindSocket - , listen - , addrFamily - , addrAddress - , defaultProtocol - , setSocketOption - , accept - , sClose - ) +import qualified Network.Socket as N + ( HostName + , ServiceName + , Socket + , SocketType(Stream) + , SocketOption(ReuseAddr) + , getAddrInfo + , defaultHints + , socket + , bindSocket + , listen + , addrFamily + , addrAddress + , defaultProtocol + , setSocketOption + , accept + , sClose + ) import qualified Network.Socket.ByteString as NBS (recv) import Control.Concurrent (ThreadId) import Control.Monad (forever, when) From ef41ac51b170399fe92f3e5c18205de73d3ac3d3 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 10:28:45 +0100 Subject: [PATCH 0210/2357] Layout --- src/Network/Transport.hs | 45 ++++++++++----------- src/Network/Transport/Internal.hs | 66 ++++++++++++++++--------------- src/Network/Transport/Util.hs | 11 +++--- 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 04d427db..d923b008 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -1,26 +1,27 @@ -- | Network Transport -module Network.Transport ( -- * Types - Transport(..) - , EndPoint(..) - , Connection(..) - , Event(..) - , ConnectionId - , Reliability(..) - , MulticastGroup(..) - , EndPointAddress(..) - , MulticastAddress(..) - -- * Hints - , ConnectHints(..) - , defaultConnectHints - -- * Error codes - , TransportError(..) - , NewEndPointErrorCode(..) - , ConnectErrorCode(..) - , NewMulticastGroupErrorCode(..) - , ResolveMulticastGroupErrorCode(..) - , SendErrorCode(..) - , EventErrorCode(..) - ) where +module Network.Transport + ( -- * Types + Transport(..) + , EndPoint(..) + , Connection(..) + , Event(..) + , ConnectionId + , Reliability(..) + , MulticastGroup(..) + , EndPointAddress(..) + , MulticastAddress(..) + -- * Hints + , ConnectHints(..) + , defaultConnectHints + -- * Error codes + , TransportError(..) + , NewEndPointErrorCode(..) + , ConnectErrorCode(..) + , NewMulticastGroupErrorCode(..) + , ResolveMulticastGroupErrorCode(..) + , SendErrorCode(..) + , EventErrorCode(..) + ) where import Data.ByteString (ByteString) import qualified Data.ByteString as BS (copy) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 14812a8f..076460ec 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -1,22 +1,23 @@ -- | Internal functions -module Network.Transport.Internal ( -- * Encoders/decoders - encodeInt32 - , decodeInt32 - , encodeInt16 - , decodeInt16 - , prependLength - -- * Miscellaneous abstractions - , mapIOException - , tryIO - , tryToEnum - , timeoutMaybe - , asyncWhenCancelled - -- * Replicated functionality from "base" - , void - , forkIOWithUnmask - -- * Debugging - , tlog - ) where +module Network.Transport.Internal + ( -- * Encoders/decoders + encodeInt32 + , decodeInt32 + , encodeInt16 + , decodeInt16 + , prependLength + -- * Miscellaneous abstractions + , mapIOException + , tryIO + , tryToEnum + , timeoutMaybe + , asyncWhenCancelled + -- * Replicated functionality from "base" + , void + , forkIOWithUnmask + -- * Debugging + , tlog + ) where #if ! MIN_VERSION_base(4,6,0) import Prelude hiding (catch) @@ -27,20 +28,23 @@ import Foreign.C (CInt(..), CShort(..)) import Foreign.ForeignPtr (withForeignPtr) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length) -import qualified Data.ByteString.Internal as BSI ( unsafeCreate - , toForeignPtr - , inlinePerformIO) +import qualified Data.ByteString.Internal as BSI + ( unsafeCreate + , toForeignPtr + , inlinePerformIO + ) import Control.Monad.IO.Class (MonadIO, liftIO) -import Control.Exception ( IOException - , SomeException - , AsyncException - , Exception - , catch - , try - , throw - , throwIO - , mask_ - ) +import Control.Exception + ( IOException + , SomeException + , AsyncException + , Exception + , catch + , try + , throw + , throwIO + , mask_ + ) import Control.Concurrent (ThreadId, forkIO) import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar) import GHC.IO (unsafeUnmask) diff --git a/src/Network/Transport/Util.hs b/src/Network/Transport/Util.hs index d2c0d4ad..bea5da5e 100644 --- a/src/Network/Transport/Util.hs +++ b/src/Network/Transport/Util.hs @@ -3,11 +3,12 @@ -- Note: this module is bound to change even more than the rest of the API :) module Network.Transport.Util (spawn) where -import Network.Transport ( Transport - , EndPoint(..) - , EndPointAddress - , newEndPoint - ) +import Network.Transport + ( Transport + , EndPoint(..) + , EndPointAddress + , newEndPoint + ) import Control.Concurrent (forkIO) import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) From a2aaa88d8c44070a7620f6d0bfece585c3b229d2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 10:31:05 +0100 Subject: [PATCH 0211/2357] Unique fields --- src/Network/Transport/TCP.hs | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 06261bf4..fc0824ee 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -231,34 +231,34 @@ import Data.Foldable (forM_, mapM_) -- ValidRemoteEndPointState). data TCPTransport = TCPTransport - { transportHost :: N.HostName - , transportPort :: N.ServiceName - , transportState :: MVar TransportState - , transportParams :: TCPParameters + { transportHost :: !N.HostName + , transportPort :: !N.ServiceName + , transportState :: !(MVar TransportState) + , transportParams :: !TCPParameters } data TransportState = - TransportValid ValidTransportState + TransportValid !ValidTransportState | TransportClosed data ValidTransportState = ValidTransportState - { _localEndPoints :: Map EndPointAddress LocalEndPoint - , _nextEndPointId :: EndPointId + { _localEndPoints :: !(Map EndPointAddress LocalEndPoint) + , _nextEndPointId :: !EndPointId } data LocalEndPoint = LocalEndPoint - { localAddress :: EndPointAddress - , localChannel :: Chan Event - , localState :: MVar LocalEndPointState + { localAddress :: !EndPointAddress + , localChannel :: !(Chan Event) + , localState :: !(MVar LocalEndPointState) } data LocalEndPointState = - LocalEndPointValid ValidLocalEndPointState + LocalEndPointValid !ValidLocalEndPointState | LocalEndPointClosed data ValidLocalEndPointState = ValidLocalEndPointState { _nextConnectionId :: !ConnectionId - , _localConnections :: Map EndPointAddress RemoteEndPoint + , _localConnections :: !(Map EndPointAddress RemoteEndPoint) , _nextRemoteId :: !Int } @@ -345,9 +345,9 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- modifyRemoteState. data RemoteEndPoint = RemoteEndPoint - { remoteAddress :: EndPointAddress - , remoteState :: MVar RemoteState - , remoteId :: Int + { remoteAddress :: !EndPointAddress + , remoteState :: !(MVar RemoteState) + , remoteId :: !Int } data RequestedBy = RequestedByUs | RequestedByThem @@ -355,25 +355,25 @@ data RequestedBy = RequestedByUs | RequestedByThem data RemoteState = -- | Invalid remote endpoint (for example, invalid address) - RemoteEndPointInvalid (TransportError ConnectErrorCode) + RemoteEndPointInvalid !(TransportError ConnectErrorCode) -- | The remote endpoint is being initialized - | RemoteEndPointInit (MVar ()) RequestedBy + | RemoteEndPointInit !(MVar ()) !RequestedBy -- | "Normal" working endpoint - | RemoteEndPointValid ValidRemoteEndPointState + | RemoteEndPointValid !ValidRemoteEndPointState -- | The remote endpoint is being closed (garbage collected) - | RemoteEndPointClosing (MVar ()) ValidRemoteEndPointState + | RemoteEndPointClosing !(MVar ()) !ValidRemoteEndPointState -- | The remote endpoint has been closed (garbage collected) | RemoteEndPointClosed -- | The remote endpoint has failed, or has been forcefully shutdown -- using a closeTransport or closeEndPoint API call - | RemoteEndPointFailed IOException + | RemoteEndPointFailed !IOException data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int - , _remoteIncoming :: IntSet - , remoteSocket :: N.Socket + , _remoteIncoming :: !IntSet + , remoteSocket :: !N.Socket , sendOn :: [ByteString] -> IO () - , _pendingCtrlRequests :: IntMap (MVar (Either IOException [ByteString])) + , _pendingCtrlRequests :: !(IntMap (MVar (Either IOException [ByteString]))) , _nextCtrlRequestId :: !ControlRequestId } From 3855e91dcad7381f6626fc47adb8f4c9c60d5279 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 11:09:14 +0100 Subject: [PATCH 0212/2357] Avoid I/O inside modifyMVar Many of the API functions had the structure modifyMVar (state of heavyweight connection) $ do ... send on heavyweight connection ... return (new heavyweight connection state) Unfortunately, the thread that handles *incoming* messages also occassionally needs to make modifications to the state of the heavyweight connection, and hence also calls modifyMVar in various places. This can lead to "distributed deadlock": consider endpoints A and B. Both A and B start sending lots of messages to the other party, so much that in both cases the 'send' in the above example starts to block (because the OS send buffer fills up). This means that both parties will be stuck, with the MVar "locked", until the other party receives some messages. However, it might happen that both parties did a receive and need to obtain the mvar in order to make some changes -- but that MVar is still blocked by the sending thread on that side. So we have waiting on A send thread ---------------\ /------------ B send thread ^ \ / ^ | \ / | | waiting on /--------------/ | | / \ | A receive thread <----/ \-------------------> B receive thread A nice distributed deadlock. To avoid this problem we now avoid doing sends within a modifyMVar. This is not entirely trivial, however. Simply doing thingToSend <- modifyMVar (....) send thingToSend is not good enough, because now if we have to threads doing the above the sends may be done in the wrong order. We therefore do act <- modifyMVar (... ; act <- schedule (...); ...; return act) runScheduledAction act 'schedule' adds an IO operation to a queue, and runSCheduledAction runs the next operation from this queue. This may mean of course that 'runScheduledAction' may not execute the action which was scheduled "textually before" it, so we have to be careful that the right exceptions are thrown in the right place. That's why 'schedule' creates an MVar for each operation, and 'runScheduledAction' waits for that MVar to be filled. --- src/Network/Transport/TCP.hs | 404 +++++++++++++++-------------------- 1 file changed, 171 insertions(+), 233 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index fc0824ee..e1cc0954 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -86,7 +86,7 @@ import Control.Concurrent.MVar ) import Control.Category ((>>>)) import Control.Applicative ((<$>)) -import Control.Monad (when, unless) +import Control.Monad (when, unless, join) import Control.Exception ( IOException , SomeException @@ -96,9 +96,8 @@ import Control.Exception , throwIO , try , bracketOnError - , mask - , onException , fromException + , catch ) import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) @@ -345,9 +344,10 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- modifyRemoteState. data RemoteEndPoint = RemoteEndPoint - { remoteAddress :: !EndPointAddress - , remoteState :: !(MVar RemoteState) - , remoteId :: !Int + { remoteAddress :: !EndPointAddress + , remoteState :: !(MVar RemoteState) + , remoteId :: !Int + , remoteScheduled :: !(Chan (IO ())) } data RequestedBy = RequestedByUs | RequestedByThem @@ -571,22 +571,27 @@ apiConnect params ourEndPoint theirAddress _reliability hints = apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO . asyncWhenCancelled return $ do - modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity - { caseValid = \vst -> do - alive <- readIORef connAlive - if alive - then do - writeIORef connAlive False - sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] - return ( RemoteEndPointValid - . (remoteOutgoing ^: (\x -> x - 1)) - $ vst - ) - else - return (RemoteEndPointValid vst) - } + mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> do + alive <- readIORef connAlive + if alive + then do + writeIORef connAlive False + act <- schedule theirEndPoint $ + sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] + return ( RemoteEndPointValid + . (remoteOutgoing ^: (\x -> x - 1)) + $ vst + , Just act + ) + else + return (RemoteEndPointValid vst, Nothing) + _ -> + return (st, Nothing) + forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) closeIfUnused (ourEndPoint, theirEndPoint) + -- | Send data across a connection apiSend :: EndPointPair -- ^ Local and remote endpoint -> ConnectionId -- ^ Connection ID (supplied by remote endpoint) @@ -595,33 +600,33 @@ apiSend :: EndPointPair -- ^ Local and remote endpoint -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = -- We don't need the overhead of asyncWhenCancelled here - try . mapIOException sendFailed $ - withRemoteState (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseInvalid = \_ -> - relyViolation (ourEndPoint, theirEndPoint) "apiSend" - , caseInit = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "apiSend" - , caseValid = \vst -> do - alive <- readIORef connAlive - if alive - then sendOn vst (encodeInt32 connId : prependLength payload) - else throwIO $ TransportError SendClosed "Connection closed" - , caseClosing = \_ _ -> do - alive <- readIORef connAlive - if alive - then relyViolation (ourEndPoint, theirEndPoint) "apiSend" - else throwIO $ TransportError SendClosed "Connection closed" - , caseClosed = do - alive <- readIORef connAlive - if alive - then relyViolation (ourEndPoint, theirEndPoint) "apiSend" - else throwIO $ TransportError SendClosed "Connection closed" - , caseFailed = \err -> do - alive <- readIORef connAlive - if alive - then throwIO $ TransportError SendFailed (show err) - else throwIO $ TransportError SendClosed "Connection closed" - } + try . mapIOException sendFailed $ do + act <- withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInvalid _ -> + relyViolation (ourEndPoint, theirEndPoint) "apiSend" + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) "apiSend" + RemoteEndPointValid vst -> do + alive <- readIORef connAlive + if alive + then schedule theirEndPoint $ sendOn vst (encodeInt32 connId : prependLength payload) + else throwIO $ TransportError SendClosed "Connection closed" + RemoteEndPointClosing _ _ -> do + alive <- readIORef connAlive + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + else throwIO $ TransportError SendClosed "Connection closed" + RemoteEndPointClosed -> do + alive <- readIORef connAlive + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + else throwIO $ TransportError SendClosed "Connection closed" + RemoteEndPointFailed err -> do + alive <- readIORef connAlive + if alive + then throwIO $ TransportError SendFailed (show err) + else throwIO $ TransportError SendClosed "Connection closed" + runScheduledAction (ourEndPoint, theirEndPoint) act where sendFailed = TransportError SendFailed . show @@ -651,155 +656,28 @@ apiCloseEndPoint transport evs ourEndPoint = -- We make an attempt to close the connection nicely -- (by sending a CloseSocket first) let closed = RemoteEndPointFailed . userError $ "apiCloseEndPoint" - modifyMVar_ (remoteState theirEndPoint) $ \st -> + mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> - return st + return (st, Nothing) RemoteEndPointInit resolved _ -> do putMVar resolved () - return closed - RemoteEndPointValid conn -> do - tryIO $ sendOn conn [encodeInt32 CloseSocket] - tryCloseSocket (remoteSocket conn) - return closed - RemoteEndPointClosing resolved conn -> do + return (closed, Nothing) + RemoteEndPointValid vst -> do + act <- schedule theirEndPoint $ do + tryIO $ sendOn vst [encodeInt32 CloseSocket] + tryCloseSocket (remoteSocket vst) + return (closed, Just act) + RemoteEndPointClosing resolved vst -> do putMVar resolved () - tryCloseSocket (remoteSocket conn) - return closed + act <- schedule theirEndPoint $ tryCloseSocket (remoteSocket vst) + return (closed, Just act) RemoteEndPointClosed -> - return st + return (st, Nothing) RemoteEndPointFailed err -> - return $ RemoteEndPointFailed err - --------------------------------------------------------------------------------- --- As soon as a remote connection fails, we want to put notify our endpoint -- --- and put it into a closed state. Since this may happen in many places, we -- --- provide some abstractions. -- --------------------------------------------------------------------------------- - -data RemoteStatePatternMatch a = RemoteStatePatternMatch - { caseInvalid :: TransportError ConnectErrorCode -> IO a - , caseInit :: MVar () -> RequestedBy -> IO a - , caseValid :: ValidRemoteEndPointState -> IO a - , caseClosing :: MVar () -> ValidRemoteEndPointState -> IO a - , caseClosed :: IO a - , caseFailed :: IOException -> IO a - } - -remoteStateIdentity :: RemoteStatePatternMatch RemoteState -remoteStateIdentity = - RemoteStatePatternMatch - { caseInvalid = return . RemoteEndPointInvalid - , caseInit = (return .) . RemoteEndPointInit - , caseValid = return . RemoteEndPointValid - , caseClosing = (return .) . RemoteEndPointClosing - , caseClosed = return RemoteEndPointClosed - , caseFailed = return . RemoteEndPointFailed - } - --- | Like modifyMVar, but if an I/O exception occurs don't restore the remote --- endpoint to its original value but close it instead -modifyRemoteState :: EndPointPair - -> RemoteStatePatternMatch (RemoteState, a) - -> IO a -modifyRemoteState (ourEndPoint, theirEndPoint) match = - mask $ \restore -> do - st <- takeMVar theirState - case st of - RemoteEndPointValid vst -> do - mResult <- try $ restore (caseValid match vst) - case mResult of - Right (st', a) -> do - putMVar theirState st' - return a - Left ex -> do - case fromException ex of - Just ioEx -> handleIOException ioEx vst - Nothing -> putMVar theirState st - throwIO ex - -- The other cases are less interesting, because unless the endpoint is - -- in Valid state we're not supposed to do any IO on it - RemoteEndPointInit resolved origin -> do - (st', a) <- onException (restore $ caseInit match resolved origin) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointClosing resolved vst -> do - (st', a) <- onException (restore $ caseClosing match resolved vst) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointInvalid err -> do - (st', a) <- onException (restore $ caseInvalid match err) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointClosed -> do - (st', a) <- onException (restore $ caseClosed match) - (putMVar theirState st) - putMVar theirState st' - return a - RemoteEndPointFailed err -> do - (st', a) <- onException (restore $ caseFailed match err) - (putMVar theirState st) - putMVar theirState st' - return a - where - theirState :: MVar RemoteState - theirState = remoteState theirEndPoint - - handleIOException :: IOException -> ValidRemoteEndPointState -> IO () - handleIOException ex vst = do - tryCloseSocket (remoteSocket vst) - putMVar theirState (RemoteEndPointFailed ex) - let code = EventConnectionLost (remoteAddress theirEndPoint) - err = TransportError code (show ex) - writeChan (localChannel ourEndPoint) $ ErrorEvent err - --- | Like 'modifyRemoteState' but without a return value -modifyRemoteState_ :: EndPointPair - -> RemoteStatePatternMatch RemoteState - -> IO () -modifyRemoteState_ (ourEndPoint, theirEndPoint) match = - modifyRemoteState (ourEndPoint, theirEndPoint) - RemoteStatePatternMatch - { caseInvalid = u . caseInvalid match - , caseInit = \resolved origin -> u $ caseInit match resolved origin - , caseValid = u . caseValid match - , caseClosing = \resolved vst -> u $ caseClosing match resolved vst - , caseClosed = u $ caseClosed match - , caseFailed = u . caseFailed match - } - where - u :: IO a -> IO (a, ()) - u p = p >>= \a -> return (a, ()) - --- | Like 'modifyRemoteState' but without the ability to change the state -withRemoteState :: EndPointPair - -> RemoteStatePatternMatch a - -> IO a -withRemoteState (ourEndPoint, theirEndPoint) match = - modifyRemoteState (ourEndPoint, theirEndPoint) - RemoteStatePatternMatch - { caseInvalid = \err -> do - a <- caseInvalid match err - return (RemoteEndPointInvalid err, a) - , caseInit = \resolved origin -> do - a <- caseInit match resolved origin - return (RemoteEndPointInit resolved origin, a) - , caseValid = \vst -> do - a <- caseValid match vst - return (RemoteEndPointValid vst, a) - , caseClosing = \resolved vst -> do - a <- caseClosing match resolved vst - return (RemoteEndPointClosing resolved vst, a) - , caseClosed = do - a <- caseClosed match - return (RemoteEndPointClosed, a) - , caseFailed = \err -> do - a <- caseFailed match err - return (RemoteEndPointFailed err, a) - } + return (RemoteEndPointFailed err, Nothing) + forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) + -------------------------------------------------------------------------------- -- Incoming requests -- @@ -1029,8 +907,8 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- Close the socket (if we don't have any outgoing connections) closeSocket :: N.Socket -> IO Bool - closeSocket sock = - modifyMVar theirState $ \st -> + closeSocket sock = do + mAct <- modifyMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) @@ -1050,22 +928,28 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do then do removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- Attempt to reply (but don't insist) - tryIO $ sendOn vst' [encodeInt32 CloseSocket] - tryCloseSocket sock - return (RemoteEndPointClosed, True) + act <- schedule theirEndPoint $ do + tryIO $ sendOn vst' [encodeInt32 CloseSocket] + tryCloseSocket sock + return (RemoteEndPointClosed, Just act) else - return (RemoteEndPointValid vst', False) + return (RemoteEndPointValid vst', Nothing) RemoteEndPointClosing resolved _ -> do removeRemoteEndPoint (ourEndPoint, theirEndPoint) - tryCloseSocket sock + act <- schedule theirEndPoint $ tryCloseSocket sock putMVar resolved () - return (RemoteEndPointClosed, True) + return (RemoteEndPointClosed, Just act) RemoteEndPointFailed err -> throwIO err RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (closed)" - + case mAct of + Nothing -> return False + Just act -> do + runScheduledAction (ourEndPoint, theirEndPoint) act + return True + -- Read a message and output it on the endPoint's channel. By rights we -- should verify that the connection ID is valid, but this is unnecessary -- overhead @@ -1191,27 +1075,28 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] doRemoteRequest (ourEndPoint, theirEndPoint) header = do replyMVar <- newEmptyMVar - modifyRemoteState_ (ourEndPoint, theirEndPoint) RemoteStatePatternMatch - { caseValid = \vst -> do - let reqId = vst ^. nextCtrlRequestId - sendOn vst [encodeInt32 header, encodeInt32 reqId] - return ( RemoteEndPointValid - . (nextCtrlRequestId ^: (+ 1)) - . (pendingCtrlRequestsAt reqId ^= Just replyMVar) - $ vst - ) + act <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> do + let reqId = vst ^. nextCtrlRequestId + act <- schedule theirEndPoint $ sendOn vst [encodeInt32 header, encodeInt32 reqId] + return ( RemoteEndPointValid + . (nextCtrlRequestId ^: (+ 1)) + . (pendingCtrlRequestsAt reqId ^= Just replyMVar) + $ vst + , act + ) -- Error cases - , caseInvalid = - throwIO - , caseInit = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" - , caseClosing = \_ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - , caseClosed = - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" - , caseFailed = - throwIO - } + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointInit _ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" + RemoteEndPointClosing _ _ -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" + RemoteEndPointClosed -> + relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" + RemoteEndPointFailed err -> + throwIO err + runScheduledAction (ourEndPoint, theirEndPoint) act mReply <- takeMVar replyMVar case mReply of Left err -> throwIO err @@ -1219,17 +1104,19 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do -- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO () -closeIfUnused (ourEndPoint, theirEndPoint) = - modifyRemoteState_ (ourEndPoint, theirEndPoint) remoteStateIdentity - { caseValid = \vst -> - if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) - then do - sendOn vst [encodeInt32 CloseSocket] - resolved <- newEmptyMVar - return $ RemoteEndPointClosing resolved vst - else - return $ RemoteEndPointValid vst - } +closeIfUnused (ourEndPoint, theirEndPoint) = do + mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> + if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + then do + resolved <- newEmptyMVar + act <- schedule theirEndPoint $ sendOn vst [encodeInt32 CloseSocket] + return (RemoteEndPointClosing resolved vst, Just act) + else + return (RemoteEndPointValid vst, Nothing) + _ -> + return (st, Nothing) + forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) -- | Reset a remote endpoint if it is in Invalid mode -- @@ -1419,12 +1306,14 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go Just theirEndPoint -> return (st, (theirEndPoint, False)) Nothing -> do - resolved <- newEmptyMVar + resolved <- newEmptyMVar theirState <- newMVar (RemoteEndPointInit resolved findOrigin) + scheduled <- newChan let theirEndPoint = RemoteEndPoint - { remoteAddress = theirAddress - , remoteState = theirState - , remoteId = vst ^. nextRemoteId + { remoteAddress = theirAddress + , remoteState = theirState + , remoteId = vst ^. nextRemoteId + , remoteScheduled = scheduled } return ( LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) @@ -1489,6 +1378,55 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go ourState = localState ourEndPoint ourAddress = localAddress ourEndPoint +-------------------------------------------------------------------------------- +-- Scheduling actions -- +-------------------------------------------------------------------------------- + +-- | See 'schedule'/'runScheduledAction' +type Action a = MVar (Either SomeException a) + +-- | Schedule an action to be executed (see also 'runScheduledAction') +schedule :: RemoteEndPoint -> IO a -> IO (Action a) +schedule theirEndPoint act = do + mvar <- newEmptyMVar + writeChan (remoteScheduled theirEndPoint) $ + catch (act >>= putMVar mvar . Right) (putMVar mvar . Left) + return mvar + +-- | Run a scheduled action. Every call to 'schedule' should be paired with a +-- call to 'runScheduledAction' so that every scheduled action is run. Note +-- however that the there is no guarantee that in +-- +-- > do act <- schedule p +-- > runScheduledAction +-- +-- 'runScheduledAction' will run @p@ (it might run some other scheduled action). +-- However, it will then wait until @p@ is executed (by this call to +-- 'runScheduledAction' or by another). +runScheduledAction :: EndPointPair -> Action a -> IO a +runScheduledAction (ourEndPoint, theirEndPoint) mvar = do + join $ readChan (remoteScheduled theirEndPoint) + ma <- readMVar mvar + case ma of + Right a -> return a + Left e -> do + forM_ (fromException e) $ \ioe -> + modifyMVar_ (remoteState theirEndPoint) $ \st -> + case st of + RemoteEndPointValid vst -> handleIOException ioe vst + _ -> return (RemoteEndPointFailed ioe) + throwIO e + where + handleIOException :: IOException + -> ValidRemoteEndPointState + -> IO RemoteState + handleIOException ex vst = do + tryCloseSocket (remoteSocket vst) + let code = EventConnectionLost (remoteAddress theirEndPoint) + err = TransportError code (show ex) + writeChan (localChannel ourEndPoint) $ ErrorEvent err + return (RemoteEndPointFailed ex) + -------------------------------------------------------------------------------- -- "Stateless" (MVar free) functions -- -------------------------------------------------------------------------------- From 6207de60bae7fcf39c108a49bdc152a98abdd277 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 11:53:20 +0100 Subject: [PATCH 0213/2357] Optimize handling of 'Crossed' connection requests When A tries to connect (set up a heavyweight connection to) B, while B tries to connect to A at the same time, there will be two threads trying to set up a heavyweight connection for A (and similarly for B): the thread that deals with the apiConnect, and the thread that deals with the incoming request from B. In the case that "B" wins and sends a ConnectionRequestCrossed message to A, the thread that daels with the apiConenct should simply wait until the handleIncomingRequest thread completes. We did this already, but it is important that the handleIncomingRequest thread *in B* (which loses to B's apiConnect thread) waits until A receives the ConnectionRequestCrossed message (to avoid certain ordering problems). We had implemented this by regarding an incoming ConnectionRequestCrossed message as changing the heavyweight connection from Init to Closed state (and signalling on resolved), but that's wrong as it will cause A to try and connect again. Now instead when we receive a ConnectionRequestCrossed message we signal on a (new) MVar "crossed" (part of the Init state), but leave the EndPoint in Init state (it will be resolved by the handleIncmoingMessage thread). --- src/Network/Transport/TCP.hs | 42 +++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index e1cc0954..787b875f 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -357,7 +357,7 @@ data RemoteState = -- | Invalid remote endpoint (for example, invalid address) RemoteEndPointInvalid !(TransportError ConnectErrorCode) -- | The remote endpoint is being initialized - | RemoteEndPointInit !(MVar ()) !RequestedBy + | RemoteEndPointInit !(MVar ()) !(MVar ()) !RequestedBy -- | "Normal" working endpoint | RemoteEndPointValid !ValidRemoteEndPointState -- | The remote endpoint is being closed (garbage collected) @@ -604,7 +604,7 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = act <- withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "apiSend" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "apiSend" RemoteEndPointValid vst -> do alive <- readIORef connAlive @@ -660,7 +660,7 @@ apiCloseEndPoint transport evs ourEndPoint = case st of RemoteEndPointInvalid _ -> return (st, Nothing) - RemoteEndPointInit resolved _ -> do + RemoteEndPointInit resolved _ _ -> do putMVar resolved () return (closed, Nothing) RemoteEndPointValid vst -> do @@ -764,7 +764,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (invalid)" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (init)" RemoteEndPointValid ep -> @@ -820,7 +820,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection (invalid)" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection (init)" RemoteEndPointValid vst -> @@ -854,7 +854,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "readControlResponse (invalid)" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "readControlResponse (init)" RemoteEndPointValid vst -> @@ -884,7 +884,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (invalid)" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (init)" RemoteEndPointValid vst -> do unless (IntSet.member cid (vst ^. remoteIncoming)) $ @@ -913,7 +913,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (invalid)" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (init)" RemoteEndPointValid vst -> do @@ -971,7 +971,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointValid vst -> do @@ -1055,7 +1055,13 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do tryCloseSocket sock return False Right (sock, ConnectionRequestCrossed) -> do - resolveInit (ourEndPoint, theirEndPoint) RemoteEndPointClosed + withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointInit _ crossed _ -> + putMVar crossed () + RemoteEndPointFailed ex -> + throwIO ex + _ -> + relyViolation (ourEndPoint, theirEndPoint) "setupRemoteEndPoint: Crossed" tryCloseSocket sock return False Left err -> do @@ -1088,7 +1094,7 @@ doRemoteRequest (ourEndPoint, theirEndPoint) header = do -- Error cases RemoteEndPointInvalid err -> throwIO err - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" RemoteEndPointClosing _ _ -> relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" @@ -1193,7 +1199,7 @@ connectToSelf ourEndPoint = do resolveInit :: EndPointPair -> RemoteState -> IO () resolveInit (ourEndPoint, theirEndPoint) newState = modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit resolved _ -> do + RemoteEndPointInit resolved _ _ -> do putMVar resolved () case newState of RemoteEndPointClosed -> @@ -1307,7 +1313,8 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go return (st, (theirEndPoint, False)) Nothing -> do resolved <- newEmptyMVar - theirState <- newMVar (RemoteEndPointInit resolved findOrigin) + crossed <- newEmptyMVar + theirState <- newMVar (RemoteEndPointInit resolved crossed findOrigin) scheduled <- newChan let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress @@ -1347,7 +1354,7 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go case snapshot of RemoteEndPointInvalid err -> throwIO err - RemoteEndPointInit resolved initOrigin -> + RemoteEndPointInit resolved crossed initOrigin -> case (findOrigin, initOrigin) of (RequestedByUs, RequestedByUs) -> readMVar resolved >> go @@ -1355,9 +1362,10 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go readMVar resolved >> go (RequestedByThem, RequestedByUs) -> if ourAddress > theirAddress - then + then do -- Wait for the Crossed message - readMVar resolved >> go + readMVar crossed + return (theirEndPoint, True) else return (theirEndPoint, False) (RequestedByThem, RequestedByThem) -> @@ -1531,7 +1539,7 @@ internalSocketBetween transport ourAddress theirAddress = do Nothing -> throwIO $ userError "Remote endpoint not found" Just ep -> return ep withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit _ _ -> + RemoteEndPointInit _ _ _ -> throwIO $ userError "Remote endpoint not yet initialized" RemoteEndPointValid vst -> return $ remoteSocket vst From 876a3dc94e516c9ec60ea8b0a65ea3f13b8c1f27 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 12:11:39 +0100 Subject: [PATCH 0214/2357] Make sure that concurrent sends don't interleave --- src/Network/Transport/TCP.hs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 787b875f..29234426 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -372,7 +372,7 @@ data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int , _remoteIncoming :: !IntSet , remoteSocket :: !N.Socket - , sendOn :: [ByteString] -> IO () + , remoteSendLock :: !(MVar ()) , _pendingCtrlRequests :: !(IntMap (MVar (Either IOException [ByteString]))) , _nextCtrlRequestId :: !ControlRequestId } @@ -727,11 +727,12 @@ handleConnectionRequest transport sock = handle handleException $ do tryCloseSocket sock return Nothing else do + sendLock <- newMVar () let vst = ValidRemoteEndPointState { remoteSocket = sock + , remoteSendLock = sendLock , _remoteOutgoing = 0 , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock , _pendingCtrlRequests = IntMap.empty , _nextCtrlRequestId = 0 } @@ -1039,11 +1040,12 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do (connectTimeout hints) didAccept <- case result of Right (sock, ConnectionRequestAccepted) -> do + sendLock <- newMVar () let vst = ValidRemoteEndPointState { remoteSocket = sock + , remoteSendLock = sendLock , _remoteOutgoing = 0 , _remoteIncoming = IntSet.empty - , sendOn = sendMany sock , _pendingCtrlRequests = IntMap.empty , _nextCtrlRequestId = 0 } @@ -1386,6 +1388,11 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go ourState = localState ourEndPoint ourAddress = localAddress ourEndPoint +-- | Send a payload over a heavyweight connection (thread safe) +sendOn :: ValidRemoteEndPointState -> [ByteString] -> IO () +sendOn vst bs = withMVar (remoteSendLock vst) $ \() -> + sendMany (remoteSocket vst) bs + -------------------------------------------------------------------------------- -- Scheduling actions -- -------------------------------------------------------------------------------- From 61a4ad39ed036c3a2ba8edc69a1e0b43a246fac2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 13:19:55 +0100 Subject: [PATCH 0215/2357] Switch to asynchronous connect In the original protocol we had recipient-allocated connection IDs. Hence, when doing a connect, we would send a connection request to the remote endpoint (provided that a heavyweight connection is already in place) and then wait for the recipient to reply with a new connection ID. This however makes connection much slower than it needs to be. We now use sender-allocated connection IDs, so that connect turns into a simple message to the remote endpoint that we allocated a new connection, and we no longer have to wait for a response. This means that all operations, with the sole exception of creating the heavyweight connection itself, are now synchronous. Hence, we have completely removed infrastructure relating to 'remote requests' (doRemoteRequest, ConnectionRequestResponse, etc.). What's not obvious is that we also had to change the 'CloseSocket' message. A sends a CloseSocket message to B when A thinks that the heavyweight connection to B is no longer required. A doesn't immediately close the socket, because it is possible that B has already sent a new connection request (now a CreatedNewConnection message) to A, even though A has not yet received it. Instead, it puts the heavyweight connection in 'Closing' state and waits for B to confirm or deny the request. When B receives A's CloseSocket message, it checks if it agrees that the heavyweight connection can be closed. If not, it simply ignores the request and A will forget it ever sent a CloseSocket request when it receives a CreatedNewConnection message from B (that is, it moves the endpoint from Closing to Valid state). If B does agree, then it will reply with a CloseSocket message, and then actually close the socket. When A receives a CloseSocket message having sent a CloseSocket message it closes the socket immediately. However, now that we have asynchronous connection requests we might have the following potential ordering of messages: A sends CloseSocket request, and puts endpoint in closing state B sends CreatedNewConnection, CloseConnection, and CloseSocket requests, putting its own representation of the endpoint also in closing state. B receives the CloseSocket message from A. Since it's endpoint is in Closing state, it immediately closes the socket. A receives B CreatedNewConnection message, hence *cancelling* the CloseSocket message it sent to be and putting the endpoint back in Valid state -- *even though* B did closed the socket. To avoid this problem, we now send the ID of the last created connection along with the close socket request. Then when B receives a CloseSocket request, it can compare this ID with the ID of the last connection it opened. If this does not match, B knows that there is still a CreatedNewConnection message underway to A, which will cancel the CloseSocket, and hence it will know to ignore the CloseSocket request from A. --- ChangeLog | 1 + src/Network/Transport.hs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index e050fd49..c5e92ee1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,7 @@ < Edsko de Vries 0.3.0 * Clarify disconnection +* Require that 'connect' be "as asynchronous as possible" 2012-07-16 Edsko de Vries 0.2.0.2 diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index d923b008..7974daa1 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -30,6 +30,7 @@ import Control.Exception (Exception) import Control.Applicative ((<$>)) import Data.Typeable (Typeable) import Data.Binary (Binary(get, put)) +import Data.Word (Word64) -------------------------------------------------------------------------------- -- Main API -- @@ -51,6 +52,11 @@ data EndPoint = EndPoint { -- | EndPointAddress of the endpoint. , address :: EndPointAddress -- | Create a new lightweight connection. + -- + -- 'connect' should be as asynchronous as possible; for instance, in + -- Transport implementations based on some heavy-weight underlying network + -- protocol (TCP, ssh), a call to 'connect' should be asynchronous when a + -- heavyweight connection has already been established. , connect :: EndPointAddress -> Reliability -> ConnectHints -> IO (Either (TransportError ConnectErrorCode) Connection) -- | Create a new multicast group. , newMulticastGroup :: IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) @@ -80,6 +86,8 @@ data Event = -- | Connection closed | ConnectionClosed ConnectionId -- | Connection opened + -- + -- 'ConnectionId's need not be allocated contiguously. | ConnectionOpened ConnectionId Reliability EndPointAddress -- | Received multicast | ReceivedMulticast MulticastAddress [ByteString] @@ -90,7 +98,7 @@ data Event = deriving (Show, Eq) -- | Connection data ConnectHintsIDs enable receivers to distinguish one connection from another. -type ConnectionId = Int +type ConnectionId = Word64 -- | Reliability guarantees of a connection. data Reliability = From 93765e7fd4fe37bf355da0931f7b9c250da06959 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 13:19:55 +0100 Subject: [PATCH 0216/2357] Switch to asynchronous connect In the original protocol we had recipient-allocated connection IDs. Hence, when doing a connect, we would send a connection request to the remote endpoint (provided that a heavyweight connection is already in place) and then wait for the recipient to reply with a new connection ID. This however makes connection much slower than it needs to be. We now use sender-allocated connection IDs, so that connect turns into a simple message to the remote endpoint that we allocated a new connection, and we no longer have to wait for a response. This means that all operations, with the sole exception of creating the heavyweight connection itself, are now synchronous. Hence, we have completely removed infrastructure relating to 'remote requests' (doRemoteRequest, ConnectionRequestResponse, etc.). What's not obvious is that we also had to change the 'CloseSocket' message. A sends a CloseSocket message to B when A thinks that the heavyweight connection to B is no longer required. A doesn't immediately close the socket, because it is possible that B has already sent a new connection request (now a CreatedNewConnection message) to A, even though A has not yet received it. Instead, it puts the heavyweight connection in 'Closing' state and waits for B to confirm or deny the request. When B receives A's CloseSocket message, it checks if it agrees that the heavyweight connection can be closed. If not, it simply ignores the request and A will forget it ever sent a CloseSocket request when it receives a CreatedNewConnection message from B (that is, it moves the endpoint from Closing to Valid state). If B does agree, then it will reply with a CloseSocket message, and then actually close the socket. When A receives a CloseSocket message having sent a CloseSocket message it closes the socket immediately. However, now that we have asynchronous connection requests we might have the following potential ordering of messages: A sends CloseSocket request, and puts endpoint in closing state B sends CreatedNewConnection, CloseConnection, and CloseSocket requests, putting its own representation of the endpoint also in closing state. B receives the CloseSocket message from A. Since it's endpoint is in Closing state, it immediately closes the socket. A receives B CreatedNewConnection message, hence *cancelling* the CloseSocket message it sent to be and putting the endpoint back in Valid state -- *even though* B did closed the socket. To avoid this problem, we now send the ID of the last created connection along with the close socket request. Then when B receives a CloseSocket request, it can compare this ID with the ID of the last connection it opened. If this does not match, B knows that there is still a CreatedNewConnection message underway to A, which will cancel the CloseSocket, and hence it will know to ignore the CloseSocket request from A. --- ChangeLog | 1 + src/Network/Transport/TCP.hs | 415 ++++++++++++++++++----------------- tests/TestTCP.hs | 336 ++++++++++++++-------------- 3 files changed, 375 insertions(+), 377 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0e8f159b..4c5b3222 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,7 @@ <> Edsko de Vries 0.3.0 * Implement new disconnection semantics +* Make 'connect' asynchronous (sender allocated connection IDs) 2012-08-20 Edsko de Vries 0.2.0.3 diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 29234426..d582d593 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -23,8 +23,10 @@ module Network.Transport.TCP , decodeEndPointAddress , ControlHeader(..) , ConnectionRequestResponse(..) - , firstNonReservedConnectionId + , firstNonReservedLightweightConnectionId + , firstNonReservedHeavyweightConnectionId , socketToEndPoint + , LightweightConnectionId -- * Design notes -- $design ) where @@ -45,7 +47,6 @@ import Network.Transport.TCP.Internal ) import Network.Transport.Internal ( encodeInt32 - , decodeInt32 , prependLength , mapIOException , tryIO @@ -79,7 +80,6 @@ import Control.Concurrent.MVar , modifyMVar , modifyMVar_ , readMVar - , takeMVar , putMVar , newEmptyMVar , withMVar @@ -103,11 +103,10 @@ import Data.IORef (IORef, newIORef, writeIORef, readIORef) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (concat) import qualified Data.ByteString.Char8 as BSC (pack, unpack) -import Data.Int (Int32) -import Data.IntMap (IntMap) -import qualified Data.IntMap as IntMap (empty) -import Data.IntSet (IntSet) -import qualified Data.IntSet as IntSet +import Data.Bits (shiftL, (.|.)) +import Data.Word (Word32) +import Data.Set (Set) +import qualified Data.Set as Set ( empty , insert , elems @@ -119,7 +118,7 @@ import qualified Data.IntSet as IntSet import Data.Map (Map) import qualified Data.Map as Map (empty) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) -import qualified Data.Accessor.Container as DAC (mapMaybe, intMapMaybe) +import qualified Data.Accessor.Container as DAC (mapMaybe) import Data.Foldable (forM_, mapM_) -- $design @@ -256,9 +255,9 @@ data LocalEndPointState = | LocalEndPointClosed data ValidLocalEndPointState = ValidLocalEndPointState - { _nextConnectionId :: !ConnectionId + { _nextConnOutId :: !LightweightConnectionId + , _nextConnInId :: !HeavyweightConnectionId , _localConnections :: !(Map EndPointAddress RemoteEndPoint) - , _nextRemoteId :: !Int } -- REMOTE ENDPOINTS @@ -278,14 +277,14 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- \-------+----------+--------> Failed -- -- Init: There are two places where we create new remote endpoints: in --- requestConnectionTo (in response to an API 'connect' call) and in +-- createConnectionTo (in response to an API 'connect' call) and in -- handleConnectionRequest (when a remote node tries to connect to us). -- 'Init' carries an MVar () 'resolved' which concurrent threads can use to -- wait for the remote endpoint to finish initialization. We record who -- requested the connection (the local endpoint or the remote endpoint). -- -- Invalid: We put the remote endpoint in invalid state only during --- requestConnectionTo when we fail to connect. +-- createConnectionTo when we fail to connect. -- -- Valid: This is the "normal" state for a working remote endpoint. -- @@ -346,7 +345,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState data RemoteEndPoint = RemoteEndPoint { remoteAddress :: !EndPointAddress , remoteState :: !(MVar RemoteState) - , remoteId :: !Int + , remoteId :: !HeavyweightConnectionId , remoteScheduled :: !(Chan (IO ())) } @@ -368,35 +367,43 @@ data RemoteState = -- using a closeTransport or closeEndPoint API call | RemoteEndPointFailed !IOException +-- TODO: we might want to replace Set (here and elsewhere) by faster +-- containers +-- +-- TODO: we could get rid of 'remoteIncoming' (and maintain less state) if +-- we introduce a new event 'AllConnectionsClosed' data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int - , _remoteIncoming :: !IntSet + , _remoteIncoming :: !(Set LightweightConnectionId) + , _remoteMaxIncoming :: !LightweightConnectionId , remoteSocket :: !N.Socket , remoteSendLock :: !(MVar ()) - , _pendingCtrlRequests :: !(IntMap (MVar (Either IOException [ByteString]))) - , _nextCtrlRequestId :: !ControlRequestId } -- | Local identifier for an endpoint within this transport -type EndPointId = Int32 - --- | Control request ID --- --- Control requests are asynchronous; the request ID makes it possible to match --- requests and replies -type ControlRequestId = Int32 +type EndPointId = Word32 -- | Pair of local and a remote endpoint (for conciseness in signatures) -type EndPointPair = (LocalEndPoint, RemoteEndPoint) +type EndPointPair = (LocalEndPoint, RemoteEndPoint) + +-- | Lightweight connection ID (sender allocated) +-- +-- A ConnectionId is the concentation of a 'HeavyweightConnectionId' and a +-- 'LightweightConnectionId'. +type LightweightConnectionId = Word32 + +-- | Heavyweight connection ID (recipient allocated) +-- +-- A ConnectionId is the concentation of a 'HeavyweightConnectionId' and a +-- 'LightweightConnectionId'. +type HeavyweightConnectionId = Word32 -- | Control headers data ControlHeader = - -- | Request a new connection ID from the remote endpoint - RequestConnectionId + -- | Tell the remote endpoint that we created a new connection + CreatedNewConnection -- | Tell the remote endpoint we will no longer be using a connection | CloseConnection - -- | Respond to a control request /from/ the remote endpoint - | ControlResponse -- | Request to close the connection (see module description) | CloseSocket deriving (Enum, Bounded, Show) @@ -558,7 +565,7 @@ apiConnect params ourEndPoint theirAddress _reliability hints = else do resetIfBroken ourEndPoint theirAddress (theirEndPoint, connId) <- - requestConnectionTo params ourEndPoint theirAddress hints + createConnectionTo params ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True @@ -568,7 +575,7 @@ apiConnect params ourEndPoint theirAddress _reliability hints = } -- | Close a connection -apiClose :: EndPointPair -> ConnectionId -> IORef Bool -> IO () +apiClose :: EndPointPair -> LightweightConnectionId -> IORef Bool -> IO () apiClose (ourEndPoint, theirEndPoint) connId connAlive = void . tryIO . asyncWhenCancelled return $ do mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of @@ -593,10 +600,10 @@ apiClose (ourEndPoint, theirEndPoint) connId connAlive = -- | Send data across a connection -apiSend :: EndPointPair -- ^ Local and remote endpoint - -> ConnectionId -- ^ Connection ID (supplied by remote endpoint) - -> IORef Bool -- ^ Is the connection still alive? - -> [ByteString] -- ^ Payload +apiSend :: EndPointPair -- ^ Local and remote endpoint + -> LightweightConnectionId -- ^ Connection ID + -> IORef Bool -- ^ Is the connection still alive? + -> [ByteString] -- ^ Payload -> IO (Either (TransportError SendErrorCode) ()) apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = -- We don't need the overhead of asyncWhenCancelled here @@ -665,7 +672,9 @@ apiCloseEndPoint transport evs ourEndPoint = return (closed, Nothing) RemoteEndPointValid vst -> do act <- schedule theirEndPoint $ do - tryIO $ sendOn vst [encodeInt32 CloseSocket] + tryIO $ sendOn vst [ encodeInt32 CloseSocket + , encodeInt32 (vst ^. remoteMaxIncoming) + ] tryCloseSocket (remoteSocket vst) return (closed, Just act) RemoteEndPointClosing resolved vst -> do @@ -729,12 +738,11 @@ handleConnectionRequest transport sock = handle handleException $ do else do sendLock <- newMVar () let vst = ValidRemoteEndPointState - { remoteSocket = sock - , remoteSendLock = sendLock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 + { remoteSocket = sock + , remoteSendLock = sendLock + , _remoteOutgoing = 0 + , _remoteIncoming = Set.empty + , _remoteMaxIncoming = 0 } sendMany sock [encodeInt32 ConnectionRequestAccepted] resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) @@ -790,32 +798,28 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- exception thrown by 'recv'. go :: N.Socket -> IO () go sock = do - connId <- recvInt32 sock - if connId >= firstNonReservedConnectionId + lcid <- recvInt32 sock :: IO LightweightConnectionId + if lcid >= firstNonReservedLightweightConnectionId then do - readMessage sock connId + readMessage sock lcid go sock else - case tryToEnum (fromIntegral connId) of - Just RequestConnectionId -> do - recvInt32 sock >>= createNewConnection + case tryToEnum (fromIntegral lcid) of + Just CreatedNewConnection -> do + recvInt32 sock >>= createdNewConnection go sock - Just ControlResponse -> do - recvInt32 sock >>= readControlResponse sock - go sock Just CloseConnection -> do recvInt32 sock >>= closeConnection go sock Just CloseSocket -> do - didClose <- closeSocket sock + didClose <- recvInt32 sock >>= closeSocket sock unless didClose $ go sock Nothing -> throwIO $ userError "Invalid control request" -- Create a new connection - createNewConnection :: ControlRequestId -> IO () - createNewConnection reqId = do - newId <- getNextConnectionId ourEndPoint + createdNewConnection :: LightweightConnectionId -> IO () + createdNewConnection lcid = do modifyMVar_ theirState $ \st -> do vst <- case st of RemoteEndPointInvalid _ -> @@ -825,73 +829,45 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection (init)" RemoteEndPointValid vst -> - return (remoteIncoming ^: IntSet.insert newId $ vst) + return ( (remoteIncoming ^: Set.insert lcid) + $ (remoteMaxIncoming ^= lcid) + vst + ) RemoteEndPointClosing resolved vst -> do -- If the endpoint is in closing state that means we send a -- CloseSocket request to the remote endpoint. If the remote - -- endpoint replies with the request to create a new connection, it - -- either ignored our request or it sent the request before it got - -- ours. Either way, at this point we simply restore the endpoint - -- to RemoteEndPointValid + -- endpoint replies that it created a new connection, it either + -- ignored our request or it sent the request before it got ours. + -- Either way, at this point we simply restore the endpoint to + -- RemoteEndPointValid putMVar resolved () - return (remoteIncoming ^= IntSet.singleton newId $ vst) + return ( (remoteIncoming ^= Set.singleton lcid) + . (remoteMaxIncoming ^= lcid) + $ vst + ) RemoteEndPointFailed err -> throwIO err RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "createNewConnection (closed)" - sendOn vst ( encodeInt32 ControlResponse - : encodeInt32 reqId - : prependLength [encodeInt32 newId] - ) return (RemoteEndPointValid vst) - writeChan ourChannel (ConnectionOpened newId ReliableOrdered theirAddr) - - -- Read a control response - readControlResponse :: N.Socket -> ControlRequestId -> IO () - readControlResponse sock reqId = do - response <- recvWithLength sock - mmvar <- modifyMVar theirState $ \st -> case st of - RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "readControlResponse (invalid)" - RemoteEndPointInit _ _ _ -> - relyViolation (ourEndPoint, theirEndPoint) - "readControlResponse (init)" - RemoteEndPointValid vst -> - return ( RemoteEndPointValid - . (pendingCtrlRequestsAt reqId ^= Nothing) - $ vst - , vst ^. pendingCtrlRequestsAt reqId - ) - RemoteEndPointClosing _ _ -> - throwIO $ userError "Invalid control response" - RemoteEndPointFailed err -> - throwIO err - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) - "readControlResponse (closed)" - case mmvar of - Nothing -> - throwIO $ userError "Invalid request ID" - Just mvar -> - putMVar mvar (Right response) + writeChan ourChannel (ConnectionOpened (connId lcid) ReliableOrdered theirAddr) -- Close a connection -- It is important that we verify that the connection is in fact open, -- because otherwise we should not decrement the reference count - closeConnection :: ConnectionId -> IO () - closeConnection cid = do + closeConnection :: LightweightConnectionId -> IO () + closeConnection lcid = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (invalid)" RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (init)" RemoteEndPointValid vst -> do - unless (IntSet.member cid (vst ^. remoteIncoming)) $ + unless (Set.member lcid (vst ^. remoteIncoming)) $ throwIO $ userError "Invalid CloseConnection" return ( RemoteEndPointValid - . (remoteIncoming ^: IntSet.delete cid) + . (remoteIncoming ^: Set.delete lcid) $ vst ) RemoteEndPointClosing _ _ -> @@ -903,13 +879,13 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do throwIO err RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (closed)" - writeChan ourChannel (ConnectionClosed cid) - closeIfUnused (ourEndPoint, theirEndPoint) + writeChan ourChannel (ConnectionClosed (connId lcid)) -- Close the socket (if we don't have any outgoing connections) - closeSocket :: N.Socket -> IO Bool - closeSocket sock = do - mAct <- modifyMVar theirState $ \st -> + closeSocket :: N.Socket -> LightweightConnectionId -> IO Bool + closeSocket sock lastReceivedId = do + mAct <- modifyMVar theirState $ \st -> do + lastSentId <- getLastConnOutId ourEndPoint case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) @@ -921,25 +897,44 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- We regard a CloseSocket message as an (optimized) way for the -- remote endpoint to indicate that all its connections to us are -- now properly closed - forM_ (IntSet.elems $ vst ^. remoteIncoming) $ - writeChan ourChannel . ConnectionClosed - let vst' = remoteIncoming ^= IntSet.empty $ vst - -- Check if we agree that the connection should be closed - if vst' ^. remoteOutgoing == 0 - then do + forM_ (Set.elems $ vst ^. remoteIncoming) $ + writeChan ourChannel . ConnectionClosed . connId + let vst' = remoteIncoming ^= Set.empty $ vst + -- If we still have outgoing connections then we ignore the + -- CloseSocket request (we sent a ConnectionCreated message to the + -- remote endpoint, but it did not receive it before sending the + -- CloseSocket request). Similarly, if lastReceivedId < lastSentId + -- then we sent a ConnectionCreated *AND* a ConnectionClosed + -- message to the remote endpoint, *both of which* it did not yet + -- receive before sending the CloseSocket request. + if vst' ^. remoteOutgoing > 0 || lastReceivedId < lastSentId + then + return (RemoteEndPointValid vst', Nothing) + else do removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- Attempt to reply (but don't insist) act <- schedule theirEndPoint $ do - tryIO $ sendOn vst' [encodeInt32 CloseSocket] + tryIO $ sendOn vst' [ encodeInt32 CloseSocket + , encodeInt32 (vst ^. remoteMaxIncoming) + ] tryCloseSocket sock return (RemoteEndPointClosed, Just act) - else - return (RemoteEndPointValid vst', Nothing) - RemoteEndPointClosing resolved _ -> do - removeRemoteEndPoint (ourEndPoint, theirEndPoint) - act <- schedule theirEndPoint $ tryCloseSocket sock - putMVar resolved () - return (RemoteEndPointClosed, Just act) + RemoteEndPointClosing resolved vst -> + -- Like above, we need to check if there is a ConnectionCreated + -- message that we sent but that the remote endpoint has not yet + -- received. However, since we are in 'closing' state, the only + -- way this may happen is when we sent a ConnectionCreated, + -- ConnectionClosed, and CloseSocket message, none of which have + -- yet been received. We leave the endpoint in closing state in + -- that case. + if lastReceivedId < lastSentId + then + return (RemoteEndPointClosing resolved vst, Nothing) + else do + removeRemoteEndPoint (ourEndPoint, theirEndPoint) + act <- schedule theirEndPoint $ tryCloseSocket sock + putMVar resolved () + return (RemoteEndPointClosed, Just act) RemoteEndPointFailed err -> throwIO err RemoteEndPointClosed -> @@ -950,13 +945,13 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do Just act -> do runScheduledAction (ourEndPoint, theirEndPoint) act return True - + -- Read a message and output it on the endPoint's channel. By rights we -- should verify that the connection ID is valid, but this is unnecessary -- overhead - readMessage :: N.Socket -> ConnectionId -> IO () - readMessage sock connId = - recvWithLength sock >>= writeChan ourChannel . Received connId + readMessage :: N.Socket -> LightweightConnectionId -> IO () + readMessage sock lcid = + recvWithLength sock >>= writeChan ourChannel . Received (connId lcid) -- Arguments ourChannel = localChannel ourEndPoint @@ -975,10 +970,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" - RemoteEndPointValid vst -> do + RemoteEndPointValid _ -> do let code = EventConnectionLost (remoteAddress theirEndPoint) writeChan ourChannel . ErrorEvent $ TransportError code (show err) - forM_ (vst ^. pendingCtrlRequests) $ flip putMVar (Left err) return (RemoteEndPointFailed err) RemoteEndPointClosing resolved _ -> do putMVar resolved () @@ -989,6 +983,10 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do RemoteEndPointFailed err' -> return (RemoteEndPointFailed err') + -- Construct a connection ID + connId :: LightweightConnectionId -> ConnectionId + connId = createConnectionId (remoteId theirEndPoint) + -------------------------------------------------------------------------------- -- Uninterruptable auxiliary functions -- -- -- @@ -996,19 +994,18 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- be killed. -------------------------------------------------------------------------------- --- | Request a connection to a remote endpoint +-- | Create a connection to a remote endpoint -- --- This will block until we get a connection ID from the remote endpoint; if --- the remote endpoint was in 'RemoteEndPointClosing' state then we will --- additionally block until that is resolved. +-- If the remote endpoint is in 'RemoteEndPointClosing' state then we will +-- block until that is resolved. -- -- May throw a TransportError ConnectErrorCode exception. -requestConnectionTo :: TCPParameters +createConnectionTo :: TCPParameters -> LocalEndPoint -> EndPointAddress -> ConnectHints - -> IO (RemoteEndPoint, ConnectionId) -requestConnectionTo params ourEndPoint theirAddress hints = go + -> IO (RemoteEndPoint, LightweightConnectionId) +createConnectionTo params ourEndPoint theirAddress hints = go where go = do (theirEndPoint, isNew) <- mapIOException connectFailed $ @@ -1020,9 +1017,26 @@ requestConnectionTo params ourEndPoint theirAddress hints = go setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints go else do - reply <- mapIOException connectFailed $ - doRemoteRequest (ourEndPoint, theirEndPoint) RequestConnectionId - return (theirEndPoint, decodeInt32 . BS.concat $ reply) + -- 'findRemoteEndPoint' will have increased 'remoteOutgoing' + mapIOException connectFailed $ do + act <- withMVar (remoteState theirEndPoint) $ \st -> case st of + RemoteEndPointValid vst -> do + connId <- getNextConnOutId ourEndPoint + schedule theirEndPoint $ do + sendOn vst [encodeInt32 CreatedNewConnection, encodeInt32 connId] + return connId + -- Error cases + RemoteEndPointInvalid err -> + throwIO err + RemoteEndPointFailed err -> + throwIO err + -- Algorithmic errors + _ -> + relyViolation (ourEndPoint, theirEndPoint) "createConnectionTo" + -- TODO: deal with exception case? + connId <- runScheduledAction (ourEndPoint, theirEndPoint) act + return (theirEndPoint, connId) + connectFailed :: IOException -> TransportError ConnectErrorCode connectFailed = TransportError ConnectFailed . show @@ -1042,12 +1056,11 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do Right (sock, ConnectionRequestAccepted) -> do sendLock <- newMVar () let vst = ValidRemoteEndPointState - { remoteSocket = sock - , remoteSendLock = sendLock - , _remoteOutgoing = 0 - , _remoteIncoming = IntSet.empty - , _pendingCtrlRequests = IntMap.empty - , _nextCtrlRequestId = 0 + { remoteSocket = sock + , remoteSendLock = sendLock + , _remoteOutgoing = 0 + , _remoteIncoming = Set.empty + , _remoteMaxIncoming = 0 } resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return True @@ -1076,56 +1089,25 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do theirAddress = remoteAddress theirEndPoint invalidAddress = TransportError ConnectNotFound --- | Do a (blocking) remote request --- --- May throw IO (user) exception if the local or the remote endpoint is closed, --- if the send fails, or if the remote endpoint fails before it replies. -doRemoteRequest :: EndPointPair -> ControlHeader -> IO [ByteString] -doRemoteRequest (ourEndPoint, theirEndPoint) header = do - replyMVar <- newEmptyMVar - act <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointValid vst -> do - let reqId = vst ^. nextCtrlRequestId - act <- schedule theirEndPoint $ sendOn vst [encodeInt32 header, encodeInt32 reqId] - return ( RemoteEndPointValid - . (nextCtrlRequestId ^: (+ 1)) - . (pendingCtrlRequestsAt reqId ^= Just replyMVar) - $ vst - , act - ) - -- Error cases - RemoteEndPointInvalid err -> - throwIO err - RemoteEndPointInit _ _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (init)" - RemoteEndPointClosing _ _ -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closing)" - RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) "doRemoteRequest (closed)" - RemoteEndPointFailed err -> - throwIO err - runScheduledAction (ourEndPoint, theirEndPoint) act - mReply <- takeMVar replyMVar - case mReply of - Left err -> throwIO err - Right reply -> return reply - -- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO () closeIfUnused (ourEndPoint, theirEndPoint) = do mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointValid vst -> - if vst ^. remoteOutgoing == 0 && IntSet.null (vst ^. remoteIncoming) + if vst ^. remoteOutgoing == 0 && Set.null (vst ^. remoteIncoming) then do resolved <- newEmptyMVar - act <- schedule theirEndPoint $ sendOn vst [encodeInt32 CloseSocket] + act <- schedule theirEndPoint $ + sendOn vst [ encodeInt32 CloseSocket + , encodeInt32 (vst ^. remoteMaxIncoming) + ] return (RemoteEndPointClosing resolved vst, Just act) else return (RemoteEndPointValid vst, Nothing) _ -> return (st, Nothing) forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) - + -- | Reset a remote endpoint if it is in Invalid mode -- -- If the remote endpoint is currently in broken state, and @@ -1160,7 +1142,8 @@ connectToSelf :: LocalEndPoint -> IO Connection connectToSelf ourEndPoint = do connAlive <- newIORef True -- Protected by the local endpoint lock - connId <- mapIOException connectFailed $ getNextConnectionId ourEndPoint + lconnId <- mapIOException connectFailed $ getNextConnOutId ourEndPoint + let connId = createConnectionId heavyweightSelfConnectionId lconnId writeChan ourChan $ ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) return Connection @@ -1214,21 +1197,33 @@ resolveInit (ourEndPoint, theirEndPoint) newState = _ -> relyViolation (ourEndPoint, theirEndPoint) "resolveInit" --- | Get the next connection ID +-- | Get the next outgoing connection ID -- -- Throws an IO exception when the endpoint is closed. -getNextConnectionId :: LocalEndPoint -> IO ConnectionId -getNextConnectionId ourEndpoint = +getNextConnOutId :: LocalEndPoint -> IO LightweightConnectionId +getNextConnOutId ourEndpoint = modifyMVar (localState ourEndpoint) $ \st -> case st of LocalEndPointValid vst -> do - let connId = vst ^. nextConnectionId + let connId = vst ^. nextConnOutId return ( LocalEndPointValid - . (nextConnectionId ^= connId + 1) + . (nextConnOutId ^= connId + 1) $ vst , connId) LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" +-- | The last outgoing connection ID we created, or zero if we never created any +getLastConnOutId :: LocalEndPoint -> IO LightweightConnectionId +getLastConnOutId ourEndPoint = + withMVar (localState ourEndPoint) $ \st -> case st of + LocalEndPointValid vst -> + let nextId = vst ^. nextConnOutId in + if nextId == firstNonReservedLightweightConnectionId + then return 0 + else return (nextId - 1) + LocalEndPointClosed -> + throwIO $ userError "Local endpoint closed" + -- | Create a new local endpoint -- -- May throw a TransportError NewEndPointErrorCode exception if the transport @@ -1237,9 +1232,9 @@ createLocalEndPoint :: TCPTransport -> IO LocalEndPoint createLocalEndPoint transport = do chan <- newChan state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState - { _nextConnectionId = firstNonReservedConnectionId - , _localConnections = Map.empty - , _nextRemoteId = 0 + { _nextConnOutId = firstNonReservedLightweightConnectionId + , _nextConnInId = firstNonReservedHeavyweightConnectionId + , _localConnections = Map.empty } modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> do @@ -1321,12 +1316,12 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress , remoteState = theirState - , remoteId = vst ^. nextRemoteId + , remoteId = vst ^. nextConnInId , remoteScheduled = scheduled } return ( LocalEndPointValid . (localConnectionTo theirAddress ^= Just theirEndPoint) - . (nextRemoteId ^: (+ 1)) + . (nextConnInId ^: (+ 1)) $ vst , (theirEndPoint, True) ) @@ -1504,6 +1499,13 @@ decodeEndPointAddress (EndPointAddress bs) = _ -> Nothing +-- | Construct a ConnectionId +createConnectionId :: HeavyweightConnectionId + -> LightweightConnectionId + -> ConnectionId +createConnectionId hcid lcid = + (fromIntegral hcid `shiftL` 32) .|. fromIntegral lcid + -- | @spltiMaxFromEnd p n xs@ splits list @xs@ at elements matching @p@, -- returning at most @p@ segments -- counting from the /end/ -- @@ -1564,8 +1566,16 @@ internalSocketBetween transport ourAddress theirAddress = do -------------------------------------------------------------------------------- -- | We reserve a bunch of connection IDs for control messages -firstNonReservedConnectionId :: ConnectionId -firstNonReservedConnectionId = 1024 +firstNonReservedLightweightConnectionId :: LightweightConnectionId +firstNonReservedLightweightConnectionId = 1024 + +-- | Self-connection +heavyweightSelfConnectionId :: HeavyweightConnectionId +heavyweightSelfConnectionId = 0 + +-- | We reserve some connection IDs for special heavyweight connections +firstNonReservedHeavyweightConnectionId :: HeavyweightConnectionId +firstNonReservedHeavyweightConnectionId = 1 -------------------------------------------------------------------------------- -- Accessor definitions -- @@ -1577,35 +1587,28 @@ localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es } nextEndPointId :: Accessor ValidTransportState EndPointId nextEndPointId = accessor _nextEndPointId (\eid st -> st { _nextEndPointId = eid }) -nextConnectionId :: Accessor ValidLocalEndPointState ConnectionId -nextConnectionId = accessor _nextConnectionId (\cix st -> st { _nextConnectionId = cix }) +nextConnOutId :: Accessor ValidLocalEndPointState LightweightConnectionId +nextConnOutId = accessor _nextConnOutId (\cix st -> st { _nextConnOutId = cix }) localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) -nextRemoteId :: Accessor ValidLocalEndPointState Int -nextRemoteId = accessor _nextRemoteId (\rid st -> st { _nextRemoteId = rid }) +nextConnInId :: Accessor ValidLocalEndPointState HeavyweightConnectionId +nextConnInId = accessor _nextConnInId (\rid st -> st { _nextConnInId = rid }) remoteOutgoing :: Accessor ValidRemoteEndPointState Int remoteOutgoing = accessor _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs }) -remoteIncoming :: Accessor ValidRemoteEndPointState IntSet +remoteIncoming :: Accessor ValidRemoteEndPointState (Set LightweightConnectionId) remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) -pendingCtrlRequests :: Accessor ValidRemoteEndPointState (IntMap (MVar (Either IOException [ByteString]))) -pendingCtrlRequests = accessor _pendingCtrlRequests (\rep st -> st { _pendingCtrlRequests = rep }) - -nextCtrlRequestId :: Accessor ValidRemoteEndPointState ControlRequestId -nextCtrlRequestId = accessor _nextCtrlRequestId (\cid st -> st { _nextCtrlRequestId = cid }) +remoteMaxIncoming :: Accessor ValidRemoteEndPointState LightweightConnectionId +remoteMaxIncoming = accessor _remoteMaxIncoming (\lcid st -> st { _remoteMaxIncoming = lcid }) localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr -pendingCtrlRequestsAt :: ControlRequestId -> Accessor ValidRemoteEndPointState (Maybe (MVar (Either IOException [ByteString]))) -pendingCtrlRequestsAt ix = pendingCtrlRequests >>> DAC.intMapMaybe (fromIntegral ix) - -localConnectionTo :: EndPointAddress - -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) +localConnectionTo :: EndPointAddress -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) localConnectionTo addr = localConnections >>> DAC.mapMaybe addr ------------------------------------------------------------------------------- diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 58f60a94..65eec7ad 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE RebindableSyntax #-} +{-# LANGUAGE RebindableSyntax, TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where @@ -17,6 +17,7 @@ import Network.Transport.TCP ( createTransport , TransportInternals(..) , encodeEndPointAddress , defaultTCPParameters + , LightweightConnectionId ) import Data.Int (Int32) import Control.Concurrent (threadDelay, killThread) @@ -28,6 +29,7 @@ import Control.Concurrent.MVar ( MVar , isEmptyMVar , newMVar , modifyMVar + , modifyMVar_ ) import Control.Monad (replicateM, guard, forM_, replicateM_, when) import Control.Applicative ((<$>)) @@ -149,21 +151,17 @@ testEarlyDisconnect nextPort = do _ <- recvWithLength sock sendMany sock [encodeInt32 ConnectionRequestAccepted] - -- Server requests a logical connection - RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) - reqId <- recvInt32 sock :: IO Int - sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + -- Server opens a logical connection + CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) + 1024 <- recvInt32 sock :: IO LightweightConnectionId -- Server sends a message - 10001 <- recvInt32 sock :: IO Int + 1024 <- recvInt32 sock :: IO Int ["ping"] <- recvWithLength sock -- Reply - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] - ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) - 10002 <- recvInt32 sock :: IO Int - [cid] <- recvWithLength sock - sendMany sock (cid : prependLength ["pong"]) + sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10002 :: Int)] + sendMany sock (encodeInt32 10002 : prependLength ["pong"]) -- Close the socket N.sClose sock @@ -171,9 +169,8 @@ testEarlyDisconnect nextPort = do -- Connect to the server Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - -- Request a new connection, but don't wait for the response - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + -- Open a new connection + sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10003 :: Int)] -- Close the socket without closing the connection explicitly -- The server should receive an error event @@ -260,37 +257,32 @@ testEarlyCloseSocket nextPort = do _ <- recvWithLength sock sendMany sock [encodeInt32 ConnectionRequestAccepted] - -- Server requests a logical connection - RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) - reqId <- recvInt32 sock :: IO Int - sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + -- Server opens a logical connection + CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) + 1024 <- recvInt32 sock :: IO LightweightConnectionId -- Server sends a message - 10001 <- recvInt32 sock :: IO Int + 1024 <- recvInt32 sock :: IO Int ["ping"] <- recvWithLength sock -- Reply - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 (10002 :: Int)] - ControlResponse <- toEnum <$> (recvInt32 sock :: IO Int) - 10002 <- recvInt32 sock :: IO Int - [cid] <- recvWithLength sock - sendMany sock (cid : prependLength ["pong"]) + sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10002 :: Int)] + sendMany sock (encodeInt32 (10002 :: Int) : prependLength ["pong"]) -- Send a CloseSocket even though there are still connections *in both -- directions* - sendMany sock [encodeInt32 CloseSocket] + sendMany sock [encodeInt32 CloseSocket, encodeInt32 (1024 :: Int)] N.sClose sock -- Connect to the server Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - -- Request a new connection, but don't wait for the response - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] + -- Open a new connection + sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10003 :: Int)] -- Send a CloseSocket without sending a closeconnecton -- The server should still receive a ConnectionClosed message - sendMany sock [encodeInt32 CloseSocket] + sendMany sock [encodeInt32 CloseSocket, encodeInt32 (0 :: Int)] N.sClose sock -- | Test the creation of a transport with an invalid address @@ -329,159 +321,160 @@ testInvalidConnect nextPort = do -- receiving an (already underway) CloseSocket request) testIgnoreCloseSocket :: IO N.ServiceName -> IO () testIgnoreCloseSocket nextPort = do - serverAddr <- newEmptyMVar - clientDone <- newEmptyMVar - Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - - forkTry $ server transport serverAddr - forkTry $ client transport serverAddr clientDone - - takeMVar clientDone + serverAddr <- newEmptyMVar + clientAddr <- newEmptyMVar + clientDone <- newEmptyMVar + serverDone <- newEmptyMVar + connectionEstablished <- newEmptyMVar + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - where - server :: Transport -> MVar EndPointAddress -> IO () - server transport serverAddr = do - tlog "Server" - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) + -- Server + forkTry $ do + tlog "Server" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) - -- Wait for the client to connect and disconnect - tlog "Waiting for ConnectionOpened" - ConnectionOpened _ _ _ <- receive endpoint - tlog "Waiting for ConnectionClosed" - ConnectionClosed _ <- receive endpoint + let ourAddress = address endpoint + theirAddress <- readMVar clientAddr - -- At this point the server will have sent a CloseSocket request to the - -- client, which however ignores it, instead it requests and closes - -- another connection - tlog "Waiting for ConnectionOpened" - ConnectionOpened _ _ _ <- receive endpoint - tlog "Waiting for ConnectionClosed" - ConnectionClosed _ <- receive endpoint + -- Wait for the client to set up the TCP connection to us + takeMVar connectionEstablished - - tlog "Server waiting.." + -- Connect then disconnect to the client + Right conn <- connect endpoint theirAddress ReliableOrdered defaultConnectHints + close conn - client :: Transport -> MVar EndPointAddress -> MVar () -> IO () - client transport serverAddr clientDone = do - tlog "Client" - Right endpoint <- newEndPoint transport - let ourAddress = address endpoint + -- At this point the server will have sent a CloseSocket request to the + -- client, which however ignores it, instead it requests and closes + -- another connection + tlog "Waiting for ConnectionOpened" + ConnectionOpened _ _ _ <- receive endpoint + tlog "Waiting for ConnectionClosed" + ConnectionClosed _ <- receive endpoint - -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + putMVar serverDone () - -- Request a new connection - tlog "Requesting connection" - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] - response <- replicateM 4 $ recvInt32 sock :: IO [Int32] - - -- Close the connection again - tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] - - -- Server will now send a CloseSocket request as its refcount reached 0 - tlog "Waiting for CloseSocket request" - CloseSocket <- toEnum <$> recvInt32 sock - - -- But we ignore it and request another connection - tlog "Ignoring it, requesting another connection" - let reqId' = 1 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] - response' <- replicateM 4 $ recvInt32 sock :: IO [Int32] - - -- Close it again - tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response' !! 3)] - - -- We now get a CloseSocket again, and this time we heed it - tlog "Waiting for second CloseSocket request" - CloseSocket <- toEnum <$> recvInt32 sock - - tlog "Closing socket" - sendMany sock [encodeInt32 CloseSocket] - N.sClose sock + -- Client + forkTry $ do + tlog "Client" + Right endpoint <- newEndPoint transport + putMVar clientAddr (address endpoint) - putMVar clientDone () + let ourAddress = address endpoint + theirAddress <- readMVar serverAddr + + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- socketToEndPoint ourAddress theirAddress True Nothing + putMVar connectionEstablished () + + -- Server connects to us, and then closes the connection + CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) + 1024 <- recvInt32 sock :: IO LightweightConnectionId + + CloseConnection <- toEnum <$> (recvInt32 sock :: IO Int) + 1024 <- recvInt32 sock :: IO LightweightConnectionId + + -- Server will now send a CloseSocket request as its refcount reached 0 + tlog "Waiting for CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + _ <- recvInt32 sock :: IO LightweightConnectionId + + -- But we ignore it and request another connection in the other direction + tlog "Ignoring it, requesting another connection" + sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (1024 :: Int)] + + -- Close it again + tlog "Closing connection" + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (1024 :: Int)] + + -- And close the connection completely + tlog "Closing socket" + sendMany sock [encodeInt32 CloseSocket, encodeInt32 (1024 :: Int)] + N.sClose sock + + putMVar clientDone () + + takeMVar clientDone + takeMVar serverDone -- | Like 'testIgnoreSocket', but now the server requests a connection after the -- client closed their connection. In the meantime, the server will have sent a -- CloseSocket request to the client, and must block until the client responds. testBlockAfterCloseSocket :: IO N.ServiceName -> IO () testBlockAfterCloseSocket nextPort = do - serverAddr <- newEmptyMVar - clientAddr <- newEmptyMVar - clientDone <- newEmptyMVar - port <- nextPort - Right transport <- createTransport "127.0.0.1" port defaultTCPParameters - - forkTry $ server transport serverAddr clientAddr - forkTry $ client transport serverAddr clientAddr clientDone - - takeMVar clientDone + serverAddr <- newEmptyMVar + clientAddr <- newEmptyMVar + clientDone <- newEmptyMVar + serverDone <- newEmptyMVar + connectionEstablished <- newEmptyMVar + Right transport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - where - server :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> IO () - server transport serverAddr clientAddr = do - tlog "Server" - Right endpoint <- newEndPoint transport - putMVar serverAddr (address endpoint) + -- Server + forkTry $ do + tlog "Server" + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) - -- Wait for the client to connect and disconnect - tlog "Waiting for ConnectionOpened" - ConnectionOpened _ _ _ <- receive endpoint - tlog "Waiting for ConnectionClosed" - ConnectionClosed _ <- receive endpoint - - -- At this point the server will have sent a CloseSocket request to the - -- client, and must block until the client responds - tlog "Server waiting to connect to the client.." - Right _ <- readMVar clientAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - - tlog "Server waiting.." + let ourAddress = address endpoint + theirAddress <- readMVar clientAddr - client :: Transport -> MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () - client transport serverAddr clientAddr clientDone = do - tlog "Client" - Right endpoint <- newEndPoint transport - putMVar clientAddr (address endpoint) - let ourAddress = address endpoint + -- Wait for the client to set up the TCP connection to us + takeMVar connectionEstablished - -- Connect to the server - Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + -- Connect then disconnect to the client + Right conn <- connect endpoint theirAddress ReliableOrdered defaultConnectHints + close conn - -- Request a new connection - tlog "Requesting connection" - let reqId = 0 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId] - response <- replicateM 4 $ recvInt32 sock :: IO [Int32] + -- At this point the server will have sent a CloseSocket request to the + -- client, and must block until the client responds + Right conn <- connect endpoint theirAddress ReliableOrdered defaultConnectHints - -- Close the connection again - tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (response !! 3)] + putMVar serverDone () - -- Server will now send a CloseSocket request as its refcount reached 0 - tlog "Waiting for CloseSocket request" - CloseSocket <- toEnum <$> recvInt32 sock + -- Client + forkTry $ do + tlog "Client" + Right endpoint <- newEndPoint transport + putMVar clientAddr (address endpoint) - unblocked <- newEmptyMVar + let ourAddress = address endpoint + theirAddress <- readMVar serverAddr - -- We should not hear from the server until we unblock him by - -- responding to the CloseSocket request (in this case, we - -- respond by sending a ConnectionRequest) - forkTry $ do - recvInt32 sock :: IO Int32 - isEmptyMVar unblocked >>= (guard . not) - putMVar clientDone () + -- Connect to the server + Right (sock, ConnectionRequestAccepted) <- socketToEndPoint ourAddress theirAddress True Nothing + putMVar connectionEstablished () + + -- Server connects to us, and then closes the connection + CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) + 1024 <- recvInt32 sock :: IO LightweightConnectionId - threadDelay 1000000 + CloseConnection <- toEnum <$> (recvInt32 sock :: IO Int) + 1024 <- recvInt32 sock :: IO LightweightConnectionId - tlog "Client ignores close socket and sends connection request" - tlog "This should unblock the server" - putMVar unblocked () - let reqId' = 1 :: Int32 - sendMany sock [encodeInt32 RequestConnectionId, encodeInt32 reqId'] + -- Server will now send a CloseSocket request as its refcount reached 0 + tlog "Waiting for CloseSocket request" + CloseSocket <- toEnum <$> recvInt32 sock + _ <- recvInt32 sock :: IO LightweightConnectionId + + unblocked <- newMVar False + + -- We should not hear from the server until we unblock him by + -- responding to the CloseSocket request (in this case, we + -- respond by sending a ConnectionRequest) + forkTry $ do + recvInt32 sock :: IO Int32 + readMVar unblocked >>= guard + putMVar clientDone () + + threadDelay 1000000 + + tlog "Client ignores close socket and sends connection request" + tlog "This should unblock the server" + modifyMVar_ unblocked $ \_ -> return True + sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (1024 :: Int)] + + takeMVar clientDone + takeMVar serverDone -- | Test what happens when a remote endpoint sends a connection request to our -- transport for an endpoint it already has a connection to @@ -592,16 +585,16 @@ testReconnect nextPort = do when (count > 0) $ do -- Client requests a logical connection - Right RequestConnectionId <- tryIO $ toEnum <$> (recvInt32 sock :: IO Int) - Right reqId <- tryIO $ (recvInt32 sock :: IO Int) - Right () <- tryIO $ sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + Right CreatedNewConnection <- tryIO $ toEnum <$> (recvInt32 sock :: IO Int) + connId <- recvInt32 sock :: IO LightweightConnectionId return () - when (count > 1) $ do - -- Client sends a message - Right 10001 <- tryIO $ (recvInt32 sock :: IO Int) - Right ["ping"] <- tryIO $ recvWithLength sock - putMVar serverDone () + when (count > 1) $ do + -- Client sends a message + Right connId' <- tryIO $ (recvInt32 sock :: IO LightweightConnectionId) + True <- return $ connId == connId' + Right ["ping"] <- tryIO $ recvWithLength sock + putMVar serverDone () Right () <- tryIO $ N.sClose sock return () @@ -615,7 +608,7 @@ testReconnect nextPort = do let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 -- The first attempt will fail because no endpoint is yet set up - -- Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + Left (TransportError ConnectNotFound _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints putMVar firstAttempt () -- The second attempt will fail because the server closes the socket before we can request a connection @@ -633,6 +626,7 @@ testReconnect nextPort = do Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- But a send will fail because the server has closed the connection again + threadDelay 100000 Left (TransportError SendFailed _) <- send conn1 ["ping"] ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint @@ -667,11 +661,11 @@ testUnidirectionalError nextPort = do _ <- recvWithLength sock () <- sendMany sock [encodeInt32 ConnectionRequestAccepted] - RequestConnectionId <- toEnum <$> (recvInt32 sock :: IO Int) - reqId <- recvInt32 sock :: IO Int - sendMany sock (encodeInt32 ControlResponse : encodeInt32 reqId : prependLength [encodeInt32 (10001 :: Int)]) + CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) + connId <- recvInt32 sock :: IO LightweightConnectionId - 10001 <- recvInt32 sock :: IO Int + connId' <- recvInt32 sock :: IO LightweightConnectionId + True <- return $ connId == connId' ["ping"] <- recvWithLength sock putMVar serverGotPing () @@ -777,7 +771,7 @@ main = do , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) , ("BlockAfterCloseSocket", testBlockAfterCloseSocket nextPort) - , ("TestUnnecessaryConnect", testUnnecessaryConnect nextPort 10) + , ("UnnecessaryConnect", testUnnecessaryConnect nextPort 10) , ("InvalidAddress", testInvalidAddress nextPort) , ("InvalidConnect", testInvalidConnect nextPort) , ("Many", testMany nextPort) From d986e4978b85f6cbaf7ef9cc3ab47d5c4d7bd15d Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 17 Sep 2012 13:19:55 +0100 Subject: [PATCH 0217/2357] Switch to asynchronous connect In the original protocol we had recipient-allocated connection IDs. Hence, when doing a connect, we would send a connection request to the remote endpoint (provided that a heavyweight connection is already in place) and then wait for the recipient to reply with a new connection ID. This however makes connection much slower than it needs to be. We now use sender-allocated connection IDs, so that connect turns into a simple message to the remote endpoint that we allocated a new connection, and we no longer have to wait for a response. This means that all operations, with the sole exception of creating the heavyweight connection itself, are now synchronous. Hence, we have completely removed infrastructure relating to 'remote requests' (doRemoteRequest, ConnectionRequestResponse, etc.). What's not obvious is that we also had to change the 'CloseSocket' message. A sends a CloseSocket message to B when A thinks that the heavyweight connection to B is no longer required. A doesn't immediately close the socket, because it is possible that B has already sent a new connection request (now a CreatedNewConnection message) to A, even though A has not yet received it. Instead, it puts the heavyweight connection in 'Closing' state and waits for B to confirm or deny the request. When B receives A's CloseSocket message, it checks if it agrees that the heavyweight connection can be closed. If not, it simply ignores the request and A will forget it ever sent a CloseSocket request when it receives a CreatedNewConnection message from B (that is, it moves the endpoint from Closing to Valid state). If B does agree, then it will reply with a CloseSocket message, and then actually close the socket. When A receives a CloseSocket message having sent a CloseSocket message it closes the socket immediately. However, now that we have asynchronous connection requests we might have the following potential ordering of messages: A sends CloseSocket request, and puts endpoint in closing state B sends CreatedNewConnection, CloseConnection, and CloseSocket requests, putting its own representation of the endpoint also in closing state. B receives the CloseSocket message from A. Since it's endpoint is in Closing state, it immediately closes the socket. A receives B CreatedNewConnection message, hence *cancelling* the CloseSocket message it sent to be and putting the endpoint back in Valid state -- *even though* B did closed the socket. To avoid this problem, we now send the ID of the last created connection along with the close socket request. Then when B receives a CloseSocket request, it can compare this ID with the ID of the last connection it opened. If this does not match, B knows that there is still a CreatedNewConnection message underway to A, which will cancel the CloseSocket, and hence it will know to ignore the CloseSocket request from A. --- src/Network/Transport/Tests.hs | 6 ++++-- src/Network/Transport/Tests/Traced.hs | 12 +++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Network/Transport/Tests.hs b/src/Network/Transport/Tests.hs index 1a6141dd..c5394b87 100644 --- a/src/Network/Transport/Tests.hs +++ b/src/Network/Transport/Tests.hs @@ -691,12 +691,14 @@ testCloseTransport newTransport = do send conn ["pong"] -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) - evs <- replicateM 3 $ receive endpoint + -- TODO: should we get an EventConnectionLost for theirAddr1? We have no outgoing connections + evs <- replicateM 4 $ receive endpoint let expected = [ ConnectionClosed cid1 , ConnectionClosed cid2 + , ErrorEvent (TransportError (EventConnectionLost theirAddr1) "") , ErrorEvent (TransportError (EventConnectionLost theirAddr2) "") ] - True <- return $ any (== expected) (permutations evs) + True <- return $ expected `elem` permutations evs -- An attempt to send to the endpoint should now fail Left (TransportError SendFailed _) <- send conn ["pong2"] diff --git a/src/Network/Transport/Tests/Traced.hs b/src/Network/Transport/Tests/Traced.hs index f0d8d834..69e2e4ed 100644 --- a/src/Network/Transport/Tests/Traced.hs +++ b/src/Network/Transport/Tests/Traced.hs @@ -66,7 +66,8 @@ import Control.Applicative ((<$>)) import Data.Typeable (Typeable) import Data.Maybe (catMaybes) import Data.ByteString (ByteString) -import Data.Int (Int32) +import Data.Int (Int32, Int64) +import Data.Word (Word32, Word64) import Control.Concurrent.MVar (MVar) -------------------------------------------------------------------------------- @@ -152,6 +153,15 @@ instance Traceable Int where instance Traceable Int32 where trace = traceShow +instance Traceable Int64 where + trace = traceShow + +instance Traceable Word32 where + trace = traceShow + +instance Traceable Word64 where + trace = traceShow + instance Traceable Bool where trace = const Nothing From 7ad3ad1b840422492bbd24c47a22fc607e8aa15f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 19 Sep 2012 09:34:41 +0100 Subject: [PATCH 0218/2357] Fix leak in N.T.TCP by making ConnectionId strict --- src/Network/Transport.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index 7974daa1..ef39fd9e 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -82,13 +82,13 @@ data Connection = Connection { -- | Event on an endpoint. data Event = -- | Received a message - Received ConnectionId [ByteString] + Received {-# UNPACK #-} !ConnectionId [ByteString] -- | Connection closed - | ConnectionClosed ConnectionId + | ConnectionClosed {-# UNPACK #-} !ConnectionId -- | Connection opened -- -- 'ConnectionId's need not be allocated contiguously. - | ConnectionOpened ConnectionId Reliability EndPointAddress + | ConnectionOpened {-# UNPACK #-} !ConnectionId Reliability EndPointAddress -- | Received multicast | ReceivedMulticast MulticastAddress [ByteString] -- | The endpoint got closed (manually, by a call to closeEndPoint or closeTransport) From 0df0776e276839bbe9b6ec7a2e43d06ff6891106 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 24 Sep 2012 18:20:49 +0100 Subject: [PATCH 0219/2357] Avoid name clash. Clarify error message. --- src/Control/Distributed/Process/Backend/Azure.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index c087fd1d..53a82b15 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -792,7 +792,7 @@ localExpect = LocalProcess $ do isE <- readIntChannel ch len <- readIntChannel ch lenAgain <- readIntChannel ch - when (len /= lenAgain) $ throwIO (userError "Protocol violation") + when (len /= lenAgain) $ throwIO (userError "Internal error: protocol violation (perhaps the remote binary is not installed correctly?)") msg <- readSizeChannel ch len if isE /= 0 then error (decode msg) @@ -939,7 +939,7 @@ instance Binary ServiceProcessMsg where serviceProcess :: Backend -> Process () serviceProcess _backend = do us <- getSelfPid - register "serviceProcess" us + register "$azureBackendServiceProcess" us go where go = do @@ -958,7 +958,7 @@ spawnNodeOnVM backend vm port = -- | Terminate a node started with 'spawnNodeOnVM' terminateNode :: NodeId -> Process () -terminateNode nid = nsendRemote nid "serviceProcess" ServiceProcessTerminate +terminateNode nid = nsendRemote nid "$azureBackendServiceProcess" ServiceProcessTerminate __remoteTable :: RemoteTable -> RemoteTable __remoteTable = registerStatic "serviceProcess" (toDynamic serviceProcess) From 0932d3fdf0ce6127b0913a8f95ef32e3d9bb29ca Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 27 Sep 2012 16:02:51 +0100 Subject: [PATCH 0220/2357] Relax lower bound. Up version to 0.1.0.1 --- ChangeLog | 7 +++++++ rank1dynamic.cabal | 4 ++-- src/Data/Rank1Typeable.hs | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..b85caa2c --- /dev/null +++ b/ChangeLog @@ -0,0 +1,7 @@ +2012-09-27 Edsko de Vries 0.1.0.1 + +* Relax lower bound of base to 4.4 (ghc 7.2) + +2012-08-10 Edsko de Vries 0.1.0.0 + +* Initial release. diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index 1d6c8f19..9cb22756 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -1,5 +1,5 @@ Name: rank1dynamic -Version: 0.1.0.0 +Version: 0.1.0.1 Synopsis: Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types Description: "Data.Typeable" and "Data.Dynamic" only support monomorphic types. In this package we provide similar functionality but with @@ -17,7 +17,7 @@ Cabal-Version: >=1.8 Library Exposed-Modules: Data.Rank1Dynamic, Data.Rank1Typeable - Build-Depends: base >= 4.5 && < 4.7, + Build-Depends: base >= 4.4 && < 4.7, ghc-prim >= 0.2 && < 0.4, binary >= 0.5 && < 0.6 HS-Source-Dirs: src diff --git a/src/Data/Rank1Typeable.hs b/src/Data/Rank1Typeable.hs index fff8148c..75a7c8ab 100644 --- a/src/Data/Rank1Typeable.hs +++ b/src/Data/Rank1Typeable.hs @@ -101,8 +101,8 @@ import Control.Monad (void) import Control.Applicative ((<$>)) import Data.List (intersperse, isPrefixOf) import Data.Maybe (fromMaybe) -import Data.Typeable (Typeable, tyConName, mkTyCon3) -import Data.Typeable.Internal (listTc, funTc, TyCon(TyCon)) +import Data.Typeable (Typeable, mkTyCon3) +import Data.Typeable.Internal (listTc, funTc, TyCon(TyCon), tyConName) import Data.Binary (Binary(get, put)) import GHC.Fingerprint.Type (Fingerprint(..)) import qualified Data.Typeable as Typeable From d7865f6c11567cda332bf1954743d04f4ef00d87 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 27 Sep 2012 16:18:52 +0100 Subject: [PATCH 0221/2357] Support staticFlip. --- ChangeLog | 4 ++++ distributed-static.cabal | 2 +- src/Control/Distributed/Static.hs | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 133c7c8f..38153a45 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +<> Edsko de Vries 0.2.1 + +* Add support for 'staticFlip' + 2012-08-16 Edsko de Vries 0.2 * Hide the 'Closure' constructor and export 'closure' instead so that we are diff --git a/distributed-static.cabal b/distributed-static.cabal index 037363d2..9f6634a8 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -1,5 +1,5 @@ Name: distributed-static -Version: 0.2.0.0 +Version: 0.2.1 Synopsis: Compositional, type-safe, polymorphic static values and closures Description: /Towards Haskell in the Cloud/ (Epstein et al, Haskell Symposium 2011) introduces the concept of /static/ values: diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 3dfce7ce..53cae039 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -205,6 +205,7 @@ module Control.Distributed.Static , staticCompose , staticSplit , staticConst + , staticFlip -- * Closures , Closure , closure @@ -310,6 +311,7 @@ initRemoteTable = . registerStatic "$split" (toDynamic ((***) :: (ANY1 -> ANY3) -> (ANY2 -> ANY4) -> (ANY1, ANY2) -> (ANY3, ANY4))) . registerStatic "$app" (toDynamic (app :: (ANY1 -> ANY2, ANY1) -> ANY2)) . registerStatic "$decodeEnvPair" (toDynamic (decode :: ByteString -> (ByteString, ByteString))) + . registerStatic "$flip" (toDynamic (flip :: (ANY1 -> ANY2 -> ANY3) -> ANY2 -> ANY1 -> ANY3)) $ RemoteTable Map.empty -- | Register a static label @@ -385,6 +387,11 @@ appStatic :: (Typeable a, Typeable b) => Static ((a -> b, a) -> b) appStatic = staticLabel "$app" +-- | Static version of 'flip' +flipStatic :: (Typeable a, Typeable b, Typeable c) + => Static ((a -> b -> c) -> b -> a -> c) +flipStatic = staticLabel "$flip" + -------------------------------------------------------------------------------- -- Combinators on static values -- -------------------------------------------------------------------------------- @@ -404,6 +411,11 @@ staticConst :: (Typeable a, Typeable b) => Static a -> Static (b -> a) staticConst x = constStatic `staticApply` x +-- | Static version of 'Prelude.flip' +staticFlip :: (Typeable a, Typeable b, Typeable c) + => Static (a -> b -> c) -> Static (b -> a -> c) +staticFlip f = flipStatic `staticApply` f + -------------------------------------------------------------------------------- -- Combinators on Closures -- -------------------------------------------------------------------------------- From 852ac131102d1f94cdfc8a95d4295b4fca5b0e49 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 28 Sep 2012 09:30:26 +0100 Subject: [PATCH 0222/2357] Cleanup. Relax upper bound on network. --- network-transport-tcp.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 7516be69..e89cc22f 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -26,7 +26,7 @@ Library data-accessor >= 0.2 && < 0.3, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, - network >= 2.3 && < 2.4 + network >= 2.3 && < 2.5 Exposed-modules: Network.Transport.TCP, Network.Transport.TCP.Internal Extensions: CPP @@ -38,7 +38,7 @@ Test-Suite TestTCP Main-Is: TestTCP.hs Build-Depends: base >= 4.3 && < 5, network-transport-tests >= 0.1 && < 0.2, - network >= 2.3 && < 2.4, + network >= 2.3 && < 2.5, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4 ghc-options: -threaded -rtsopts -with-rtsopts=-N From 65a0a221f569f9a4193ad5d0c6f4d15ba8af9d57 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 28 Sep 2012 14:12:15 +0100 Subject: [PATCH 0223/2357] Support network-2.4.0 --- distributed-process-simplelocalnet.cabal | 3 ++- .../Process/Backend/SimpleLocalnet/Internal/Multicast.hs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 879957f9..6730fe73 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -38,7 +38,8 @@ Library Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast Extensions: RankNTypes, - DeriveDataTypeable + DeriveDataTypeable, + CPP ghc-options: -Wall HS-Source-Dirs: src diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs index db91d6a1..01b3e84c 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs @@ -61,10 +61,12 @@ initMulticast host port bufferSize = do type UDPState = Map SockAddr BSL.ByteString --- TODO: This is inefficient and an orphan instance. --- Requested official instance (https://github.com/haskell/network/issues/38) +#if MIN_VERSION_network(2,4,0) +-- network-2.4.0 provides the Ord instance for us +#else instance Ord SockAddr where compare = compare `on` show +#endif bufferFor :: SockAddr -> Accessor UDPState BSL.ByteString bufferFor = DAC.mapDefault BSL.empty From 2ccc33c1bc7a5ee6dd5a390205402779b2cdcb44 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 28 Sep 2012 14:18:04 +0100 Subject: [PATCH 0224/2357] Whoops. Committed wrong version of the .cabal file --- ChangeLog | 1 + distributed-process-simplelocalnet.cabal | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0197da7d..2e45c22e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,7 @@ <> Edsko de Vries 0.2.0.6 * Use new version of network-transport +* network-2.4.0 compatibility 2012-08-22 Edsko de Vries 0.2.0.5 diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 6730fe73..1bfaa605 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -26,7 +26,7 @@ Source-Repository head Library Build-Depends: base >= 4.4 && < 5, bytestring >= 0.9 && < 0.11, - network >= 2.3 && < 2.4, + network >= 2.3 && < 2.5, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, binary >= 0.5 && < 0.6, @@ -49,7 +49,7 @@ Test-Suite TestSimpleLocalnet Main-Is: TestSimpleLocalnet.hs Build-Depends: base >= 4.4 && < 5, bytestring >= 0.9 && < 0.11, - network >= 2.3 && < 2.4, + network >= 2.3 && < 2.5, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, binary >= 0.5 && < 0.6, From dcf060eb2d5239a92ee734bb6a9410f345f05762 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 2 Oct 2012 10:09:26 +0100 Subject: [PATCH 0225/2357] smallChunkSize instead of 4kB to avoid overhead --- src/Network/Transport/TCP/Internal.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs index d084285e..3aa73878 100644 --- a/src/Network/Transport/TCP/Internal.hs +++ b/src/Network/Transport/TCP/Internal.hs @@ -38,6 +38,7 @@ import Control.Applicative ((<$>)) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length, concat, null) import Data.Int (Int32) +import Data.ByteString.Lazy.Internal (smallChunkSize) -- | Start a server at the specified address. -- @@ -115,7 +116,7 @@ recvExact sock len = go [] len go :: [ByteString] -> Int32 -> IO [ByteString] go acc 0 = return (reverse acc) go acc l = do - bs <- NBS.recv sock (fromIntegral l `min` 4096) + bs <- NBS.recv sock (fromIntegral l `min` smallChunkSize) if BS.null bs then throwIO (userError "recvExact: Socket closed") else go (bs : acc) (l - fromIntegral (BS.length bs)) From 4510da63fc6bf0d96f5683937542cf2293ea30ab Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 3 Oct 2012 14:54:05 +0100 Subject: [PATCH 0226/2357] Update ChangeLog for release 0.3.0. --- ChangeLog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index c5e92ee1..1a8a11d8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,8 @@ -< Edsko de Vries 0.3.0 +2012-10-03 Edsko de Vries 0.3.0 * Clarify disconnection * Require that 'connect' be "as asynchronous as possible" +* Added strictness annotations 2012-07-16 Edsko de Vries 0.2.0.2 From 756062874da07d0c37c1e909f35ec2ac88cc4b08 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 3 Oct 2012 14:58:34 +0100 Subject: [PATCH 0227/2357] Add ChangeLog for release 0.1.0. --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..4a414f5d --- /dev/null +++ b/ChangeLog @@ -0,0 +1,3 @@ +2012-10-03 Edsko de Vries 0.1.0 + +* Initial release (these tests used to be part of the individual transports) From e1c94ff7b0b9b1a5f64781d11f3cac70a3ad51aa Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 3 Oct 2012 15:02:44 +0100 Subject: [PATCH 0228/2357] Update ChangeLog for release 0.3.0 --- ChangeLog | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 4c5b3222..17325a5e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,11 @@ -<> Edsko de Vries 0.3.0 +2012-10-03 Edsko de Vries 0.3.0 * Implement new disconnection semantics * Make 'connect' asynchronous (sender allocated connection IDs) +* Fix distributed deadlock +* Optimize treatment of crossed connection requests +* Relax upper bound on network +* Fix memory leaks 2012-08-20 Edsko de Vries 0.2.0.3 From 318599d6984aa732e9a65529dec49476789bee56 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 3 Oct 2012 15:05:49 +0100 Subject: [PATCH 0229/2357] Update ChangeLog for release 0.2.1. --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 38153a45..e3ba0c32 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,4 @@ -<> Edsko de Vries 0.2.1 +2012-10-03 Edsko de Vries 0.2.1 * Add support for 'staticFlip' From 868412b6a13235862a826fbda5dedc1f58f0b758 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 3 Oct 2012 15:20:00 +0100 Subject: [PATCH 0230/2357] Update changelog for release 0.2.0.6. --- ChangeLog | 3 ++- distributed-process-simplelocalnet.cabal | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 2e45c22e..e8721ef4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,8 @@ -<> Edsko de Vries 0.2.0.6 +2012-10-03 Edsko de Vries 0.2.0.6 * Use new version of network-transport * network-2.4.0 compatibility +* Relax upper bound on distributed-process dependency 2012-08-22 Edsko de Vries 0.2.0.5 diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 1bfaa605..c74fa8e6 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -34,7 +34,7 @@ Library transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4, - distributed-process >= 0.2 && < 0.4 + distributed-process >= 0.2 && < 0.5 Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast Extensions: RankNTypes, @@ -57,7 +57,7 @@ Test-Suite TestSimpleLocalnet transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4, - distributed-process >= 0.2 && < 0.4 + distributed-process >= 0.2 && < 0.5 Extensions: RankNTypes, DeriveDataTypeable ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind From 3a362b1a7b438c0fcd59cfaa0eb4966ba609c9cf Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 5 Oct 2012 17:56:02 +0100 Subject: [PATCH 0231/2357] Start work on a mock network layer for testing --- network-transport-tcp.cabal | 10 + src/Network/Transport/TCP.hs | 11 + src/Network/Transport/TCP/Internal.hs | 11 + src/Network/Transport/TCP/Mock/Socket.hs | 249 ++++++++++++++++++ .../Transport/TCP/Mock/Socket/ByteString.hs | 27 ++ tests/TestTCP.hs | 32 ++- 6 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 src/Network/Transport/TCP/Mock/Socket.hs create mode 100644 src/Network/Transport/TCP/Mock/Socket/ByteString.hs diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index e89cc22f..539175b2 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -20,6 +20,10 @@ Source-Repository head Location: https://github.com/haskell-distributed/distributed-process SubDir: network-transport-tcp +Flag use-mock-network + Description: Use mock network implementation (for testing) + Default: False + Library Build-Depends: base >= 4.3 && < 5, network-transport >= 0.3 && < 0.4, @@ -32,6 +36,10 @@ Library Extensions: CPP ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src + If flag(use-mock-network) + CPP-Options: -DUSE_MOCK_NETWORK + Exposed-modules: Network.Transport.TCP.Mock.Socket + Network.Transport.TCP.Mock.Socket.ByteString Test-Suite TestTCP Type: exitcode-stdio-1.0 @@ -45,3 +53,5 @@ Test-Suite TestTCP HS-Source-Dirs: tests Extensions: CPP, OverloadedStrings + If flag(use-mock-network) + CPP-Options: -DUSE_MOCK_NETWORK diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index d582d593..049e5019 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -55,7 +55,12 @@ import Network.Transport.Internal , timeoutMaybe , asyncWhenCancelled ) + +#ifdef USE_MOCK_NETWORK +import qualified Network.Transport.TCP.Mock.Socket as N +#else import qualified Network.Socket as N +#endif ( HostName , ServiceName , Socket @@ -71,7 +76,13 @@ import qualified Network.Socket as N , sOMAXCONN , AddrInfo ) + +#ifdef USE_MOCK_NETWORK +import Network.Transport.TCP.Mock.Socket.ByteString (sendMany) +#else import Network.Socket.ByteString (sendMany) +#endif + import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) import Control.Concurrent.MVar diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs index 3aa73878..2c24eefe 100644 --- a/src/Network/Transport/TCP/Internal.hs +++ b/src/Network/Transport/TCP/Internal.hs @@ -12,7 +12,12 @@ import Prelude hiding (catch) #endif import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) + +#ifdef USE_MOCK_NETWORK +import qualified Network.Transport.TCP.Mock.Socket as N +#else import qualified Network.Socket as N +#endif ( HostName , ServiceName , Socket @@ -30,7 +35,13 @@ import qualified Network.Socket as N , accept , sClose ) + +#ifdef USE_MOCK_NETWORK +import qualified Network.Transport.TCP.Mock.Socket.ByteString as NBS (recv) +#else import qualified Network.Socket.ByteString as NBS (recv) +#endif + import Control.Concurrent (ThreadId) import Control.Monad (forever, when) import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_) diff --git a/src/Network/Transport/TCP/Mock/Socket.hs b/src/Network/Transport/TCP/Mock/Socket.hs new file mode 100644 index 00000000..e0b7330f --- /dev/null +++ b/src/Network/Transport/TCP/Mock/Socket.hs @@ -0,0 +1,249 @@ +{-# LANGUAGE EmptyDataDecls #-} +module Network.Transport.TCP.Mock.Socket + ( -- * Types + HostName + , ServiceName + , Socket + , SocketType(..) + , SocketOption(..) + , AddrInfo(..) + , Family + , SockAddr + , ProtocolNumber + , ShutdownCmd(..) + -- * Functions + , getAddrInfo + , socket + , bindSocket + , listen + , setSocketOption + , accept + , sClose + , connect + , shutdown + -- * Constants + , defaultHints + , defaultProtocol + , sOMAXCONN + -- * Internal API + , writeSocket + , readSocket + ) where + +import Data.Word (Word8) +import Data.Map (Map) +import qualified Data.Map as Map +import Control.Category ((>>>)) +import Control.Applicative ((<$>)) +import Control.Concurrent.MVar +import Control.Concurrent.Chan +import System.IO.Unsafe (unsafePerformIO) +import Data.Accessor (Accessor, accessor, (^=), (^.)) +import qualified Data.Accessor.Container as DAC (mapMaybe) + +-------------------------------------------------------------------------------- +-- Mock state -- +-------------------------------------------------------------------------------- + +data MockState = MockState { + _boundSockets :: !(Map SockAddr Socket) + , _nextSocketId :: !Int + } + +initialMockState :: MockState +initialMockState = MockState { + _boundSockets = Map.empty + , _nextSocketId = 0 + } + +mockState :: MVar MockState +{-# NOINLINE mockState #-} +mockState = unsafePerformIO $ newMVar initialMockState + +get :: Accessor MockState a -> IO a +get acc = withMVar mockState $ return . (^. acc) + +set :: Accessor MockState a -> a -> IO () +set acc val = modifyMVar_ mockState $ return . (acc ^= val) + +boundSockets :: Accessor MockState (Map SockAddr Socket) +boundSockets = accessor _boundSockets (\bs st -> st { _boundSockets = bs }) + +boundSocketAt :: SockAddr -> Accessor MockState (Maybe Socket) +boundSocketAt addr = boundSockets >>> DAC.mapMaybe addr + +nextSocketId :: Accessor MockState Int +nextSocketId = accessor _nextSocketId (\sid st -> st { _nextSocketId = sid }) + +-------------------------------------------------------------------------------- +-- The public API (mirroring Network.Socket) -- +-------------------------------------------------------------------------------- + +type HostName = String +type ServiceName = String +type PortNumber = String +type HostAddress = String + +data SocketType = Stream +data SocketOption = ReuseAddr +data ShutdownCmd = ShutdownSend + +data Family +data ProtocolNumber + +data Socket = Socket { + socketState :: MVar SocketState + , socketDescription :: String + } + +data SocketState = + Uninit + | BoundSocket { socketBacklog :: Chan (Socket, SockAddr, MVar Socket) } + | Connected { socketPeer :: Socket,socketBuff :: Chan Word8 } + | Closed + +data AddrInfo = AddrInfo { + addrFamily :: Family + , addrAddress :: !SockAddr + } + +data SockAddr = SockAddrInet PortNumber HostAddress + deriving (Eq, Ord, Show) + +instance Show AddrInfo where + show = show . addrAddress + +instance Show Socket where + show sock = "<>" + +getAddrInfo :: Maybe AddrInfo -> Maybe HostName -> Maybe ServiceName -> IO [AddrInfo] +getAddrInfo _ (Just host) (Just port) = return . return $ AddrInfo { + addrFamily = error "Family unused" + , addrAddress = SockAddrInet port host + } +getAddrInfo _ _ _ = error "getAddrInfo: unsupported arguments" + +defaultHints :: AddrInfo +defaultHints = error "defaultHints not implemented" + +socket :: Family -> SocketType -> ProtocolNumber -> IO Socket +socket _ Stream _ = do + state <- newMVar Uninit + sid <- get nextSocketId + set nextSocketId (sid + 1) + return Socket { + socketState = state + , socketDescription = show sid + } + +bindSocket :: Socket -> SockAddr -> IO () +bindSocket sock addr = do + modifyMVar_ (socketState sock) $ \st -> case st of + Uninit -> do + backlog <- newChan + return BoundSocket { + socketBacklog = backlog + } + _ -> + error "bind: socket already initialized" + set (boundSocketAt addr) (Just sock) + +listen :: Socket -> Int -> IO () +listen _ _ = return () + +defaultProtocol :: ProtocolNumber +defaultProtocol = error "defaultProtocol not implemented" + +setSocketOption :: Socket -> SocketOption -> Int -> IO () +setSocketOption _ ReuseAddr 1 = return () +setSocketOption _ _ _ = error "setSocketOption: unsupported arguments" + +accept :: Socket -> IO (Socket, SockAddr) +accept serverSock = do + backlog <- withMVar (socketState serverSock) $ \st -> case st of + BoundSocket {} -> + return (socketBacklog st) + _ -> + error "accept: socket not bound" + (them, theirAddress, reply) <- readChan backlog + buff <- newChan + ourState <- newMVar Connected { + socketPeer = them + , socketBuff = buff + } + let us = Socket { + socketState = ourState + , socketDescription = "" + } + putMVar reply us + return (us, theirAddress) + +sClose :: Socket -> IO () +sClose sock = do + mPeer <- modifyMVar (socketState sock) $ \st -> case st of + Connected {} -> + return (Closed, Just $ socketPeer st) + _ -> + return (Closed, Nothing) + case mPeer of + Just peer -> modifyMVar_ (socketState peer) $ const (return Closed) + Nothing -> return () + +connect :: Socket -> SockAddr -> IO () +connect us serverAddr = do + mServer <- get (boundSocketAt serverAddr) + case mServer of + Just server -> do + serverBacklog <- withMVar (socketState server) $ \st -> case st of + BoundSocket {} -> + return (socketBacklog st) + _ -> + error "connect: server socket not bound" + reply <- newEmptyMVar + writeChan serverBacklog (us, SockAddrInet "" "", reply) + them <- readMVar reply + modifyMVar_ (socketState us) $ \st -> case st of + Uninit -> do + buff <- newChan + return Connected { + socketPeer = them + , socketBuff = buff + } + _ -> + error "connect: already connected" + Nothing -> error "connect: unknown address" + +sOMAXCONN :: Int +sOMAXCONN = error "sOMAXCONN not implemented" + +shutdown :: Socket -> ShutdownCmd -> IO () +shutdown = error "shutdown not implemented" + +-------------------------------------------------------------------------------- +-- Functions with no direct public counterpart -- +-------------------------------------------------------------------------------- + +writeSocket :: Socket -> Word8 -> IO () +writeSocket sock w = do + peer <- withMVar (socketState sock) $ \st -> case st of + Connected {} -> + return (socketPeer st) + _ -> + error "writeSocket: not connected" + theirBuff <- withMVar (socketState peer) $ \st -> case st of + Connected {} -> + return (socketBuff st) + _ -> + error "writeSocket: peer socket closed" + writeChan theirBuff w + +readSocket :: Socket -> IO (Maybe Word8) +readSocket sock = do + mBuff <- withMVar (socketState sock) $ \st -> case st of + Connected {} -> + return (Just $ socketBuff st) + _ -> + return Nothing + case mBuff of + Just buff -> Just <$> readChan buff + Nothing -> return Nothing diff --git a/src/Network/Transport/TCP/Mock/Socket/ByteString.hs b/src/Network/Transport/TCP/Mock/Socket/ByteString.hs new file mode 100644 index 00000000..8bb8cf8e --- /dev/null +++ b/src/Network/Transport/TCP/Mock/Socket/ByteString.hs @@ -0,0 +1,27 @@ +module Network.Transport.TCP.Mock.Socket.ByteString + ( sendMany + , recv + ) where + +import Data.ByteString (ByteString) +import qualified Data.ByteString as BSS (pack, foldl) +import Data.Word (Word8) +import Control.Applicative ((<$>)) +import Network.Transport.TCP.Mock.Socket + +sendMany :: Socket -> [ByteString] -> IO () +sendMany sock = mapM_ (bsMapM_ $ writeSocket sock) + where + bsMapM_ :: (Word8 -> IO ()) -> ByteString -> IO () + bsMapM_ p = BSS.foldl (\io w -> io >> p w) (return ()) + +recv :: Socket -> Int -> IO ByteString +recv sock = \n -> BSS.pack <$> go [] n + where + go :: [Word8] -> Int -> IO [Word8] + go acc 0 = return (reverse acc) + go acc n = do + mw <- readSocket sock + case mw of + Just w -> go (w : acc) (n - 1) + Nothing -> return (reverse acc) diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 65eec7ad..ec4dc146 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -45,14 +45,26 @@ import Network.Transport.Internal ( encodeInt32 , void ) import Network.Transport.TCP.Internal (recvInt32, forkServer, recvWithLength) -import qualified Network.Socket as N ( sClose - , ServiceName - , Socket - , AddrInfo - , shutdown - , ShutdownCmd(ShutdownSend) - ) -import Network.Socket.ByteString (sendMany) + +#ifdef USE_MOCK_NETWORK +import qualified Network.Transport.TCP.Mock.Socket as N +#else +import qualified Network.Socket as N +#endif + ( sClose + , ServiceName + , Socket + , AddrInfo + , shutdown + , ShutdownCmd(ShutdownSend) + ) + +#ifdef USE_MOCK_NETWORK +import Network.Transport.TCP.Mock.Socket.ByteString (sendMany) +#else +import Network.Socket.ByteString (sendMany) +#endif + import Data.String (fromString) import GHC.IO.Exception (ioe_errno) import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) @@ -766,6 +778,7 @@ main = do portMVar <- newEmptyMVar forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show let nextPort = takeMVar portMVar + {- tcpResult <- tryIO $ runTests [ ("EarlyDisconnect", testEarlyDisconnect nextPort) , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) @@ -780,9 +793,12 @@ main = do , ("UnidirectionalError", testUnidirectionalError nextPort) , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] + -} -- Run the generic tests even if the TCP specific tests failed.. testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) -- ..but if the generic tests pass, still fail if the specific tests did not + {- case tcpResult of Left err -> throwIO err Right () -> return () + -} From 5579375b113523951d722b5c49560a60cd7d9fc0 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 10 Oct 2012 10:28:09 +0100 Subject: [PATCH 0232/2357] Handle exceptions in N.T.Util.spawn This closes #42. --- src/Network/Transport/Util.hs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Network/Transport/Util.hs b/src/Network/Transport/Util.hs index bea5da5e..e247c2b2 100644 --- a/src/Network/Transport/Util.hs +++ b/src/Network/Transport/Util.hs @@ -9,6 +9,7 @@ import Network.Transport , EndPointAddress , newEndPoint ) +import Control.Exception (throwIO) import Control.Concurrent (forkIO) import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) @@ -17,9 +18,16 @@ import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) -- Returns the address of the new end point. spawn :: Transport -> (EndPoint -> IO ()) -> IO EndPointAddress spawn transport proc = do - addr <- newEmptyMVar + addrMVar <- newEmptyMVar forkIO $ do - Right endpoint <- newEndPoint transport - putMVar addr (address endpoint) - proc endpoint - takeMVar addr + mEndPoint <- newEndPoint transport + case mEndPoint of + Left err -> + putMVar addrMVar (Left err) + Right endPoint -> do + putMVar addrMVar (Right (address endPoint)) + proc endPoint + mAddr <- takeMVar addrMVar + case mAddr of + Left err -> throwIO err + Right addr -> return addr From bc5089e2e94753a5a4651b8284a1b05d5551be99 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 11 Oct 2012 13:45:03 +0100 Subject: [PATCH 0233/2357] Fix bug in mock network interface When closing a socket, the peer socket is closed _after_ messages that were already sent have been delivered. --- src/Network/Transport/TCP/Mock/Socket.hs | 81 +++++++++++-------- .../Transport/TCP/Mock/Socket/ByteString.hs | 2 +- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/Network/Transport/TCP/Mock/Socket.hs b/src/Network/Transport/TCP/Mock/Socket.hs index e0b7330f..6e2cc5bb 100644 --- a/src/Network/Transport/TCP/Mock/Socket.hs +++ b/src/Network/Transport/TCP/Mock/Socket.hs @@ -28,13 +28,13 @@ module Network.Transport.TCP.Mock.Socket -- * Internal API , writeSocket , readSocket + , Message(..) ) where import Data.Word (Word8) import Data.Map (Map) import qualified Data.Map as Map import Control.Category ((>>>)) -import Control.Applicative ((<$>)) import Control.Concurrent.MVar import Control.Concurrent.Chan import System.IO.Unsafe (unsafePerformIO) @@ -99,12 +99,16 @@ data Socket = Socket { data SocketState = Uninit | BoundSocket { socketBacklog :: Chan (Socket, SockAddr, MVar Socket) } - | Connected { socketPeer :: Socket,socketBuff :: Chan Word8 } + | Connected { socketPeer :: Socket, socketBuff :: Chan Message } | Closed +data Message = + Payload Word8 + | CloseSocket + data AddrInfo = AddrInfo { - addrFamily :: Family - , addrAddress :: !SockAddr + addrFamily :: Family + , addrAddress :: SockAddr } data SockAddr = SockAddrInet PortNumber HostAddress @@ -165,29 +169,23 @@ accept serverSock = do return (socketBacklog st) _ -> error "accept: socket not bound" - (them, theirAddress, reply) <- readChan backlog - buff <- newChan + (theirSocket, theirAddress, reply) <- readChan backlog + ourBuff <- newChan ourState <- newMVar Connected { - socketPeer = them - , socketBuff = buff + socketPeer = theirSocket + , socketBuff = ourBuff } - let us = Socket { + let ourSocket = Socket { socketState = ourState , socketDescription = "" } - putMVar reply us - return (us, theirAddress) + putMVar reply ourSocket + return (ourSocket, theirAddress) sClose :: Socket -> IO () sClose sock = do - mPeer <- modifyMVar (socketState sock) $ \st -> case st of - Connected {} -> - return (Closed, Just $ socketPeer st) - _ -> - return (Closed, Nothing) - case mPeer of - Just peer -> modifyMVar_ (socketState peer) $ const (return Closed) - Nothing -> return () + writeSocket sock CloseSocket + modifyMVar_ (socketState sock) $ const (return Closed) connect :: Socket -> SockAddr -> IO () connect us serverAddr = do @@ -223,19 +221,29 @@ shutdown = error "shutdown not implemented" -- Functions with no direct public counterpart -- -------------------------------------------------------------------------------- -writeSocket :: Socket -> Word8 -> IO () -writeSocket sock w = do - peer <- withMVar (socketState sock) $ \st -> case st of +peerBuffer :: Socket -> IO (Either String (Chan Message)) +peerBuffer sock = do + mPeer <- withMVar (socketState sock) $ \st -> case st of Connected {} -> - return (socketPeer st) - _ -> - error "writeSocket: not connected" - theirBuff <- withMVar (socketState peer) $ \st -> case st of - Connected {} -> - return (socketBuff st) + return (Just (socketPeer st)) _ -> - error "writeSocket: peer socket closed" - writeChan theirBuff w + return Nothing + case mPeer of + Just peer -> withMVar (socketState peer) $ \st -> case st of + Connected {} -> + return (Right (socketBuff st)) + _ -> + return (Left "Peer socket closed") + Nothing -> + return (Left "Socket closed") + +writeSocket :: Socket -> Message -> IO () +writeSocket sock msg = do + theirBuff <- peerBuffer sock + case theirBuff of + Right buff -> writeChan buff msg + Left err -> case msg of Payload _ -> error $ "writeSocket: " ++ err + CloseSocket -> return () readSocket :: Socket -> IO (Maybe Word8) readSocket sock = do @@ -245,5 +253,14 @@ readSocket sock = do _ -> return Nothing case mBuff of - Just buff -> Just <$> readChan buff - Nothing -> return Nothing + Just buff -> do + msg <- readChan buff + case msg of + Payload w -> return (Just w) + CloseSocket -> modifyMVar (socketState sock) $ \st -> case st of + Connected {} -> + return (Closed, Nothing) + _ -> + error "readSocket: socket in unexpected state" + Nothing -> + return Nothing diff --git a/src/Network/Transport/TCP/Mock/Socket/ByteString.hs b/src/Network/Transport/TCP/Mock/Socket/ByteString.hs index 8bb8cf8e..24be4e44 100644 --- a/src/Network/Transport/TCP/Mock/Socket/ByteString.hs +++ b/src/Network/Transport/TCP/Mock/Socket/ByteString.hs @@ -10,7 +10,7 @@ import Control.Applicative ((<$>)) import Network.Transport.TCP.Mock.Socket sendMany :: Socket -> [ByteString] -> IO () -sendMany sock = mapM_ (bsMapM_ $ writeSocket sock) +sendMany sock = mapM_ (bsMapM_ (writeSocket sock . Payload)) where bsMapM_ :: (Word8 -> IO ()) -> ByteString -> IO () bsMapM_ p = BSS.foldl (\io w -> io >> p w) (return ()) From 9ee5599cb06e7610bf23638a1dd09526ad28fffc Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 11 Oct 2012 14:47:46 +0100 Subject: [PATCH 0234/2357] Relax upper bound on base --- network-transport-tests.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport-tests.cabal b/network-transport-tests.cabal index 2bafd0b8..c4a1de76 100644 --- a/network-transport-tests.cabal +++ b/network-transport-tests.cabal @@ -18,7 +18,7 @@ library Network.Transport.Tests.Auxiliary, Network.Transport.Tests.Traced -- other-modules: - build-depends: base ==4.5.*, + build-depends: base >= 4.5 && < 4.7, network-transport >= 0.3 && < 0.4, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, From f2f3b08c29be376abc3f7c1828febeb31f16f2da Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 11 Oct 2012 15:40:49 +0100 Subject: [PATCH 0235/2357] All the TCP-specific tests now work with the mock --- src/Network/Transport/TCP/Mock/Socket.hs | 59 ++++++++++++++++-------- tests/TestTCP.hs | 4 -- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/Network/Transport/TCP/Mock/Socket.hs b/src/Network/Transport/TCP/Mock/Socket.hs index 6e2cc5bb..427aa1aa 100644 --- a/src/Network/Transport/TCP/Mock/Socket.hs +++ b/src/Network/Transport/TCP/Mock/Socket.hs @@ -34,6 +34,7 @@ module Network.Transport.TCP.Mock.Socket import Data.Word (Word8) import Data.Map (Map) import qualified Data.Map as Map +import Control.Exception (throwIO) import Control.Category ((>>>)) import Control.Concurrent.MVar import Control.Concurrent.Chan @@ -46,14 +47,16 @@ import qualified Data.Accessor.Container as DAC (mapMaybe) -------------------------------------------------------------------------------- data MockState = MockState { - _boundSockets :: !(Map SockAddr Socket) - , _nextSocketId :: !Int + _boundSockets :: !(Map SockAddr Socket) + , _nextSocketId :: !Int + , _validHostnames :: [HostName] } initialMockState :: MockState initialMockState = MockState { - _boundSockets = Map.empty - , _nextSocketId = 0 + _boundSockets = Map.empty + , _nextSocketId = 0 + , _validHostnames = ["localhost", "127.0.0.1"] } mockState :: MVar MockState @@ -75,6 +78,9 @@ boundSocketAt addr = boundSockets >>> DAC.mapMaybe addr nextSocketId :: Accessor MockState Int nextSocketId = accessor _nextSocketId (\sid st -> st { _nextSocketId = sid }) +validHostnames :: Accessor MockState [HostName] +validHostnames = accessor _validHostnames (\ns st -> st { _validHostnames = ns }) + -------------------------------------------------------------------------------- -- The public API (mirroring Network.Socket) -- -------------------------------------------------------------------------------- @@ -99,7 +105,7 @@ data Socket = Socket { data SocketState = Uninit | BoundSocket { socketBacklog :: Chan (Socket, SockAddr, MVar Socket) } - | Connected { socketPeer :: Socket, socketBuff :: Chan Message } + | Connected { socketPeer :: Maybe Socket, socketBuff :: Chan Message } | Closed data Message = @@ -121,10 +127,14 @@ instance Show Socket where show sock = "<>" getAddrInfo :: Maybe AddrInfo -> Maybe HostName -> Maybe ServiceName -> IO [AddrInfo] -getAddrInfo _ (Just host) (Just port) = return . return $ AddrInfo { - addrFamily = error "Family unused" - , addrAddress = SockAddrInet port host - } +getAddrInfo _ (Just host) (Just port) = do + validHosts <- get validHostnames + if host `elem` validHosts + then return . return $ AddrInfo { + addrFamily = error "Family unused" + , addrAddress = SockAddrInet port host + } + else throwSocketError $ "getAddrInfo: invalid hostname '" ++ host ++ "'" getAddrInfo _ _ _ = error "getAddrInfo: unsupported arguments" defaultHints :: AddrInfo @@ -149,7 +159,7 @@ bindSocket sock addr = do socketBacklog = backlog } _ -> - error "bind: socket already initialized" + throwSocketError "bind: socket already initialized" set (boundSocketAt addr) (Just sock) listen :: Socket -> Int -> IO () @@ -168,11 +178,11 @@ accept serverSock = do BoundSocket {} -> return (socketBacklog st) _ -> - error "accept: socket not bound" + throwSocketError "accept: socket not bound" (theirSocket, theirAddress, reply) <- readChan backlog ourBuff <- newChan ourState <- newMVar Connected { - socketPeer = theirSocket + socketPeer = Just theirSocket , socketBuff = ourBuff } let ourSocket = Socket { @@ -196,7 +206,7 @@ connect us serverAddr = do BoundSocket {} -> return (socketBacklog st) _ -> - error "connect: server socket not bound" + throwSocketError "connect: server socket not bound" reply <- newEmptyMVar writeChan serverBacklog (us, SockAddrInet "" "", reply) them <- readMVar reply @@ -204,18 +214,24 @@ connect us serverAddr = do Uninit -> do buff <- newChan return Connected { - socketPeer = them + socketPeer = Just them , socketBuff = buff } _ -> - error "connect: already connected" - Nothing -> error "connect: unknown address" + throwSocketError "connect: already connected" + Nothing -> throwSocketError "connect: unknown address" sOMAXCONN :: Int sOMAXCONN = error "sOMAXCONN not implemented" shutdown :: Socket -> ShutdownCmd -> IO () -shutdown = error "shutdown not implemented" +shutdown sock ShutdownSend = do + writeSocket sock CloseSocket + modifyMVar_ (socketState sock) $ \st -> case st of + Connected {} -> + return (Connected Nothing (socketBuff st)) + _ -> + return st -------------------------------------------------------------------------------- -- Functions with no direct public counterpart -- @@ -225,7 +241,7 @@ peerBuffer :: Socket -> IO (Either String (Chan Message)) peerBuffer sock = do mPeer <- withMVar (socketState sock) $ \st -> case st of Connected {} -> - return (Just (socketPeer st)) + return (socketPeer st) _ -> return Nothing case mPeer of @@ -237,12 +253,15 @@ peerBuffer sock = do Nothing -> return (Left "Socket closed") +throwSocketError :: String -> IO a +throwSocketError = throwIO . userError + writeSocket :: Socket -> Message -> IO () writeSocket sock msg = do theirBuff <- peerBuffer sock case theirBuff of Right buff -> writeChan buff msg - Left err -> case msg of Payload _ -> error $ "writeSocket: " ++ err + Left err -> case msg of Payload _ -> throwSocketError $ "writeSocket: " ++ err CloseSocket -> return () readSocket :: Socket -> IO (Maybe Word8) @@ -261,6 +280,6 @@ readSocket sock = do Connected {} -> return (Closed, Nothing) _ -> - error "readSocket: socket in unexpected state" + throwSocketError "readSocket: socket in unexpected state" Nothing -> return Nothing diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index ec4dc146..71f45973 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -778,7 +778,6 @@ main = do portMVar <- newEmptyMVar forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show let nextPort = takeMVar portMVar - {- tcpResult <- tryIO $ runTests [ ("EarlyDisconnect", testEarlyDisconnect nextPort) , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) @@ -793,12 +792,9 @@ main = do , ("UnidirectionalError", testUnidirectionalError nextPort) , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] - -} -- Run the generic tests even if the TCP specific tests failed.. testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) -- ..but if the generic tests pass, still fail if the specific tests did not - {- case tcpResult of Left err -> throwIO err Right () -> return () - -} From 5231ce339b167a71604ba5653488a58b51bfde6c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 11 Oct 2012 17:31:28 +0100 Subject: [PATCH 0236/2357] Start work on more unit testing. --- network-transport-tcp.cabal | 13 +++++++ tests/TestQC.hs | 74 +++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/TestQC.hs diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 539175b2..e289229a 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -55,3 +55,16 @@ Test-Suite TestTCP OverloadedStrings If flag(use-mock-network) CPP-Options: -DUSE_MOCK_NETWORK + +Test-Suite TestQC + Type: exitcode-stdio-1.0 + Main-Is: TestQC.hs + Build-Depends: base >= 4.3 && < 5, + test-framework, + test-framework-quickcheck2, + QuickCheck, + network-transport, + network-transport-tcp, + containers + ghc-options: -threaded + HS-Source-Dirs: tests diff --git a/tests/TestQC.hs b/tests/TestQC.hs new file mode 100644 index 00000000..0dd12ad4 --- /dev/null +++ b/tests/TestQC.hs @@ -0,0 +1,74 @@ +import Test.Framework (defaultMain, testGroup) +import Test.Framework.Providers.QuickCheck2 (testProperty) +import Test.QuickCheck (Gen, choose, suchThatMaybe, forAll) +import Test.QuickCheck.Property (morallyDubiousIOProperty) +import Data.Map (Map) +import qualified Data.Map as Map + +import Network.Transport +import Network.Transport.TCP (createTransport, defaultTCPParameters) + +data ScriptCmd = + Connect Int Int Reliability ConnectHints + | Close Int + +instance Show ScriptCmd where + show (Connect fr to _ _) = "Connect " ++ show fr ++ " " ++ show to + show (Close i) = "Close " ++ show i + +type Script = [ScriptCmd] + +connectCloseScript :: Int -> Gen Script +connectCloseScript numEndPoints = go Map.empty + where + go :: Map Int Bool -> Gen Script + go conns = do + next <- choose (0, 2) :: Gen Int + case next of + 0 -> do + fr <- choose (0, numEndPoints - 1) + to <- choose (0, numEndPoints - 1) + cmds <- go (Map.insert (Map.size conns) True conns) + return (Connect fr to ReliableOrdered defaultConnectHints : cmds) + 1 -> do + mConn <- suchThatMaybe (choose (0, Map.size conns - 1)) (conns Map.!) + case mConn of + Nothing -> go conns + Just conn -> do + cmds <- go (Map.insert conn False conns) + return (Close conn : cmds) + _ -> + return [] + +execScript :: [EndPoint] -> Script -> IO [Event] +execScript endPoints = go [] [] + where + go :: [Event] -> [(Connection, Int)] -> Script -> IO [Event] + go acc _ [] = return (reverse acc) + go acc conns (Connect fr to rel hints : cmds) = do + Right conn <- connect (endPoints !! fr) (address (endPoints !! to)) rel hints + ev <- receive (endPoints !! to) + go (ev : acc) (conns ++ [(conn, to)]) cmds + go acc conns (Close connIdx : cmds) = do + let (conn, connDst) = conns !! connIdx + close conn + ev <- receive (endPoints !! connDst) + go (ev : acc) conns cmds + +prop_connect_close transport = forAll (connectCloseScript 2) $ \script -> + morallyDubiousIOProperty $ do + Right endPointA <- newEndPoint transport + Right endPointB <- newEndPoint transport + evs <- execScript [endPointA, endPointB] script + return (evs == []) + +tests transport = [ + testGroup "Unidirectional" [ + testProperty "ConnectClose" (prop_connect_close transport) + ] + ] + +main :: IO () +main = do + Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters + defaultMain (tests transport) From 0fc50b4b064a2ecd7fc3f139e04b831acc7b0775 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 12 Oct 2012 12:54:07 +0100 Subject: [PATCH 0237/2357] Continue work on the script-driven testing Collecting of events is more implemented properly; still need the abstract interpreter. --- tests/TestQC.hs | 75 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 0dd12ad4..64a680e9 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -4,6 +4,11 @@ import Test.QuickCheck (Gen, choose, suchThatMaybe, forAll) import Test.QuickCheck.Property (morallyDubiousIOProperty) import Data.Map (Map) import qualified Data.Map as Map +import Control.Applicative ((<$>)) +import Control.Concurrent (forkIO, threadDelay) +import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan) +import Control.Monad (forM_, replicateM) +import Data.Either (rights) import Network.Transport import Network.Transport.TCP (createTransport, defaultTCPParameters) @@ -18,6 +23,9 @@ instance Show ScriptCmd where type Script = [ScriptCmd] +logShow :: Show a => a -> IO () +logShow = appendFile "log" . (++ "\n") . show + connectCloseScript :: Int -> Gen Script connectCloseScript numEndPoints = go Map.empty where @@ -31,7 +39,7 @@ connectCloseScript numEndPoints = go Map.empty cmds <- go (Map.insert (Map.size conns) True conns) return (Connect fr to ReliableOrdered defaultConnectHints : cmds) 1 -> do - mConn <- suchThatMaybe (choose (0, Map.size conns - 1)) (conns Map.!) + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns case mConn of Nothing -> go conns Just conn -> do @@ -40,27 +48,58 @@ connectCloseScript numEndPoints = go Map.empty _ -> return [] -execScript :: [EndPoint] -> Script -> IO [Event] -execScript endPoints = go [] [] + isOpen :: Map Int Bool -> Int -> Bool + isOpen conns connIdx = connIdx `Map.member` conns && conns Map.! connIdx + +execScript :: Transport -> Int -> Script -> IO (Map Int [Event]) +execScript transport numEndPoints script = do + endPoints <- rights <$> replicateM numEndPoints (newEndPoint transport) + chan <- newChan + forM_ (zip [0..] endPoints) $ forkIO . forwardTo chan + forkIO $ runScript endPoints [] script + collectAll chan 0 where - go :: [Event] -> [(Connection, Int)] -> Script -> IO [Event] - go acc _ [] = return (reverse acc) - go acc conns (Connect fr to rel hints : cmds) = do - Right conn <- connect (endPoints !! fr) (address (endPoints !! to)) rel hints - ev <- receive (endPoints !! to) - go (ev : acc) (conns ++ [(conn, to)]) cmds - go acc conns (Close connIdx : cmds) = do - let (conn, connDst) = conns !! connIdx - close conn - ev <- receive (endPoints !! connDst) - go (ev : acc) conns cmds + runScript :: [EndPoint] -> [Connection] -> Script -> IO () + runScript endPoints = go + where + go :: [Connection] -> Script -> IO () + go _ [] = threadDelay 50000 >> mapM_ closeEndPoint endPoints + go conns cmd@(Connect fr to rel hints : cmds) = do + Right conn <- connect (endPoints !! fr) (address (endPoints !! to)) rel hints + go (conns ++ [conn]) cmds + go conns cmd@(Close connIdx : cmds) = do + close (conns !! connIdx) + go conns cmds + + forwardTo :: Chan (Int, Event) -> (Int, EndPoint) -> IO () + forwardTo chan (ix, endPoint) = go + where + go :: IO () + go = do + ev <- receive endPoint + writeChan chan (ix, ev) + case ev of + EndPointClosed -> return () + _ -> go + + collectAll :: Chan (Int, Event) -> Int -> IO (Map Int [Event]) + collectAll chan = go (Map.fromList (zip [0 .. numEndPoints - 1] (repeat []))) + where + go :: Map Int [Event] -> Int -> IO (Map Int [Event]) + go acc numDone | numDone == numEndPoints = return $ Map.map reverse acc + go acc numDone = do + logShow acc + (ix, ev) <- readChan chan + let acc' = Map.adjust (ev :) ix acc + numDone' = case ev of EndPointClosed -> numDone + 1 + _ -> numDone + go acc' numDone' prop_connect_close transport = forAll (connectCloseScript 2) $ \script -> morallyDubiousIOProperty $ do - Right endPointA <- newEndPoint transport - Right endPointB <- newEndPoint transport - evs <- execScript [endPointA, endPointB] script - return (evs == []) + logShow script + evs <- execScript transport 2 script + return (evs == Map.fromList []) tests transport = [ testGroup "Unidirectional" [ From f73c8ac876eb2df2975fd0e65e8409cff0466c8b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 12 Oct 2012 13:43:33 +0100 Subject: [PATCH 0238/2357] Implement the abstract interpreter --- tests/TestQC.hs | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 64a680e9..7c0158aa 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -1,7 +1,7 @@ import Test.Framework (defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck (Gen, choose, suchThatMaybe, forAll) -import Test.QuickCheck.Property (morallyDubiousIOProperty) +import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) import Data.Map (Map) import qualified Data.Map as Map import Control.Applicative ((<$>)) @@ -49,7 +49,7 @@ connectCloseScript numEndPoints = go Map.empty return [] isOpen :: Map Int Bool -> Int -> Bool - isOpen conns connIdx = connIdx `Map.member` conns && conns Map.! connIdx + isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx execScript :: Transport -> Int -> Script -> IO (Map Int [Event]) execScript transport numEndPoints script = do @@ -63,12 +63,12 @@ execScript transport numEndPoints script = do runScript endPoints = go where go :: [Connection] -> Script -> IO () - go _ [] = threadDelay 50000 >> mapM_ closeEndPoint endPoints + go _ [] = threadDelay 500000 >> mapM_ closeEndPoint endPoints go conns cmd@(Connect fr to rel hints : cmds) = do Right conn <- connect (endPoints !! fr) (address (endPoints !! to)) rel hints go (conns ++ [conn]) cmds - go conns cmd@(Close connIdx : cmds) = do - close (conns !! connIdx) + go conns cmd@(Close connIx : cmds) = do + close (conns !! connIx) go conns cmds forwardTo :: Chan (Int, Event) -> (Int, EndPoint) -> IO () @@ -81,7 +81,7 @@ execScript transport numEndPoints script = do case ev of EndPointClosed -> return () _ -> go - + collectAll :: Chan (Int, Event) -> Int -> IO (Map Int [Event]) collectAll chan = go (Map.fromList (zip [0 .. numEndPoints - 1] (repeat []))) where @@ -95,11 +95,37 @@ execScript transport numEndPoints script = do _ -> numDone go acc' numDone' +verify :: Int -> Script -> Map Int [Event] -> Result +verify numEndPoints script evs = + case go script [] evs of + Nothing -> result { ok = Just True + } + Just err -> result { ok = Just False + , reason = err + } + where + go :: Script -> [(Int, ConnectionId)] -> Map Int [Event] -> Maybe String + go [] conns evs = + let closed = Map.fromList (zip [0 .. numEndPoints - 1] (repeat [EndPointClosed])) in + if evs == closed then Nothing + else Just $ "Expected " ++ show closed ++ "; got " ++ show evs + go (Connect fr to rel hints : cmds) conns evs = + case evs Map.! to of + (ConnectionOpened connId rel' addr : epEvs) | rel' == rel -> + go cmds (conns ++ [(to, connId)]) (Map.insert to epEvs evs) + _ -> Just $ "Missing ConnectionOpened event in " ++ show evs + go (Close connIx : cmds) conns evs = + let (epIx, connId) = conns !! connIx in + case evs Map.! epIx of + (ConnectionClosed connId' : epEvs) | connId' == connId -> + go cmds conns (Map.insert epIx epEvs evs) + _ -> Just $ "Missing ConnectionClosed event in " ++ show evs + prop_connect_close transport = forAll (connectCloseScript 2) $ \script -> morallyDubiousIOProperty $ do logShow script evs <- execScript transport 2 script - return (evs == Map.fromList []) + return (verify 2 script evs) tests transport = [ testGroup "Unidirectional" [ From 7ec743ccc717cdf420cc1f796fa8e124573a715c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 12 Oct 2012 17:59:55 +0100 Subject: [PATCH 0239/2357] Continue work on script driven testing There appears to be a bug in the mock network implementation, ConnectClose deadlocks sometimes with the mock but never with a real socket. --- network-transport-tcp.cabal | 2 +- src/Network/Transport/TCP/Mock/Socket.hs | 58 +++++--- tests/TestQC.hs | 165 ++++++++++++++--------- 3 files changed, 149 insertions(+), 76 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index e289229a..aa66d6c8 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -66,5 +66,5 @@ Test-Suite TestQC network-transport, network-transport-tcp, containers - ghc-options: -threaded + ghc-options: -threaded -Wall HS-Source-Dirs: tests diff --git a/src/Network/Transport/TCP/Mock/Socket.hs b/src/Network/Transport/TCP/Mock/Socket.hs index 427aa1aa..213135d4 100644 --- a/src/Network/Transport/TCP/Mock/Socket.hs +++ b/src/Network/Transport/TCP/Mock/Socket.hs @@ -41,6 +41,7 @@ import Control.Concurrent.Chan import System.IO.Unsafe (unsafePerformIO) import Data.Accessor (Accessor, accessor, (^=), (^.)) import qualified Data.Accessor.Container as DAC (mapMaybe) +import System.Timeout (timeout) -------------------------------------------------------------------------------- -- Mock state -- @@ -64,10 +65,10 @@ mockState :: MVar MockState mockState = unsafePerformIO $ newMVar initialMockState get :: Accessor MockState a -> IO a -get acc = withMVar mockState $ return . (^. acc) +get acc = timeoutThrow mvarThreshold $ withMVar mockState $ return . (^. acc) set :: Accessor MockState a -> a -> IO () -set acc val = modifyMVar_ mockState $ return . (acc ^= val) +set acc val = timeoutThrow mvarThreshold $ modifyMVar_ mockState $ return . (acc ^= val) boundSockets :: Accessor MockState (Map SockAddr Socket) boundSockets = accessor _boundSockets (\bs st -> st { _boundSockets = bs }) @@ -152,7 +153,7 @@ socket _ Stream _ = do bindSocket :: Socket -> SockAddr -> IO () bindSocket sock addr = do - modifyMVar_ (socketState sock) $ \st -> case st of + timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> case st of Uninit -> do backlog <- newChan return BoundSocket { @@ -174,7 +175,7 @@ setSocketOption _ _ _ = error "setSocketOption: unsupported arguments" accept :: Socket -> IO (Socket, SockAddr) accept serverSock = do - backlog <- withMVar (socketState serverSock) $ \st -> case st of + backlog <- timeoutThrow mvarThreshold $ withMVar (socketState serverSock) $ \st -> case st of BoundSocket {} -> return (socketBacklog st) _ -> @@ -189,28 +190,38 @@ accept serverSock = do socketState = ourState , socketDescription = "" } - putMVar reply ourSocket + timeoutThrow mvarThreshold $ putMVar reply ourSocket return (ourSocket, theirAddress) sClose :: Socket -> IO () sClose sock = do + -- Close the peer socket writeSocket sock CloseSocket - modifyMVar_ (socketState sock) $ const (return Closed) + + -- Close our socket + timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> + case st of + Connected {} -> do + -- In case there is a parallel read stuck on a readChan + writeChan (socketBuff st) CloseSocket + return Closed + _ -> + return Closed connect :: Socket -> SockAddr -> IO () connect us serverAddr = do mServer <- get (boundSocketAt serverAddr) case mServer of Just server -> do - serverBacklog <- withMVar (socketState server) $ \st -> case st of + serverBacklog <- timeoutThrow mvarThreshold $ withMVar (socketState server) $ \st -> case st of BoundSocket {} -> return (socketBacklog st) _ -> throwSocketError "connect: server socket not bound" reply <- newEmptyMVar writeChan serverBacklog (us, SockAddrInet "" "", reply) - them <- readMVar reply - modifyMVar_ (socketState us) $ \st -> case st of + them <- timeoutThrow mvarThreshold $ readMVar reply + timeoutThrow mvarThreshold $ modifyMVar_ (socketState us) $ \st -> case st of Uninit -> do buff <- newChan return Connected { @@ -227,7 +238,7 @@ sOMAXCONN = error "sOMAXCONN not implemented" shutdown :: Socket -> ShutdownCmd -> IO () shutdown sock ShutdownSend = do writeSocket sock CloseSocket - modifyMVar_ (socketState sock) $ \st -> case st of + timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> case st of Connected {} -> return (Connected Nothing (socketBuff st)) _ -> @@ -239,13 +250,13 @@ shutdown sock ShutdownSend = do peerBuffer :: Socket -> IO (Either String (Chan Message)) peerBuffer sock = do - mPeer <- withMVar (socketState sock) $ \st -> case st of + mPeer <- timeoutThrow mvarThreshold $ withMVar (socketState sock) $ \st -> case st of Connected {} -> return (socketPeer st) _ -> return Nothing case mPeer of - Just peer -> withMVar (socketState peer) $ \st -> case st of + Just peer -> timeoutThrow mvarThreshold $ withMVar (socketState peer) $ \st -> case st of Connected {} -> return (Right (socketBuff st)) _ -> @@ -266,20 +277,37 @@ writeSocket sock msg = do readSocket :: Socket -> IO (Maybe Word8) readSocket sock = do - mBuff <- withMVar (socketState sock) $ \st -> case st of + mBuff <- timeoutThrow mvarThreshold $ withMVar (socketState sock) $ \st -> case st of Connected {} -> return (Just $ socketBuff st) _ -> return Nothing case mBuff of Just buff -> do - msg <- readChan buff + msg <- timeoutThrow readSocketThreshold $ readChan buff case msg of Payload w -> return (Just w) - CloseSocket -> modifyMVar (socketState sock) $ \st -> case st of + CloseSocket -> timeoutThrow mvarThreshold $ modifyMVar (socketState sock) $ \st -> case st of Connected {} -> return (Closed, Nothing) _ -> throwSocketError "readSocket: socket in unexpected state" Nothing -> return Nothing + +-------------------------------------------------------------------------------- +-- Util -- +-------------------------------------------------------------------------------- + +mvarThreshold :: Int +mvarThreshold = 1000000 + +readSocketThreshold :: Int +readSocketThreshold = 10000000 + +timeoutThrow :: Int -> IO a -> IO a +timeoutThrow n p = do + ma <- timeout n p + case ma of + Just a -> return a + Nothing -> throwIO (userError "timeout") diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 7c0158aa..886c1462 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -1,23 +1,26 @@ -import Test.Framework (defaultMain, testGroup) +module Main (main, logShow) where + +import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) -import Test.QuickCheck (Gen, choose, suchThatMaybe, forAll) +import Test.QuickCheck (Gen, choose, suchThatMaybe, forAllShrink, Property) import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) import Data.Map (Map) import qualified Data.Map as Map -import Control.Applicative ((<$>)) +import Control.Exception (Exception, throwIO) import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan) -import Control.Monad (forM_, replicateM) -import Data.Either (rights) +import Data.List (inits) import Network.Transport import Network.Transport.TCP (createTransport, defaultTCPParameters) data ScriptCmd = - Connect Int Int Reliability ConnectHints + NewEndPoint + | Connect Int Int Reliability ConnectHints | Close Int instance Show ScriptCmd where + show NewEndPoint = "NewEndPoint" show (Connect fr to _ _) = "Connect " ++ show fr ++ " " ++ show to show (Close i) = "Close " ++ show i @@ -26,8 +29,37 @@ type Script = [ScriptCmd] logShow :: Show a => a -> IO () logShow = appendFile "log" . (++ "\n") . show -connectCloseScript :: Int -> Gen Script -connectCloseScript numEndPoints = go Map.empty +throwIfLeft :: Exception a => IO (Either a b) -> IO b +throwIfLeft p = do + mb <- p + case mb of + Left a -> throwIO a + Right b -> return b + +script_NewEndPoint :: Int -> Gen Script +script_NewEndPoint numEndPoints = return (replicate numEndPoints NewEndPoint) + +script_Connect :: Int -> Gen Script +script_Connect numEndPoints = do + script <- go + return (replicate numEndPoints NewEndPoint ++ script) + where + go :: Gen Script + go = do + next <- choose (0, 1) :: Gen Int + case next of + 0 -> do + fr <- choose (0, numEndPoints - 1) + to <- choose (0, numEndPoints - 1) + cmds <- go + return (Connect fr to ReliableOrdered defaultConnectHints : cmds) + _ -> + return [] + +script_ConnectClose :: Int -> Gen Script +script_ConnectClose numEndPoints = do + script <- go Map.empty + return (replicate numEndPoints NewEndPoint ++ script) where go :: Map Int Bool -> Gen Script go conns = do @@ -51,85 +83,98 @@ connectCloseScript numEndPoints = go Map.empty isOpen :: Map Int Bool -> Int -> Bool isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx -execScript :: Transport -> Int -> Script -> IO (Map Int [Event]) -execScript transport numEndPoints script = do - endPoints <- rights <$> replicateM numEndPoints (newEndPoint transport) +execScript :: Transport -> Script -> IO (Map Int [Event]) +execScript transport script = do chan <- newChan - forM_ (zip [0..] endPoints) $ forkIO . forwardTo chan - forkIO $ runScript endPoints [] script - collectAll chan 0 + runScript chan script + collectAll chan where - runScript :: [EndPoint] -> [Connection] -> Script -> IO () - runScript endPoints = go + runScript :: Chan (Maybe (Int, Event)) -> Script -> IO () + runScript chan = go [] [] where - go :: [Connection] -> Script -> IO () - go _ [] = threadDelay 500000 >> mapM_ closeEndPoint endPoints - go conns cmd@(Connect fr to rel hints : cmds) = do - Right conn <- connect (endPoints !! fr) (address (endPoints !! to)) rel hints - go (conns ++ [conn]) cmds - go conns cmd@(Close connIx : cmds) = do + go :: [EndPoint] -> [Connection] -> Script -> IO () + go _endPoints _conns [] = do + threadDelay 100000 + writeChan chan Nothing + go endPoints conns (NewEndPoint : cmds) = do + endPoint <- throwIfLeft $ newEndPoint transport + let endPointIx = length endPoints + _tid <- forkIO $ forwardTo chan (endPointIx, endPoint) + threadDelay 10000 + go (endPoints ++ [endPoint]) conns cmds + go endPoints conns (Connect fr to rel hints : cmds) = do + conn <- throwIfLeft $ connect (endPoints !! fr) (address (endPoints !! to)) rel hints + threadDelay 10000 + go endPoints (conns ++ [conn]) cmds + go endPoints conns (Close connIx : cmds) = do close (conns !! connIx) - go conns cmds + threadDelay 10000 + go endPoints conns cmds - forwardTo :: Chan (Int, Event) -> (Int, EndPoint) -> IO () + forwardTo :: Chan (Maybe (Int, Event)) -> (Int, EndPoint) -> IO () forwardTo chan (ix, endPoint) = go where go :: IO () go = do ev <- receive endPoint - writeChan chan (ix, ev) case ev of EndPointClosed -> return () - _ -> go + _ -> writeChan chan (Just (ix, ev)) >> go - collectAll :: Chan (Int, Event) -> Int -> IO (Map Int [Event]) - collectAll chan = go (Map.fromList (zip [0 .. numEndPoints - 1] (repeat []))) + collectAll :: Chan (Maybe (Int, Event)) -> IO (Map Int [Event]) + collectAll chan = go Map.empty where - go :: Map Int [Event] -> Int -> IO (Map Int [Event]) - go acc numDone | numDone == numEndPoints = return $ Map.map reverse acc - go acc numDone = do - logShow acc - (ix, ev) <- readChan chan - let acc' = Map.adjust (ev :) ix acc - numDone' = case ev of EndPointClosed -> numDone + 1 - _ -> numDone - go acc' numDone' - -verify :: Int -> Script -> Map Int [Event] -> Result -verify numEndPoints script evs = - case go script [] evs of - Nothing -> result { ok = Just True - } - Just err -> result { ok = Just False - , reason = err - } + go :: Map Int [Event] -> IO (Map Int [Event]) + go acc = do + mEv <- readChan chan + case mEv of + Nothing -> return $ Map.map reverse acc + Just (ix, ev) -> go (Map.alter (insertEvent ev) ix acc) + + insertEvent :: Event -> Maybe [Event] -> Maybe [Event] + insertEvent ev Nothing = Just [ev] + insertEvent ev (Just evs) = Just (ev : evs) + +verify :: Script -> Map Int [Event] -> Result +verify script = \evs -> case go script [] evs of + Nothing -> result { ok = Just True + } + Just err -> result { ok = Just False + , reason = '\n' : err ++ "\nAll events: " ++ show evs + } where go :: Script -> [(Int, ConnectionId)] -> Map Int [Event] -> Maybe String - go [] conns evs = - let closed = Map.fromList (zip [0 .. numEndPoints - 1] (repeat [EndPointClosed])) in - if evs == closed then Nothing - else Just $ "Expected " ++ show closed ++ "; got " ++ show evs - go (Connect fr to rel hints : cmds) conns evs = + go [] _conns evs = + if concat (Map.elems evs) == [] + then Nothing + else Just $ "Unexpected events: " ++ show evs + go (NewEndPoint : cmds) conns evs = + go cmds conns evs + go (Connect _fr to rel _hints : cmds) conns evs = case evs Map.! to of - (ConnectionOpened connId rel' addr : epEvs) | rel' == rel -> + (ConnectionOpened connId rel' _addr : epEvs) | rel' == rel -> go cmds (conns ++ [(to, connId)]) (Map.insert to epEvs evs) - _ -> Just $ "Missing ConnectionOpened event in " ++ show evs + _ -> Just $ "Missing (ConnectionOpened <> " ++ show rel ++ " <>) event in " ++ show evs go (Close connIx : cmds) conns evs = let (epIx, connId) = conns !! connIx in case evs Map.! epIx of (ConnectionClosed connId' : epEvs) | connId' == connId -> go cmds conns (Map.insert epIx epEvs evs) - _ -> Just $ "Missing ConnectionClosed event in " ++ show evs + _ -> Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs -prop_connect_close transport = forAll (connectCloseScript 2) $ \script -> - morallyDubiousIOProperty $ do - logShow script - evs <- execScript transport 2 script - return (verify 2 script evs) +genericProp :: Transport -> Int -> (Int -> Gen Script) -> Property +genericProp transport numEndPoints scriptGen = + forAllShrink (scriptGen numEndPoints) inits $ \script -> + morallyDubiousIOProperty $ do + evs <- execScript transport script + return (verify script evs) +tests :: Transport -> [Test] tests transport = [ testGroup "Unidirectional" [ - testProperty "ConnectClose" (prop_connect_close transport) + testProperty "NewEndPoint" (genericProp transport 2 script_NewEndPoint) + , testProperty "Connect" (genericProp transport 2 script_Connect) + , testProperty "ConnectClose" (genericProp transport 2 script_ConnectClose) ] ] From 667cc180e70722e08ffd270ecf26ba7c750575fe Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 15 Oct 2012 13:57:33 +0100 Subject: [PATCH 0240/2357] Add support for testing individual scripts And add a script that appears to identify a bug in N.T.TCP :( --- network-transport-tcp.cabal | 2 + tests/TestQC.hs | 75 +++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index aa66d6c8..91e4abe8 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -62,7 +62,9 @@ Test-Suite TestQC Build-Depends: base >= 4.3 && < 5, test-framework, test-framework-quickcheck2, + test-framework-hunit, QuickCheck, + HUnit, network-transport, network-transport-tcp, containers diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 886c1462..43b7c4e8 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -2,8 +2,10 @@ module Main (main, logShow) where import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) -import Test.QuickCheck (Gen, choose, suchThatMaybe, forAllShrink, Property) +import Test.Framework.Providers.HUnit (testCase) +import Test.QuickCheck (Gen, choose, suchThatMaybe, forAll, forAllShrink, Property) import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) +import Test.HUnit (Assertion, assertFailure) import Data.Map (Map) import qualified Data.Map as Map import Control.Exception (Exception, throwIO) @@ -16,13 +18,9 @@ import Network.Transport.TCP (createTransport, defaultTCPParameters) data ScriptCmd = NewEndPoint - | Connect Int Int Reliability ConnectHints + | Connect Int Int | Close Int - -instance Show ScriptCmd where - show NewEndPoint = "NewEndPoint" - show (Connect fr to _ _) = "Connect " ++ show fr ++ " " ++ show to - show (Close i) = "Close " ++ show i + deriving Show type Script = [ScriptCmd] @@ -52,7 +50,7 @@ script_Connect numEndPoints = do fr <- choose (0, numEndPoints - 1) to <- choose (0, numEndPoints - 1) cmds <- go - return (Connect fr to ReliableOrdered defaultConnectHints : cmds) + return (Connect fr to : cmds) _ -> return [] @@ -69,7 +67,7 @@ script_ConnectClose numEndPoints = do fr <- choose (0, numEndPoints - 1) to <- choose (0, numEndPoints - 1) cmds <- go (Map.insert (Map.size conns) True conns) - return (Connect fr to ReliableOrdered defaultConnectHints : cmds) + return (Connect fr to : cmds) 1 -> do mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns case mConn of @@ -102,8 +100,8 @@ execScript transport script = do _tid <- forkIO $ forwardTo chan (endPointIx, endPoint) threadDelay 10000 go (endPoints ++ [endPoint]) conns cmds - go endPoints conns (Connect fr to rel hints : cmds) = do - conn <- throwIfLeft $ connect (endPoints !! fr) (address (endPoints !! to)) rel hints + go endPoints conns (Connect fr to : cmds) = do + conn <- throwIfLeft $ connect (endPoints !! fr) (address (endPoints !! to)) ReliableOrdered defaultConnectHints threadDelay 10000 go endPoints (conns ++ [conn]) cmds go endPoints conns (Close connIx : cmds) = do @@ -135,13 +133,8 @@ execScript transport script = do insertEvent ev Nothing = Just [ev] insertEvent ev (Just evs) = Just (ev : evs) -verify :: Script -> Map Int [Event] -> Result -verify script = \evs -> case go script [] evs of - Nothing -> result { ok = Just True - } - Just err -> result { ok = Just False - , reason = '\n' : err ++ "\nAll events: " ++ show evs - } +verify :: Script -> Map Int [Event] -> Maybe String +verify script = go script [] where go :: Script -> [(Int, ConnectionId)] -> Map Int [Event] -> Maybe String go [] _conns evs = @@ -150,11 +143,11 @@ verify script = \evs -> case go script [] evs of else Just $ "Unexpected events: " ++ show evs go (NewEndPoint : cmds) conns evs = go cmds conns evs - go (Connect _fr to rel _hints : cmds) conns evs = + go (Connect _fr to : cmds) conns evs = case evs Map.! to of - (ConnectionOpened connId rel' _addr : epEvs) | rel' == rel -> + (ConnectionOpened connId _rel _addr : epEvs) -> go cmds (conns ++ [(to, connId)]) (Map.insert to epEvs evs) - _ -> Just $ "Missing (ConnectionOpened <> " ++ show rel ++ " <>) event in " ++ show evs + _ -> Just $ "Missing (ConnectionOpened <> <> <>) event in " ++ show evs go (Close connIx : cmds) conns evs = let (epIx, connId) = conns !! connIx in case evs Map.! epIx of @@ -162,19 +155,45 @@ verify script = \evs -> case go script [] evs of go cmds conns (Map.insert epIx epEvs evs) _ -> Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs -genericProp :: Transport -> Int -> (Int -> Gen Script) -> Property -genericProp transport numEndPoints scriptGen = - forAllShrink (scriptGen numEndPoints) inits $ \script -> +testScript1 :: Script +testScript1 = [ + NewEndPoint + , NewEndPoint + , Connect 0 1 + , Close 0 + , Connect 1 0 + , Close 1 + , Connect 1 0 + ] + +genericProp :: Transport -> Gen Script -> Property +genericProp transport scriptGen = + forAll scriptGen $ \script -> morallyDubiousIOProperty $ do + logShow script evs <- execScript transport script - return (verify script evs) + return $ case verify script evs of + Nothing -> result { ok = Just True + } + Just err -> result { ok = Just False + , reason = '\n' : err ++ "\nAll events: " ++ show evs + } + +testOneScript :: Transport -> Script -> Assertion +testOneScript transport script = do + logShow script + evs <- execScript transport script + case verify script evs of + Just err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err + Nothing -> return () tests :: Transport -> [Test] tests transport = [ testGroup "Unidirectional" [ - testProperty "NewEndPoint" (genericProp transport 2 script_NewEndPoint) - , testProperty "Connect" (genericProp transport 2 script_Connect) - , testProperty "ConnectClose" (genericProp transport 2 script_ConnectClose) + -- testProperty "NewEndPoint" (genericProp transport (script_NewEndPoint 2)) + --, testProperty "Connect" (genericProp transport (script_Connect 2)) + testCase "testScript1" (testOneScript transport testScript1) + -- testProperty "ConnectClose" (genericProp transport (script_ConnectClose 2)) ] ] From 0f5fc233caaa69b96abe0f9b89c6b84d5c0d6ef2 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 15 Oct 2012 14:21:55 +0100 Subject: [PATCH 0241/2357] Cleanup --- tests/TestQC.hs | 165 ++++++++++++++++++++++++++++-------------------- 1 file changed, 95 insertions(+), 70 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 43b7c4e8..46c7f69a 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -16,6 +16,10 @@ import Data.List (inits) import Network.Transport import Network.Transport.TCP (createTransport, defaultTCPParameters) +-------------------------------------------------------------------------------- +-- Script infrastructure -- +-------------------------------------------------------------------------------- + data ScriptCmd = NewEndPoint | Connect Int Int @@ -24,63 +28,6 @@ data ScriptCmd = type Script = [ScriptCmd] -logShow :: Show a => a -> IO () -logShow = appendFile "log" . (++ "\n") . show - -throwIfLeft :: Exception a => IO (Either a b) -> IO b -throwIfLeft p = do - mb <- p - case mb of - Left a -> throwIO a - Right b -> return b - -script_NewEndPoint :: Int -> Gen Script -script_NewEndPoint numEndPoints = return (replicate numEndPoints NewEndPoint) - -script_Connect :: Int -> Gen Script -script_Connect numEndPoints = do - script <- go - return (replicate numEndPoints NewEndPoint ++ script) - where - go :: Gen Script - go = do - next <- choose (0, 1) :: Gen Int - case next of - 0 -> do - fr <- choose (0, numEndPoints - 1) - to <- choose (0, numEndPoints - 1) - cmds <- go - return (Connect fr to : cmds) - _ -> - return [] - -script_ConnectClose :: Int -> Gen Script -script_ConnectClose numEndPoints = do - script <- go Map.empty - return (replicate numEndPoints NewEndPoint ++ script) - where - go :: Map Int Bool -> Gen Script - go conns = do - next <- choose (0, 2) :: Gen Int - case next of - 0 -> do - fr <- choose (0, numEndPoints - 1) - to <- choose (0, numEndPoints - 1) - cmds <- go (Map.insert (Map.size conns) True conns) - return (Connect fr to : cmds) - 1 -> do - mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns - case mConn of - Nothing -> go conns - Just conn -> do - cmds <- go (Map.insert conn False conns) - return (Close conn : cmds) - _ -> - return [] - - isOpen :: Map Int Bool -> Int -> Bool - isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx - execScript :: Transport -> Script -> IO (Map Int [Event]) execScript transport script = do chan <- newChan @@ -155,6 +102,61 @@ verify script = go script [] go cmds conns (Map.insert epIx epEvs evs) _ -> Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs +-------------------------------------------------------------------------------- +-- Script generators -- +-------------------------------------------------------------------------------- + +script_NewEndPoint :: Int -> Gen Script +script_NewEndPoint numEndPoints = return (replicate numEndPoints NewEndPoint) + +script_Connect :: Int -> Gen Script +script_Connect numEndPoints = do + script <- go + return (replicate numEndPoints NewEndPoint ++ script) + where + go :: Gen Script + go = do + next <- choose (0, 1) :: Gen Int + case next of + 0 -> do + fr <- choose (0, numEndPoints - 1) + to <- choose (0, numEndPoints - 1) + cmds <- go + return (Connect fr to : cmds) + _ -> + return [] + +script_ConnectClose :: Int -> Gen Script +script_ConnectClose numEndPoints = do + script <- go Map.empty + return (replicate numEndPoints NewEndPoint ++ script) + where + go :: Map Int Bool -> Gen Script + go conns = do + next <- choose (0, 2) :: Gen Int + case next of + 0 -> do + fr <- choose (0, numEndPoints - 1) + to <- choose (0, numEndPoints - 1) + cmds <- go (Map.insert (Map.size conns) True conns) + return (Connect fr to : cmds) + 1 -> do + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns + case mConn of + Nothing -> go conns + Just conn -> do + cmds <- go (Map.insert conn False conns) + return (Close conn : cmds) + _ -> + return [] + + isOpen :: Map Int Bool -> Int -> Bool + isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx + +-------------------------------------------------------------------------------- +-- Individual scripts to test specific bugs -- +-------------------------------------------------------------------------------- + testScript1 :: Script testScript1 = [ NewEndPoint @@ -166,6 +168,29 @@ testScript1 = [ , Connect 1 0 ] +-------------------------------------------------------------------------------- +-- Main application driver -- +-------------------------------------------------------------------------------- + +tests :: Transport -> [Test] +tests transport = [ + testGroup "Unidirectional" [ + -- testProperty "NewEndPoint" (genericProp transport (script_NewEndPoint 2)) + --, testProperty "Connect" (genericProp transport (script_Connect 2)) + testCase "testScript1" (testOneScript transport testScript1) + -- testProperty "ConnectClose" (genericProp transport (script_ConnectClose 2)) + ] + ] + +main :: IO () +main = do + Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters + defaultMain (tests transport) + +-------------------------------------------------------------------------------- +-- Test infrastructure -- +-------------------------------------------------------------------------------- + genericProp :: Transport -> Gen Script -> Property genericProp transport scriptGen = forAll scriptGen $ \script -> @@ -187,17 +212,17 @@ testOneScript transport script = do Just err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err Nothing -> return () -tests :: Transport -> [Test] -tests transport = [ - testGroup "Unidirectional" [ - -- testProperty "NewEndPoint" (genericProp transport (script_NewEndPoint 2)) - --, testProperty "Connect" (genericProp transport (script_Connect 2)) - testCase "testScript1" (testOneScript transport testScript1) - -- testProperty "ConnectClose" (genericProp transport (script_ConnectClose 2)) - ] - ] +-------------------------------------------------------------------------------- +-- Auxiliary +-------------------------------------------------------------------------------- + +logShow :: Show a => a -> IO () +logShow = appendFile "log" . (++ "\n") . show + +throwIfLeft :: Exception a => IO (Either a b) -> IO b +throwIfLeft p = do + mb <- p + case mb of + Left a -> throwIO a + Right b -> return b -main :: IO () -main = do - Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) From c95aa970625776a667c78c78d54e47de9c60a9c7 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 15 Oct 2012 18:23:56 +0100 Subject: [PATCH 0242/2357] Important bugfix: reconnecting didn't always work The following sequence resulted in deadlock: A connects to B A closes connection B connects to A B closes connection B reconnects to A The problem was in the network protocol, where A tells B the ID of the last connection B opened to A. If this is lower than the last connection B *actually* opened, B knows there are still some messages on the way to A and B must wait before they are resolved. However, this connection ID should be kept *per heavyweight connection*. We were inconsistent about this: we recorded the ID of the last created connection on the heavyweight connection, but the next available ID was a 'global' variable on the local endpoint. This meant that B sometimes incorrectly concluded that some messages were still on the way to A, resulting in deadlock. --- src/Network/Transport/TCP.hs | 99 ++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 049e5019..18ae2623 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -266,8 +266,12 @@ data LocalEndPointState = | LocalEndPointClosed data ValidLocalEndPointState = ValidLocalEndPointState - { _nextConnOutId :: !LightweightConnectionId - , _nextConnInId :: !HeavyweightConnectionId + { -- Next available ID for an outgoing lightweight self-connection + -- (see also remoteNextConnOutId) + _localNextConnOutId :: !LightweightConnectionId + -- Next available ID for an incoming heavyweight connection + , _nextConnInId :: !HeavyweightConnectionId + -- Currently active outgoing heavyweight connections , _localConnections :: !(Map EndPointAddress RemoteEndPoint) } @@ -387,6 +391,7 @@ data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int , _remoteIncoming :: !(Set LightweightConnectionId) , _remoteMaxIncoming :: !LightweightConnectionId + , _remoteNextConnOutId :: !LightweightConnectionId , remoteSocket :: !N.Socket , remoteSendLock :: !(MVar ()) } @@ -627,7 +632,8 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = RemoteEndPointValid vst -> do alive <- readIORef connAlive if alive - then schedule theirEndPoint $ sendOn vst (encodeInt32 connId : prependLength payload) + then schedule theirEndPoint $ + sendOn vst (encodeInt32 connId : prependLength payload) else throwIO $ TransportError SendClosed "Connection closed" RemoteEndPointClosing _ _ -> do alive <- readIORef connAlive @@ -749,11 +755,12 @@ handleConnectionRequest transport sock = handle handleException $ do else do sendLock <- newMVar () let vst = ValidRemoteEndPointState - { remoteSocket = sock - , remoteSendLock = sendLock - , _remoteOutgoing = 0 - , _remoteIncoming = Set.empty - , _remoteMaxIncoming = 0 + { remoteSocket = sock + , remoteSendLock = sendLock + , _remoteOutgoing = 0 + , _remoteIncoming = Set.empty + , _remoteMaxIncoming = 0 + , _remoteNextConnOutId = firstNonReservedLightweightConnectionId } sendMany sock [encodeInt32 ConnectionRequestAccepted] resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) @@ -896,7 +903,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do closeSocket :: N.Socket -> LightweightConnectionId -> IO Bool closeSocket sock lastReceivedId = do mAct <- modifyMVar theirState $ \st -> do - lastSentId <- getLastConnOutId ourEndPoint case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) @@ -918,7 +924,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- then we sent a ConnectionCreated *AND* a ConnectionClosed -- message to the remote endpoint, *both of which* it did not yet -- receive before sending the CloseSocket request. - if vst' ^. remoteOutgoing > 0 || lastReceivedId < lastSentId + if vst' ^. remoteOutgoing > 0 || lastReceivedId < lastSentId vst then return (RemoteEndPointValid vst', Nothing) else do @@ -930,7 +936,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do ] tryCloseSocket sock return (RemoteEndPointClosed, Just act) - RemoteEndPointClosing resolved vst -> + RemoteEndPointClosing resolved vst -> do -- Like above, we need to check if there is a ConnectionCreated -- message that we sent but that the remote endpoint has not yet -- received. However, since we are in 'closing' state, the only @@ -938,8 +944,9 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- ConnectionClosed, and CloseSocket message, none of which have -- yet been received. We leave the endpoint in closing state in -- that case. - if lastReceivedId < lastSentId - then + if lastReceivedId < lastSentId vst + then do + putStrLn "This really shouldn't happen" return (RemoteEndPointClosing resolved vst, Nothing) else do removeRemoteEndPoint (ourEndPoint, theirEndPoint) @@ -998,6 +1005,13 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do connId :: LightweightConnectionId -> ConnectionId connId = createConnectionId (remoteId theirEndPoint) + -- The ID of the last connection _we_ created (or 0 for none) + lastSentId :: ValidRemoteEndPointState -> LightweightConnectionId + lastSentId vst = + if vst ^. remoteNextConnOutId == firstNonReservedLightweightConnectionId + then 0 + else (vst ^. remoteNextConnOutId) - 1 + -------------------------------------------------------------------------------- -- Uninterruptable auxiliary functions -- -- -- @@ -1030,12 +1044,17 @@ createConnectionTo params ourEndPoint theirAddress hints = go else do -- 'findRemoteEndPoint' will have increased 'remoteOutgoing' mapIOException connectFailed $ do - act <- withMVar (remoteState theirEndPoint) $ \st -> case st of + act <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointValid vst -> do - connId <- getNextConnOutId ourEndPoint - schedule theirEndPoint $ do + let connId = vst ^. remoteNextConnOutId + act <- schedule theirEndPoint $ do sendOn vst [encodeInt32 CreatedNewConnection, encodeInt32 connId] return connId + return ( RemoteEndPointValid + $ remoteNextConnOutId ^= connId + 1 + $ vst + , act + ) -- Error cases RemoteEndPointInvalid err -> throwIO err @@ -1067,11 +1086,12 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do Right (sock, ConnectionRequestAccepted) -> do sendLock <- newMVar () let vst = ValidRemoteEndPointState - { remoteSocket = sock - , remoteSendLock = sendLock - , _remoteOutgoing = 0 - , _remoteIncoming = Set.empty - , _remoteMaxIncoming = 0 + { remoteSocket = sock + , remoteSendLock = sendLock + , _remoteOutgoing = 0 + , _remoteIncoming = Set.empty + , _remoteMaxIncoming = 0 + , _remoteNextConnOutId = firstNonReservedLightweightConnectionId } resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return True @@ -1153,7 +1173,7 @@ connectToSelf :: LocalEndPoint -> IO Connection connectToSelf ourEndPoint = do connAlive <- newIORef True -- Protected by the local endpoint lock - lconnId <- mapIOException connectFailed $ getNextConnOutId ourEndPoint + lconnId <- mapIOException connectFailed $ getLocalNextConnOutId ourEndPoint let connId = createConnectionId heavyweightSelfConnectionId lconnId writeChan ourChan $ ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) @@ -1208,33 +1228,21 @@ resolveInit (ourEndPoint, theirEndPoint) newState = _ -> relyViolation (ourEndPoint, theirEndPoint) "resolveInit" --- | Get the next outgoing connection ID +-- | Get the next outgoing self-connection ID -- -- Throws an IO exception when the endpoint is closed. -getNextConnOutId :: LocalEndPoint -> IO LightweightConnectionId -getNextConnOutId ourEndpoint = +getLocalNextConnOutId :: LocalEndPoint -> IO LightweightConnectionId +getLocalNextConnOutId ourEndpoint = modifyMVar (localState ourEndpoint) $ \st -> case st of LocalEndPointValid vst -> do - let connId = vst ^. nextConnOutId + let connId = vst ^. localNextConnOutId return ( LocalEndPointValid - . (nextConnOutId ^= connId + 1) + . (localNextConnOutId ^= connId + 1) $ vst , connId) LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" --- | The last outgoing connection ID we created, or zero if we never created any -getLastConnOutId :: LocalEndPoint -> IO LightweightConnectionId -getLastConnOutId ourEndPoint = - withMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> - let nextId = vst ^. nextConnOutId in - if nextId == firstNonReservedLightweightConnectionId - then return 0 - else return (nextId - 1) - LocalEndPointClosed -> - throwIO $ userError "Local endpoint closed" - -- | Create a new local endpoint -- -- May throw a TransportError NewEndPointErrorCode exception if the transport @@ -1243,9 +1251,9 @@ createLocalEndPoint :: TCPTransport -> IO LocalEndPoint createLocalEndPoint transport = do chan <- newChan state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState - { _nextConnOutId = firstNonReservedLightweightConnectionId - , _nextConnInId = firstNonReservedHeavyweightConnectionId - , _localConnections = Map.empty + { _localNextConnOutId = firstNonReservedLightweightConnectionId + , _localConnections = Map.empty + , _nextConnInId = firstNonReservedHeavyweightConnectionId } modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> do @@ -1598,8 +1606,8 @@ localEndPoints = accessor _localEndPoints (\es st -> st { _localEndPoints = es } nextEndPointId :: Accessor ValidTransportState EndPointId nextEndPointId = accessor _nextEndPointId (\eid st -> st { _nextEndPointId = eid }) -nextConnOutId :: Accessor ValidLocalEndPointState LightweightConnectionId -nextConnOutId = accessor _nextConnOutId (\cix st -> st { _nextConnOutId = cix }) +localNextConnOutId :: Accessor ValidLocalEndPointState LightweightConnectionId +localNextConnOutId = accessor _localNextConnOutId (\cix st -> st { _localNextConnOutId = cix }) localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) @@ -1616,6 +1624,9 @@ remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = remoteMaxIncoming :: Accessor ValidRemoteEndPointState LightweightConnectionId remoteMaxIncoming = accessor _remoteMaxIncoming (\lcid st -> st { _remoteMaxIncoming = lcid }) +remoteNextConnOutId :: Accessor ValidRemoteEndPointState LightweightConnectionId +remoteNextConnOutId = accessor _remoteNextConnOutId (\cix st -> st { _remoteNextConnOutId = cix }) + localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr From 2f063a4cb8d4e72e46ebe94239af6ba770b26f53 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 15 Oct 2012 18:23:56 +0100 Subject: [PATCH 0243/2357] Important bugfix: reconnecting didn't always work The following sequence resulted in deadlock: A connects to B A closes connection B connects to A B closes connection B reconnects to A The problem was in the network protocol, where A tells B the ID of the last connection B opened to A. If this is lower than the last connection B *actually* opened, B knows there are still some messages on the way to A and B must wait before they are resolved. However, this connection ID should be kept *per heavyweight connection*. We were inconsistent about this: we recorded the ID of the last created connection on the heavyweight connection, but the next available ID was a 'global' variable on the local endpoint. This meant that B sometimes incorrectly concluded that some messages were still on the way to A, resulting in deadlock. --- src/Network/Transport/Tests.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Network/Transport/Tests.hs b/src/Network/Transport/Tests.hs index c5394b87..f90ffb12 100644 --- a/src/Network/Transport/Tests.hs +++ b/src/Network/Transport/Tests.hs @@ -692,10 +692,10 @@ testCloseTransport newTransport = do -- Client now closes down its transport. We should receive connection closed messages (we don't know the precise order, however) -- TODO: should we get an EventConnectionLost for theirAddr1? We have no outgoing connections - evs <- replicateM 4 $ receive endpoint + evs <- replicateM 3 $ receive endpoint let expected = [ ConnectionClosed cid1 , ConnectionClosed cid2 - , ErrorEvent (TransportError (EventConnectionLost theirAddr1) "") + -- , ErrorEvent (TransportError (EventConnectionLost theirAddr1) "") , ErrorEvent (TransportError (EventConnectionLost theirAddr2) "") ] True <- return $ expected `elem` permutations evs From f5c2ebbc705022ef34974cb52986b7568e55ed26 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 16 Oct 2012 09:39:49 +0100 Subject: [PATCH 0244/2357] Cleanup. Explain previous bug in more detail. --- tests/TestQC.hs | 70 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 46c7f69a..3f7a8be0 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -157,8 +157,49 @@ script_ConnectClose numEndPoints = do -- Individual scripts to test specific bugs -- -------------------------------------------------------------------------------- -testScript1 :: Script -testScript1 = [ +-- | Bug #1 +-- +-- When process A wants to close the heavyweight connection to process B it +-- sends a CloseSocket request together with the ID of the last connection from +-- B. When B receives the CloseSocket request it can compare this ID to the last +-- connection it created; if they don't match, B knows that there are some +-- messages still on the way from B to A (in particular, a CreatedConnection +-- message) which will cancel the CloseSocket request from A. Hence, it will +-- know to ignore the CloseSocket request from A. +-- +-- The bug was that we recorded the last _created_ outgoing connection on the +-- local endpoint, but the last _received_ incoming connection on the state of +-- the heavyweight connection. So, in the script below, the following happened: +-- +-- A connects to B, records "last connection ID is 1024" +-- A closes the lightweight connection, sends [CloseConnection 1024] +-- A closes the heivyweight connection, sends [CloseSocket 0] +-- +-- (the 0 here indicates that it had not yet received any connections from B) +-- +-- B receives the [CloseSocket 0], compares it to the recorded outgoing ID (0), +-- confirms that they are equal, and confirms the CloseSocket request. +-- +-- B connects to A, records "last connection ID is 1024" +-- B closes the lightweight connection, sends [CloseConnection 1024] +-- B closes the heavyweight connection, sends [CloseSocket 0] +-- +-- (the 0 here indicates that it has not yet received any connections from A, +-- ON THIS HEAVYWEIGHT connection) +-- +-- A receives the [CloseSocket 0] request, compares it to the last recorded +-- outgoing ID (1024), sees that they are not equal, and concludes that this +-- must mean that there is still a CreatedConnection message on the way from A +-- to B. +-- +-- This of course is not the case, so B will wait forever for A to confirm +-- the CloseSocket request, and deadlock arises. (This deadlock doesn't become +-- obvious though until the next attempt from B to connect to A.) +-- +-- The solution is of course that both the recorded outgoing and recorded +-- incoming connection ID must be per heavyweight connection. +script_Bug1 :: Script +script_Bug1 = [ NewEndPoint , NewEndPoint , Connect 0 1 @@ -174,13 +215,18 @@ testScript1 = [ tests :: Transport -> [Test] tests transport = [ - testGroup "Unidirectional" [ - -- testProperty "NewEndPoint" (genericProp transport (script_NewEndPoint 2)) - --, testProperty "Connect" (genericProp transport (script_Connect 2)) - testCase "testScript1" (testOneScript transport testScript1) - -- testProperty "ConnectClose" (genericProp transport (script_ConnectClose 2)) + testGroup "Bugs" [ + testOne "Bug1" script_Bug1 + ] + , testGroup "Unidirectional" [ + testQC "NewEndPoint" (script_NewEndPoint 2) + , testQC "Connect" (script_Connect 2) + , testQC "ConnectClose" (script_ConnectClose 2) + ] ] - ] + where + testOne label script = testCase label (testScript transport script) + testQC label script = testProperty label (testScriptGen transport script) main :: IO () main = do @@ -191,8 +237,8 @@ main = do -- Test infrastructure -- -------------------------------------------------------------------------------- -genericProp :: Transport -> Gen Script -> Property -genericProp transport scriptGen = +testScriptGen :: Transport -> Gen Script -> Property +testScriptGen transport scriptGen = forAll scriptGen $ \script -> morallyDubiousIOProperty $ do logShow script @@ -204,8 +250,8 @@ genericProp transport scriptGen = , reason = '\n' : err ++ "\nAll events: " ++ show evs } -testOneScript :: Transport -> Script -> Assertion -testOneScript transport script = do +testScript :: Transport -> Script -> Assertion +testScript transport script = do logShow script evs <- execScript transport script case verify script evs of From e3fe40e68368cfef71ae78e000c1930f73ded812 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 16 Oct 2012 17:50:31 +0100 Subject: [PATCH 0245/2357] Cleanup. Add support for 'send' --- network-transport-tcp.cabal | 5 +- tests/TestQC.hs | 127 ++++++++++++++++++++++++++++++------ 2 files changed, 109 insertions(+), 23 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 91e4abe8..12b23960 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -67,6 +67,7 @@ Test-Suite TestQC HUnit, network-transport, network-transport-tcp, - containers - ghc-options: -threaded -Wall + containers, + bytestring + ghc-options: -threaded -Wall -fno-warn-orphans HS-Source-Dirs: tests diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 3f7a8be0..9e0bd8c3 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -1,9 +1,23 @@ -module Main (main, logShow) where +module Main + ( main + -- Shush the compiler about unused definitions + , logShow + , forAllShrink + , inits + ) where -import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework (Test, TestName, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.Framework.Providers.HUnit (testCase) -import Test.QuickCheck (Gen, choose, suchThatMaybe, forAll, forAllShrink, Property) +import Test.QuickCheck + ( Gen + , choose + , suchThatMaybe + , forAll + , forAllShrink + , Property + , Arbitrary(arbitrary) + ) import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) import Test.HUnit (Assertion, assertFailure) import Data.Map (Map) @@ -11,7 +25,11 @@ import qualified Data.Map as Map import Control.Exception (Exception, throwIO) import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan) +import Control.Monad (replicateM) import Data.List (inits) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (pack) +import qualified Data.ByteString as BSS (concat) import Network.Transport import Network.Transport.TCP (createTransport, defaultTCPParameters) @@ -24,6 +42,7 @@ data ScriptCmd = NewEndPoint | Connect Int Int | Close Int + | Send Int [ByteString] deriving Show type Script = [ScriptCmd] @@ -39,7 +58,7 @@ execScript transport script = do where go :: [EndPoint] -> [Connection] -> Script -> IO () go _endPoints _conns [] = do - threadDelay 100000 + threadDelay 10000 writeChan chan Nothing go endPoints conns (NewEndPoint : cmds) = do endPoint <- throwIfLeft $ newEndPoint transport @@ -55,6 +74,10 @@ execScript transport script = do close (conns !! connIx) threadDelay 10000 go endPoints conns cmds + go endPoints conns (Send connIx payload : cmds) = do + Right () <- send (conns !! connIx) payload + threadDelay 10000 + go endPoints conns cmds forwardTo :: Chan (Maybe (Int, Event)) -> (Int, EndPoint) -> IO () forwardTo chan (ix, endPoint) = go @@ -91,16 +114,28 @@ verify script = go script [] go (NewEndPoint : cmds) conns evs = go cmds conns evs go (Connect _fr to : cmds) conns evs = - case evs Map.! to of - (ConnectionOpened connId _rel _addr : epEvs) -> - go cmds (conns ++ [(to, connId)]) (Map.insert to epEvs evs) - _ -> Just $ "Missing (ConnectionOpened <> <> <>) event in " ++ show evs + let epEvs = evs Map.! to + in case epEvs of + (ConnectionOpened connId _rel _addr : epEvs') -> + go cmds (conns ++ [(to, connId)]) (Map.insert to epEvs' evs) + _ -> + Just $ "Missing (ConnectionOpened <> <> <>) event in " ++ show evs go (Close connIx : cmds) conns evs = - let (epIx, connId) = conns !! connIx in - case evs Map.! epIx of - (ConnectionClosed connId' : epEvs) | connId' == connId -> - go cmds conns (Map.insert epIx epEvs evs) - _ -> Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs + let (epIx, connId) = conns !! connIx + epEvs = evs Map.! epIx + in case epEvs of + (ConnectionClosed connId' : epEvs') | connId' == connId -> + go cmds conns (Map.insert epIx epEvs' evs) + _ -> + Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs + go (Send connIx payload : cmds) conns evs = + let (epIx, connId) = conns !! connIx + epEvs = evs Map.! epIx + in case epEvs of + (Received connId' payload' : epEvs') | connId' == connId && BSS.concat payload == BSS.concat payload' -> + go cmds conns (Map.insert epIx epEvs' evs) + _ -> + Just $ "Missing (Received " ++ show connId ++ " " ++ show payload ++ ") event in " ++ show epEvs -------------------------------------------------------------------------------- -- Script generators -- @@ -153,6 +188,41 @@ script_ConnectClose numEndPoints = do isOpen :: Map Int Bool -> Int -> Bool isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx +script_ConnectSendClose :: Int -> Gen Script +script_ConnectSendClose numEndPoints = do + script <- go Map.empty + return (replicate numEndPoints NewEndPoint ++ script) + where + go :: Map Int Bool -> Gen Script + go conns = do + next <- choose (0, 3) :: Gen Int + case next of + 0 -> do + fr <- choose (0, numEndPoints - 1) + to <- choose (0, numEndPoints - 1) + cmds <- go (Map.insert (Map.size conns) True conns) + return (Connect fr to : cmds) + 1 -> do + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns + case mConn of + Nothing -> go conns + Just conn -> do + payload <- arbitrary + cmds <- go conns + return (Send conn payload : cmds) + 2 -> do + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns + case mConn of + Nothing -> go conns + Just conn -> do + cmds <- go (Map.insert conn False conns) + return (Close conn : cmds) + _ -> + return [] + + isOpen :: Map Int Bool -> Int -> Bool + isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx + -------------------------------------------------------------------------------- -- Individual scripts to test specific bugs -- -------------------------------------------------------------------------------- @@ -213,20 +283,30 @@ script_Bug1 = [ -- Main application driver -- -------------------------------------------------------------------------------- +basicTests :: Transport -> Int -> [Test] +basicTests transport numEndPoints = [ + testGen "NewEndPoint" transport (script_NewEndPoint 2) + , testGen "Connect" transport (script_Connect 2) + , testGen "ConnectClose" transport (script_ConnectClose 2) + , testGen "ConnectSendClose" transport (script_ConnectSendClose 2) + ] + tests :: Transport -> [Test] tests transport = [ testGroup "Bugs" [ - testOne "Bug1" script_Bug1 - ] - , testGroup "Unidirectional" [ - testQC "NewEndPoint" (script_NewEndPoint 2) - , testQC "Connect" (script_Connect 2) - , testQC "ConnectClose" (script_ConnectClose 2) + testOne "Bug1" transport script_Bug1 ] + , testGroup "One endpoint, with delays" (basicTests transport 1) + , testGroup "Two endpoints, with delays" (basicTests transport 2) + , testGroup "Three endpoints, with delays" (basicTests transport 3) ] where - testOne label script = testCase label (testScript transport script) - testQC label script = testProperty label (testScriptGen transport script) + +testOne :: TestName -> Transport -> Script -> Test +testOne label transport script = testCase label (testScript transport script) + +testGen :: TestName -> Transport -> Gen Script -> Test +testGen label transport script = testProperty label (testScriptGen transport script) main :: IO () main = do @@ -272,3 +352,8 @@ throwIfLeft p = do Left a -> throwIO a Right b -> return b +instance Arbitrary ByteString where + arbitrary = do + len <- choose (0, 10) + xs <- replicateM len arbitrary + return (pack xs) From ff76d063258147490b084616f7134cb7d2ea078c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 17 Oct 2012 13:13:28 +0100 Subject: [PATCH 0246/2357] Start introducing simulated network errors Script verification does not yet handle these errors --- src/Network/Transport/TCP/Mock/Socket.hs | 74 ++++++++++++++++---- tests/TestQC.hs | 87 +++++++++++++++++++----- 2 files changed, 129 insertions(+), 32 deletions(-) diff --git a/src/Network/Transport/TCP/Mock/Socket.hs b/src/Network/Transport/TCP/Mock/Socket.hs index 213135d4..489cb643 100644 --- a/src/Network/Transport/TCP/Mock/Socket.hs +++ b/src/Network/Transport/TCP/Mock/Socket.hs @@ -25,6 +25,8 @@ module Network.Transport.TCP.Mock.Socket , defaultHints , defaultProtocol , sOMAXCONN + -- * Debugging API + , scheduleReadAction -- * Internal API , writeSocket , readSocket @@ -39,7 +41,7 @@ import Control.Category ((>>>)) import Control.Concurrent.MVar import Control.Concurrent.Chan import System.IO.Unsafe (unsafePerformIO) -import Data.Accessor (Accessor, accessor, (^=), (^.)) +import Data.Accessor (Accessor, accessor, (^=), (^.), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe) import System.Timeout (timeout) @@ -105,8 +107,14 @@ data Socket = Socket { data SocketState = Uninit - | BoundSocket { socketBacklog :: Chan (Socket, SockAddr, MVar Socket) } - | Connected { socketPeer :: Maybe Socket, socketBuff :: Chan Message } + | BoundSocket { + socketBacklog :: Chan (Socket, SockAddr, MVar Socket) + } + | Connected { + socketBuff :: Chan Message + , _socketPeer :: Maybe Socket + , _scheduledReadActions :: [(Int, IO ())] + } | Closed data Message = @@ -127,6 +135,12 @@ instance Show AddrInfo where instance Show Socket where show sock = "<>" +socketPeer :: Accessor SocketState (Maybe Socket) +socketPeer = accessor _socketPeer (\peer st -> st { _socketPeer = peer }) + +scheduledReadActions :: Accessor SocketState [(Int, IO ())] +scheduledReadActions = accessor _scheduledReadActions (\acts st -> st { _scheduledReadActions = acts }) + getAddrInfo :: Maybe AddrInfo -> Maybe HostName -> Maybe ServiceName -> IO [AddrInfo] getAddrInfo _ (Just host) (Just port) = do validHosts <- get validHostnames @@ -183,8 +197,9 @@ accept serverSock = do (theirSocket, theirAddress, reply) <- readChan backlog ourBuff <- newChan ourState <- newMVar Connected { - socketPeer = Just theirSocket - , socketBuff = ourBuff + socketBuff = ourBuff + , _socketPeer = Just theirSocket + , _scheduledReadActions = [] } let ourSocket = Socket { socketState = ourState @@ -225,8 +240,9 @@ connect us serverAddr = do Uninit -> do buff <- newChan return Connected { - socketPeer = Just them - , socketBuff = buff + socketBuff = buff + , _socketPeer = Just them + , _scheduledReadActions = [] } _ -> throwSocketError "connect: already connected" @@ -240,7 +256,7 @@ shutdown sock ShutdownSend = do writeSocket sock CloseSocket timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> case st of Connected {} -> - return (Connected Nothing (socketBuff st)) + return (socketPeer ^= Nothing $ st) _ -> return st @@ -252,7 +268,7 @@ peerBuffer :: Socket -> IO (Either String (Chan Message)) peerBuffer sock = do mPeer <- timeoutThrow mvarThreshold $ withMVar (socketState sock) $ \st -> case st of Connected {} -> - return (socketPeer st) + return (st ^. socketPeer) _ -> return Nothing case mPeer of @@ -277,13 +293,17 @@ writeSocket sock msg = do readSocket :: Socket -> IO (Maybe Word8) readSocket sock = do - mBuff <- timeoutThrow mvarThreshold $ withMVar (socketState sock) $ \st -> case st of - Connected {} -> - return (Just $ socketBuff st) + mBuff <- timeoutThrow mvarThreshold $ modifyMVar (socketState sock) $ \st -> case st of + Connected {} -> do + let (later, now) = tick $ st ^. scheduledReadActions + return ( scheduledReadActions ^= later $ st + , Just (socketBuff st, now) + ) _ -> - return Nothing + return (st, Nothing) case mBuff of - Just buff -> do + Just (buff, actions) -> do + sequence actions msg <- timeoutThrow readSocketThreshold $ readChan buff case msg of Payload w -> return (Just w) @@ -295,6 +315,32 @@ readSocket sock = do Nothing -> return Nothing +-- | Given a list of scheduled actions, reduce all delays by 1, and return the +-- actions that should be executed now. +tick :: [(Int, IO ())] -> ([(Int, IO ())], [IO ()]) +tick = go [] [] + where + go later now [] = (reverse later, reverse now) + go later now ((n, action) : actions) + | n == 0 = go later (action : now) actions + | otherwise = go ((n - 1, action) : later) now actions + +-------------------------------------------------------------------------------- +-- Debugging API -- +-------------------------------------------------------------------------------- + +-- | Schedule an action to be executed after /n/ reads on this socket +-- +-- If /n/ is zero we execute the action immediately. +scheduleReadAction :: Socket -> Int -> IO () -> IO () +scheduleReadAction _ 0 action = action +scheduleReadAction sock n action = + modifyMVar_ (socketState sock) $ \st -> case st of + Connected {} -> + return (scheduledReadActions ^: ((n, action) :) $ st) + _ -> + throwSocketError "scheduleReadAction: socket not connected" + -------------------------------------------------------------------------------- -- Util -- -------------------------------------------------------------------------------- diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 9e0bd8c3..9054c1c0 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -17,6 +17,7 @@ import Test.QuickCheck , forAllShrink , Property , Arbitrary(arbitrary) + , elements ) import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) import Test.HUnit (Assertion, assertFailure) @@ -32,23 +33,47 @@ import Data.ByteString.Char8 (pack) import qualified Data.ByteString as BSS (concat) import Network.Transport -import Network.Transport.TCP (createTransport, defaultTCPParameters) +import Network.Transport.TCP + ( createTransportExposeInternals + , defaultTCPParameters + , TransportInternals(socketBetween) + ) + +import Network.Transport.TCP.Mock.Socket (scheduleReadAction, sClose) -------------------------------------------------------------------------------- -- Script infrastructure -- -------------------------------------------------------------------------------- +-- | We randomly generate /scripts/ which are essentially a deep embedding of +-- the Transport API. These scripts are then executed and the results compared +-- against an abstract interpreter. data ScriptCmd = + -- | Create a new endpoint NewEndPoint + -- | @Connect i j@ creates a connection from endpoint @i@ to endpoint @j@, + -- where @i@ and @j@ are indices and refer to the @i@th and @j@th endpoint + -- created by NewEndPoint | Connect Int Int + -- | @Close i@ closes the @i@ connection created using 'Connect'. Note that + -- closing a connection does not shift other indices; in other words, in + -- @[Connect 0 0, Close 0, Connect 0 0, Close 0]@ the second 'Close' + -- refers to the first (already closed) connection | Close Int + -- | @Send i bs@ sends payload @bs@ on the @i@ connection created | Send Int [ByteString] + -- | @BreakAfterReads n i j@ force-closes the socket between endpoints @i@ + -- and @j@ after @n@ reads by @i@ + -- + -- We should have @i /= j@ because the TCP transport does not use sockets + -- for connections from an endpoint to itself + | BreakAfterReads Int Int Int deriving Show type Script = [ScriptCmd] -execScript :: Transport -> Script -> IO (Map Int [Event]) -execScript transport script = do +execScript :: (Transport, TransportInternals) -> Script -> IO (Map Int [Event]) +execScript (transport, transportInternals) script = do chan <- newChan runScript chan script collectAll chan @@ -78,6 +103,10 @@ execScript transport script = do Right () <- send (conns !! connIx) payload threadDelay 10000 go endPoints conns cmds + go endPoints conns (BreakAfterReads n i j : cmds) = do + sock <- socketBetween transportInternals (address (endPoints !! i)) (address (endPoints !! j)) + scheduleReadAction sock n (sClose sock) + go endPoints conns cmds forwardTo :: Chan (Maybe (Int, Event)) -> (Int, EndPoint) -> IO () forwardTo chan (ix, endPoint) = go @@ -136,6 +165,8 @@ verify script = go script [] go cmds conns (Map.insert epIx epEvs' evs) _ -> Just $ "Missing (Received " ++ show connId ++ " " ++ show payload ++ ") event in " ++ show epEvs + go (BreakAfterReads n i j : cmds) conns evs = + go cmds conns evs -------------------------------------------------------------------------------- -- Script generators -- @@ -223,6 +254,25 @@ script_ConnectSendClose numEndPoints = do isOpen :: Map Int Bool -> Int -> Bool isOpen conns connIx = connIx `Map.member` conns && conns Map.! connIx +withErrors :: Int -> Gen Script -> Gen Script +withErrors numErrors gen = gen >>= insertError numErrors + where + insertError :: Int -> Script -> Gen Script + insertError _ [] = return [] + insertError n (Connect i j : cmds) | i /= j = do + insert <- arbitrary + if insert && n > 0 + then do + -- We sometimes want big delays, but usually we want short delays + numReads <- elements (concat (replicate 10 [0 .. 9]) ++ [10 ..99]) + return $ Connect i j : BreakAfterReads numReads i j : cmds + else do + cmds' <- insertError (n - 1) cmds + return $ Connect i j : cmds' + insertError n (cmd : cmds) = do + cmds' <- insertError n cmds + return $ cmd : cmds' + -------------------------------------------------------------------------------- -- Individual scripts to test specific bugs -- -------------------------------------------------------------------------------- @@ -283,41 +333,42 @@ script_Bug1 = [ -- Main application driver -- -------------------------------------------------------------------------------- -basicTests :: Transport -> Int -> [Test] -basicTests transport numEndPoints = [ - testGen "NewEndPoint" transport (script_NewEndPoint 2) - , testGen "Connect" transport (script_Connect 2) - , testGen "ConnectClose" transport (script_ConnectClose 2) - , testGen "ConnectSendClose" transport (script_ConnectSendClose 2) +basicTests :: (Transport, TransportInternals) -> Int -> (Gen Script -> Gen Script) -> [Test] +basicTests transport numEndPoints trans = [ + testGen "NewEndPoint" transport (trans (script_NewEndPoint numEndPoints)) + , testGen "Connect" transport (trans (script_Connect numEndPoints)) + , testGen "ConnectClose" transport (trans (script_ConnectClose numEndPoints)) + , testGen "ConnectSendClose" transport (trans (script_ConnectSendClose numEndPoints)) ] -tests :: Transport -> [Test] +tests :: (Transport, TransportInternals) -> [Test] tests transport = [ testGroup "Bugs" [ testOne "Bug1" transport script_Bug1 ] - , testGroup "One endpoint, with delays" (basicTests transport 1) - , testGroup "Two endpoints, with delays" (basicTests transport 2) - , testGroup "Three endpoints, with delays" (basicTests transport 3) + , testGroup "One endpoint, with delays" (basicTests transport 1 id) + , testGroup "Two endpoints, with delays" (basicTests transport 2 id) + , testGroup "Three endpoints, with delays" (basicTests transport 3 id) + , testGroup "Four endpoints, with delay, single error" (basicTests transport 4 (withErrors 1)) ] where -testOne :: TestName -> Transport -> Script -> Test +testOne :: TestName -> (Transport, TransportInternals) -> Script -> Test testOne label transport script = testCase label (testScript transport script) -testGen :: TestName -> Transport -> Gen Script -> Test +testGen :: TestName -> (Transport, TransportInternals) -> Gen Script -> Test testGen label transport script = testProperty label (testScriptGen transport script) main :: IO () main = do - Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters + Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters defaultMain (tests transport) -------------------------------------------------------------------------------- -- Test infrastructure -- -------------------------------------------------------------------------------- -testScriptGen :: Transport -> Gen Script -> Property +testScriptGen :: (Transport, TransportInternals) -> Gen Script -> Property testScriptGen transport scriptGen = forAll scriptGen $ \script -> morallyDubiousIOProperty $ do @@ -330,7 +381,7 @@ testScriptGen transport scriptGen = , reason = '\n' : err ++ "\nAll events: " ++ show evs } -testScript :: Transport -> Script -> Assertion +testScript :: (Transport, TransportInternals) -> Script -> Assertion testScript transport script = do logShow script evs <- execScript transport script From 927a9ea33be666f72aeead0c75f8b51dbaba6479 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 17 Oct 2012 15:49:39 +0100 Subject: [PATCH 0247/2357] Cleanup. --- network-transport-tcp.cabal | 7 +- tests/TestQC.hs | 142 +++++++++++++++++++++++++++++------- 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 12b23960..20732e27 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -68,6 +68,11 @@ Test-Suite TestQC network-transport, network-transport-tcp, containers, - bytestring + bytestring, + pretty ghc-options: -threaded -Wall -fno-warn-orphans HS-Source-Dirs: tests + Extensions: TypeSynonymInstances + FlexibleInstances + OverlappingInstances + OverloadedStrings diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 9054c1c0..cce6802f 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -1,11 +1,13 @@ module Main ( main -- Shush the compiler about unused definitions + , log , logShow , forAllShrink , inits ) where +import Prelude hiding (log) import Test.Framework (Test, TestName, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.Framework.Providers.HUnit (testCase) @@ -17,20 +19,21 @@ import Test.QuickCheck , forAllShrink , Property , Arbitrary(arbitrary) - , elements ) import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) import Test.HUnit (Assertion, assertFailure) import Data.Map (Map) import qualified Data.Map as Map +import Control.Applicative ((<$>)) import Control.Exception (Exception, throwIO) import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan) -import Control.Monad (replicateM) +import Control.Monad (replicateM, void) import Data.List (inits) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) import qualified Data.ByteString as BSS (concat) +import qualified Text.PrettyPrint as PP import Network.Transport import Network.Transport.TCP @@ -72,6 +75,26 @@ data ScriptCmd = type Script = [ScriptCmd] +verticalList :: [PP.Doc] -> PP.Doc +verticalList = PP.brackets . PP.vcat + +eventsDoc :: Map Int [Event] -> PP.Doc +eventsDoc = verticalList . map aux . Map.toList + where + aux :: (Int, [Event]) -> PP.Doc + aux (i, evs) = PP.parens . PP.hsep . PP.punctuate PP.comma $ [PP.int i, verticalList (map (PP.text . show) evs)] + + +instance Show Script where + show = ("\n" ++) . show . verticalList . map (PP.text . show) + +instance Show (Map Int [Event]) where + show = ("\n" ++) . show . eventsDoc + +-- | Execute a script +-- +-- Execute ignores error codes reported back. Instead, we verify the events +-- that are posted execScript :: (Transport, TransportInternals) -> Script -> IO (Map Int [Event]) execScript (transport, transportInternals) script = do chan <- newChan @@ -81,7 +104,7 @@ execScript (transport, transportInternals) script = do runScript :: Chan (Maybe (Int, Event)) -> Script -> IO () runScript chan = go [] [] where - go :: [EndPoint] -> [Connection] -> Script -> IO () + go :: [EndPoint] -> [Either (TransportError ConnectErrorCode) Connection] -> Script -> IO () go _endPoints _conns [] = do threadDelay 10000 writeChan chan Nothing @@ -92,20 +115,24 @@ execScript (transport, transportInternals) script = do threadDelay 10000 go (endPoints ++ [endPoint]) conns cmds go endPoints conns (Connect fr to : cmds) = do - conn <- throwIfLeft $ connect (endPoints !! fr) (address (endPoints !! to)) ReliableOrdered defaultConnectHints + conn <- connect (endPoints !! fr) (address (endPoints !! to)) ReliableOrdered defaultConnectHints threadDelay 10000 go endPoints (conns ++ [conn]) cmds go endPoints conns (Close connIx : cmds) = do - close (conns !! connIx) + case conns !! connIx of + Left _err -> return () + Right conn -> close conn threadDelay 10000 go endPoints conns cmds go endPoints conns (Send connIx payload : cmds) = do - Right () <- send (conns !! connIx) payload + case conns !! connIx of + Left _err -> return () + Right conn -> void $ send conn payload threadDelay 10000 go endPoints conns cmds go endPoints conns (BreakAfterReads n i j : cmds) = do sock <- socketBetween transportInternals (address (endPoints !! i)) (address (endPoints !! j)) - scheduleReadAction sock n (sClose sock) + scheduleReadAction sock n (putStrLn "Closing" >> sClose sock) go endPoints conns cmds forwardTo :: Chan (Maybe (Int, Event)) -> (Int, EndPoint) -> IO () @@ -133,40 +160,40 @@ execScript (transport, transportInternals) script = do insertEvent ev (Just evs) = Just (ev : evs) verify :: Script -> Map Int [Event] -> Maybe String -verify script = go script [] +verify = go [] where - go :: Script -> [(Int, ConnectionId)] -> Map Int [Event] -> Maybe String - go [] _conns evs = + go :: [(Int, ConnectionId)] -> Script -> Map Int [Event] -> Maybe String + go _conns [] evs = if concat (Map.elems evs) == [] then Nothing else Just $ "Unexpected events: " ++ show evs - go (NewEndPoint : cmds) conns evs = - go cmds conns evs - go (Connect _fr to : cmds) conns evs = + go conns (NewEndPoint : cmds) evs = + go conns cmds evs + go conns (Connect _fr to : cmds) evs = let epEvs = evs Map.! to in case epEvs of (ConnectionOpened connId _rel _addr : epEvs') -> - go cmds (conns ++ [(to, connId)]) (Map.insert to epEvs' evs) + go (conns ++ [(to, connId)]) cmds (Map.insert to epEvs' evs) _ -> Just $ "Missing (ConnectionOpened <> <> <>) event in " ++ show evs - go (Close connIx : cmds) conns evs = + go conns (Close connIx : cmds) evs = let (epIx, connId) = conns !! connIx epEvs = evs Map.! epIx in case epEvs of (ConnectionClosed connId' : epEvs') | connId' == connId -> - go cmds conns (Map.insert epIx epEvs' evs) + go conns cmds (Map.insert epIx epEvs' evs) _ -> Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs - go (Send connIx payload : cmds) conns evs = + go conns (Send connIx payload : cmds) evs = let (epIx, connId) = conns !! connIx epEvs = evs Map.! epIx in case epEvs of (Received connId' payload' : epEvs') | connId' == connId && BSS.concat payload == BSS.concat payload' -> - go cmds conns (Map.insert epIx epEvs' evs) + go conns cmds (Map.insert epIx epEvs' evs) _ -> Just $ "Missing (Received " ++ show connId ++ " " ++ show payload ++ ") event in " ++ show epEvs - go (BreakAfterReads n i j : cmds) conns evs = - go cmds conns evs + go conns (BreakAfterReads n i j : cmds) evs = + go conns cmds evs -------------------------------------------------------------------------------- -- Script generators -- @@ -238,7 +265,8 @@ script_ConnectSendClose numEndPoints = do case mConn of Nothing -> go conns Just conn -> do - payload <- arbitrary + numSegments <- choose (0, 2) + payload <- replicateM numSegments arbitrary cmds <- go conns return (Send conn payload : cmds) 2 -> do @@ -263,9 +291,11 @@ withErrors numErrors gen = gen >>= insertError numErrors insert <- arbitrary if insert && n > 0 then do - -- We sometimes want big delays, but usually we want short delays - numReads <- elements (concat (replicate 10 [0 .. 9]) ++ [10 ..99]) - return $ Connect i j : BreakAfterReads numReads i j : cmds + numReads <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) + swap <- arbitrary + if swap + then return $ Connect i j : BreakAfterReads numReads j i : cmds + else return $ Connect i j : BreakAfterReads numReads i j : cmds else do cmds' <- insertError (n - 1) cmds return $ Connect i j : cmds' @@ -329,6 +359,26 @@ script_Bug1 = [ , Connect 1 0 ] +-- | Simulate broken network connection during send +script_BreakSend :: Script +script_BreakSend = [ + NewEndPoint + , NewEndPoint + , Connect 0 1 + , BreakAfterReads 1 1 0 + , Send 0 ["ping"] + ] + +-- | Simulate broken network connection during connect +script_BreakConnect :: Script +script_BreakConnect = [ + NewEndPoint + , NewEndPoint + , Connect 0 1 + , BreakAfterReads 1 1 0 + , Connect 0 1 + ] + -------------------------------------------------------------------------------- -- Main application driver -- -------------------------------------------------------------------------------- @@ -343,8 +393,10 @@ basicTests transport numEndPoints trans = [ tests :: (Transport, TransportInternals) -> [Test] tests transport = [ - testGroup "Bugs" [ - testOne "Bug1" transport script_Bug1 + testGroup "Specific scripts" [ + testOne "Bug1" transport script_Bug1 + , testOne "BreakSend" transport script_BreakSend + , testOne "BreakConnect" transport script_BreakConnect ] , testGroup "One endpoint, with delays" (basicTests transport 1 id) , testGroup "Two endpoints, with delays" (basicTests transport 2 id) @@ -393,8 +445,11 @@ testScript transport script = do -- Auxiliary -------------------------------------------------------------------------------- +log :: String -> IO () +log = appendFile "log" . (++ "\n") + logShow :: Show a => a -> IO () -logShow = appendFile "log" . (++ "\n") . show +logShow = log . show throwIfLeft :: Exception a => IO (Either a b) -> IO b throwIfLeft p = do @@ -405,6 +460,37 @@ throwIfLeft p = do instance Arbitrary ByteString where arbitrary = do - len <- choose (0, 10) + len <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) xs <- replicateM len arbitrary return (pack xs) + +-------------------------------------------------------------------------------- +-- Draw random values from probability distributions -- +-------------------------------------------------------------------------------- + +data NormalD = NormalD { mean :: Double , stdDev :: Double } + +class Distribution d where + probabilityOf :: d -> Double -> Double + +instance Distribution NormalD where + probabilityOf d x = a * exp (-0.5 * b * b) + where + a = 1 / (stdDev d * sqrt (2 * pi)) + b = (x - mean d) / stdDev d + +-- | Choose from a distribution +chooseFrom :: Distribution d => d -> (Double, Double) -> Gen Double +chooseFrom d (lo, hi) = findCandidate + where + findCandidate :: Gen Double + findCandidate = do + candidate <- choose (lo, hi) + uniformSample <- choose (0, 1) + if uniformSample < probabilityOf d candidate + then return candidate + else findCandidate + +chooseFrom' :: Distribution d => d -> (Int, Int) -> Gen Int +chooseFrom' d (lo, hi) = + round <$> chooseFrom d (fromIntegral lo, fromIntegral hi) From c9940bc5670997ae801f8df50e38636226b7f541 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 18 Oct 2012 13:11:10 +0100 Subject: [PATCH 0248/2357] More support for dealing with errors However, current setup in the mock network implementation is insufficient, because it requires the existence of a socket before we can register an error. That means we can't simulate certain errors (for instance, failure when trying to create the first connection) because we can't hook into the network layer to get the socket at that point. --- network-transport-tcp.cabal | 3 +- tests/TestQC.hs | 217 +++++++++++++++++++++++------------- 2 files changed, 140 insertions(+), 80 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 20732e27..422505fb 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -69,7 +69,8 @@ Test-Suite TestQC network-transport-tcp, containers, bytestring, - pretty + pretty, + data-accessor ghc-options: -threaded -Wall -fno-warn-orphans HS-Source-Dirs: tests Extensions: TypeSynonymInstances diff --git a/tests/TestQC.hs b/tests/TestQC.hs index cce6802f..5f590703 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -24,14 +24,16 @@ import Test.QuickCheck.Property (morallyDubiousIOProperty, Result(..), result) import Test.HUnit (Assertion, assertFailure) import Data.Map (Map) import qualified Data.Map as Map +import Control.Category ((>>>)) import Control.Applicative ((<$>)) import Control.Exception (Exception, throwIO) import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan) import Control.Monad (replicateM, void) -import Data.List (inits) +import Data.List (inits, delete) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) +import Data.Accessor (Accessor, accessor, (^:), (^.), (^=)) import qualified Data.ByteString as BSS (concat) import qualified Text.PrettyPrint as PP @@ -48,6 +50,11 @@ import Network.Transport.TCP.Mock.Socket (scheduleReadAction, sClose) -- Script infrastructure -- -------------------------------------------------------------------------------- +type EndPointIx = Int +type SourceEndPointIx = Int +type TargetEndPointIx = Int +type ConnectionIx = Int + -- | We randomly generate /scripts/ which are essentially a deep embedding of -- the Transport API. These scripts are then executed and the results compared -- against an abstract interpreter. @@ -57,20 +64,20 @@ data ScriptCmd = -- | @Connect i j@ creates a connection from endpoint @i@ to endpoint @j@, -- where @i@ and @j@ are indices and refer to the @i@th and @j@th endpoint -- created by NewEndPoint - | Connect Int Int + | Connect SourceEndPointIx TargetEndPointIx -- | @Close i@ closes the @i@ connection created using 'Connect'. Note that -- closing a connection does not shift other indices; in other words, in -- @[Connect 0 0, Close 0, Connect 0 0, Close 0]@ the second 'Close' -- refers to the first (already closed) connection - | Close Int + | Close ConnectionIx -- | @Send i bs@ sends payload @bs@ on the @i@ connection created - | Send Int [ByteString] + | Send ConnectionIx [ByteString] -- | @BreakAfterReads n i j@ force-closes the socket between endpoints @i@ -- and @j@ after @n@ reads by @i@ -- -- We should have @i /= j@ because the TCP transport does not use sockets -- for connections from an endpoint to itself - | BreakAfterReads Int Int Int + | BreakAfterReads Int SourceEndPointIx TargetEndPointIx deriving Show type Script = [ScriptCmd] @@ -90,34 +97,40 @@ instance Show Script where instance Show (Map Int [Event]) where show = ("\n" ++) . show . eventsDoc - + +-------------------------------------------------------------------------------- +-- Execution -- +-------------------------------------------------------------------------------- + -- | Execute a script -- -- Execute ignores error codes reported back. Instead, we verify the events -- that are posted -execScript :: (Transport, TransportInternals) -> Script -> IO (Map Int [Event]) +execScript :: (Transport, TransportInternals) -> Script -> IO (Map EndPointIx [Event], VerificationState) execScript (transport, transportInternals) script = do chan <- newChan - runScript chan script - collectAll chan + vst <- runScript chan script + evs <- collectAll chan + return (evs, vst) where - runScript :: Chan (Maybe (Int, Event)) -> Script -> IO () + runScript :: Chan (Maybe (EndPointIx, Event)) -> Script -> IO VerificationState runScript chan = go [] [] where - go :: [EndPoint] -> [Either (TransportError ConnectErrorCode) Connection] -> Script -> IO () - go _endPoints _conns [] = do + go :: [EndPoint] -> [Either (TransportError ConnectErrorCode) Connection] -> Script -> IO VerificationState + go endPoints _conns [] = do threadDelay 10000 writeChan chan Nothing + return (initialVerificationState (map address endPoints)) go endPoints conns (NewEndPoint : cmds) = do endPoint <- throwIfLeft $ newEndPoint transport let endPointIx = length endPoints _tid <- forkIO $ forwardTo chan (endPointIx, endPoint) threadDelay 10000 - go (endPoints ++ [endPoint]) conns cmds + go (endPoint `snoc` endPoints) conns cmds go endPoints conns (Connect fr to : cmds) = do conn <- connect (endPoints !! fr) (address (endPoints !! to)) ReliableOrdered defaultConnectHints threadDelay 10000 - go endPoints (conns ++ [conn]) cmds + go endPoints (conn `snoc` conns) cmds go endPoints conns (Close connIx : cmds) = do case conns !! connIx of Left _err -> return () @@ -135,7 +148,7 @@ execScript (transport, transportInternals) script = do scheduleReadAction sock n (putStrLn "Closing" >> sClose sock) go endPoints conns cmds - forwardTo :: Chan (Maybe (Int, Event)) -> (Int, EndPoint) -> IO () + forwardTo :: Chan (Maybe (EndPointIx, Event)) -> (EndPointIx, EndPoint) -> IO () forwardTo chan (ix, endPoint) = go where go :: IO () @@ -145,7 +158,7 @@ execScript (transport, transportInternals) script = do EndPointClosed -> return () _ -> writeChan chan (Just (ix, ev)) >> go - collectAll :: Chan (Maybe (Int, Event)) -> IO (Map Int [Event]) + collectAll :: Chan (Maybe (EndPointIx, Event)) -> IO (Map EndPointIx [Event]) collectAll chan = go Map.empty where go :: Map Int [Event] -> IO (Map Int [Event]) @@ -159,41 +172,75 @@ execScript (transport, transportInternals) script = do insertEvent ev Nothing = Just [ev] insertEvent ev (Just evs) = Just (ev : evs) -verify :: Script -> Map Int [Event] -> Maybe String -verify = go [] - where - go :: [(Int, ConnectionId)] -> Script -> Map Int [Event] -> Maybe String - go _conns [] evs = - if concat (Map.elems evs) == [] - then Nothing - else Just $ "Unexpected events: " ++ show evs - go conns (NewEndPoint : cmds) evs = - go conns cmds evs - go conns (Connect _fr to : cmds) evs = - let epEvs = evs Map.! to - in case epEvs of - (ConnectionOpened connId _rel _addr : epEvs') -> - go (conns ++ [(to, connId)]) cmds (Map.insert to epEvs' evs) - _ -> - Just $ "Missing (ConnectionOpened <> <> <>) event in " ++ show evs - go conns (Close connIx : cmds) evs = - let (epIx, connId) = conns !! connIx - epEvs = evs Map.! epIx - in case epEvs of - (ConnectionClosed connId' : epEvs') | connId' == connId -> - go conns cmds (Map.insert epIx epEvs' evs) - _ -> - Just $ "Missing (ConnectionClosed " ++ show connId ++ ") event in " ++ show evs - go conns (Send connIx payload : cmds) evs = - let (epIx, connId) = conns !! connIx - epEvs = evs Map.! epIx - in case epEvs of - (Received connId' payload' : epEvs') | connId' == connId && BSS.concat payload == BSS.concat payload' -> - go conns cmds (Map.insert epIx epEvs' evs) - _ -> - Just $ "Missing (Received " ++ show connId ++ " " ++ show payload ++ ") event in " ++ show epEvs - go conns (BreakAfterReads n i j : cmds) evs = - go conns cmds evs +-------------------------------------------------------------------------------- +-- Verification -- +-------------------------------------------------------------------------------- + +data VerificationState = VerificationState { + endPointAddrs :: [EndPointAddress] + , _connections :: [(SourceEndPointIx, TargetEndPointIx, ConnectionId)] + , _mayBreak :: [(SourceEndPointIx, TargetEndPointIx)] + } + +initialVerificationState :: [EndPointAddress] -> VerificationState +initialVerificationState addrs = VerificationState { + endPointAddrs = addrs + , _connections = [] + , _mayBreak = [] + } + +-- TODO: we currently have no way to verify addresses in ConnectionOpened +-- or EventConnectionLost (because we don't know the addresses of the endpoints) +verify :: VerificationState -> Script -> Map EndPointIx [Event] -> Maybe String +verify _st [] evs = + -- TODO: we should compare the error events against mayBreak, but we + -- cannot because we don't know the endpoint addresses + if removeErrorEvents (concat (Map.elems evs)) == [] + then Nothing + else Just $ "Unexpected events: " ++ show evs +verify st (NewEndPoint : cmds) evs = + verify st cmds evs +verify st (Connect fr to : cmds) evs = + case destruct evs to of + Just (ConnectionOpened connId _rel _addr, evs') -> + verify (connections ^: snoc (fr, to, connId) $ st) cmds evs' + ev -> + Just $ "Missing (ConnectionOpened <> <> <>). Got " ++ show ev +verify st (Close connIx : cmds) evs = + let (_fr, to, connId) = st ^. connectionAt connIx in + case destruct evs to of + Just (ConnectionClosed connId', evs') | connId' == connId -> + verify st cmds evs' + ev -> + Just $ "Missing (ConnectionClosed " ++ show connId ++ "). Got " ++ show ev +verify st (Send connIx payload : cmds) evs = + let (fr, to, connId) = st ^. connectionAt connIx in + case destruct evs to of + Just (Received connId' payload', evs') | connId' == connId && BSS.concat payload == BSS.concat payload' -> + verify st cmds evs' + Just (ErrorEvent (TransportError (EventConnectionLost _addr) _), evs') | st ^. mayBreak fr to -> + verify st cmds evs' + ev -> + Just $ "Missing (Received " ++ show connId ++ " " ++ show payload ++ "). Got " ++ show ev +verify st (BreakAfterReads _n i j : cmds) evs = + verify (mayBreak i j ^= True $ st) cmds evs + +connections :: Accessor VerificationState [(SourceEndPointIx, TargetEndPointIx, ConnectionId)] +connections = accessor _connections (\cs st -> st { _connections = cs }) + +connectionAt :: ConnectionIx -> Accessor VerificationState (SourceEndPointIx, TargetEndPointIx, ConnectionId) +connectionAt i = connections >>> listAccessor i + +mayBreak :: EndPointIx -> EndPointIx -> Accessor VerificationState Bool +mayBreak i j = accessor + (\st -> (i, j) `elem` _mayBreak st || (j, i) `elem` _mayBreak st) + (\b st -> if b then st { _mayBreak = (i, j) : _mayBreak st } + else st { _mayBreak = delete (i, j) . delete (j, i) $ _mayBreak st }) + +removeErrorEvents :: [Event] -> [Event] +removeErrorEvents [] = [] +removeErrorEvents (ErrorEvent _ : evs) = removeErrorEvents evs +removeErrorEvents (ev : evs) = ev : removeErrorEvents evs -------------------------------------------------------------------------------- -- Script generators -- @@ -425,8 +472,8 @@ testScriptGen transport scriptGen = forAll scriptGen $ \script -> morallyDubiousIOProperty $ do logShow script - evs <- execScript transport script - return $ case verify script evs of + (evs, vst) <- execScript transport script + return $ case verify vst script evs of Nothing -> result { ok = Just True } Just err -> result { ok = Just False @@ -436,34 +483,11 @@ testScriptGen transport scriptGen = testScript :: (Transport, TransportInternals) -> Script -> Assertion testScript transport script = do logShow script - evs <- execScript transport script - case verify script evs of - Just err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err + (evs, vst) <- execScript transport script + case verify vst script evs of + Just err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err ++ "\nAll events: " ++ show evs Nothing -> return () --------------------------------------------------------------------------------- --- Auxiliary --------------------------------------------------------------------------------- - -log :: String -> IO () -log = appendFile "log" . (++ "\n") - -logShow :: Show a => a -> IO () -logShow = log . show - -throwIfLeft :: Exception a => IO (Either a b) -> IO b -throwIfLeft p = do - mb <- p - case mb of - Left a -> throwIO a - Right b -> return b - -instance Arbitrary ByteString where - arbitrary = do - len <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) - xs <- replicateM len arbitrary - return (pack xs) - -------------------------------------------------------------------------------- -- Draw random values from probability distributions -- -------------------------------------------------------------------------------- @@ -494,3 +518,38 @@ chooseFrom d (lo, hi) = findCandidate chooseFrom' :: Distribution d => d -> (Int, Int) -> Gen Int chooseFrom' d (lo, hi) = round <$> chooseFrom d (fromIntegral lo, fromIntegral hi) + +-------------------------------------------------------------------------------- +-- Auxiliary +-------------------------------------------------------------------------------- + +log :: String -> IO () +log = appendFile "log" . (++ "\n") + +logShow :: Show a => a -> IO () +logShow = log . show + +throwIfLeft :: Exception a => IO (Either a b) -> IO b +throwIfLeft p = do + mb <- p + case mb of + Left a -> throwIO a + Right b -> return b + +instance Arbitrary ByteString where + arbitrary = do + len <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) + xs <- replicateM len arbitrary + return (pack xs) + +listAccessor :: Int -> Accessor [a] a +listAccessor i = accessor (!! i) (error "listAccessor.set not defined") + +snoc :: a -> [a] -> [a] +snoc x xs = xs ++ [x] + +destruct :: Map EndPointIx [Event] -> EndPointIx -> Maybe (Event, Map EndPointIx [Event]) +destruct evs i = + case evs Map.! i of + [] -> Nothing + ev : evs' -> Just (ev, Map.insert i evs' evs) From 6804731bef96b979e71310da67154b85ffeb256c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 18 Oct 2012 17:26:42 +0100 Subject: [PATCH 0249/2357] Major re-engineering of the test infrastructure Compared to the previous version, this - Merges execution and verification of scripts. This means we can be more precise in what we actually test - Introduced an explicit data type for "Expected events" which can contain variables (existentials) for connection IDs. This makes it possible to specify the expected event when we open a new connection (we don't know what connection ID will be assigned, so we just create an existential) - Make reordering of events explicit: we introduce a function that creates all permissible reordering of events (that is, events across connections can be reordered, but events within the same connection cannot) We can now run all the tests that don't contain errors without explicit delays, so that unit tests run much faster, and we are testing much more thoroughly: by introducing delays at every step we greatly reduce concurrency; we can now even run test scripts in parallel (but first we need to reintroduce support for scripts with errors). --- network-transport-tcp.cabal | 6 +- tests/TestQC.hs | 399 ++++++++++++++++++++---------------- 2 files changed, 230 insertions(+), 175 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 422505fb..3e5590eb 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -70,7 +70,11 @@ Test-Suite TestQC containers, bytestring, pretty, - data-accessor + data-accessor, + data-accessor-transformers, + mtl, + transformers, + lockfree-queue ghc-options: -threaded -Wall -fno-warn-orphans HS-Source-Dirs: tests Extensions: TypeSynonymInstances diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 5f590703..38be3804 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -25,17 +25,24 @@ import Test.HUnit (Assertion, assertFailure) import Data.Map (Map) import qualified Data.Map as Map import Control.Category ((>>>)) +import Control.Arrow (second) import Control.Applicative ((<$>)) -import Control.Exception (Exception, throwIO) -import Control.Concurrent (forkIO, threadDelay) -import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan) -import Control.Monad (replicateM, void) -import Data.List (inits, delete) +import Control.Exception (throwIO) +import Control.Concurrent (forkIO, threadDelay, ThreadId, killThread) +import Control.Monad (replicateM, forever, guard) +import Control.Monad.State.Lazy (StateT, execStateT) +import Control.Monad.IO.Class (liftIO) +import Data.Maybe (isJust) +import Data.List (inits) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) -import Data.Accessor (Accessor, accessor, (^:), (^.), (^=)) +import Data.Accessor (Accessor, accessor, (^.)) +import Data.Accessor.Monad.Trans.State (get, modify) +import qualified Data.Accessor.Container as DAC (mapDefault) import qualified Data.ByteString as BSS (concat) import qualified Text.PrettyPrint as PP +import Data.Unique (Unique, newUnique, hashUnique) +import Data.Concurrent.Queue.MichaelScott (newQ, pushL, tryPopR) import Network.Transport import Network.Transport.TCP @@ -82,165 +89,179 @@ data ScriptCmd = type Script = [ScriptCmd] -verticalList :: [PP.Doc] -> PP.Doc -verticalList = PP.brackets . PP.vcat +-------------------------------------------------------------------------------- +-- Execute and verify scripts -- +-------------------------------------------------------------------------------- -eventsDoc :: Map Int [Event] -> PP.Doc -eventsDoc = verticalList . map aux . Map.toList - where - aux :: (Int, [Event]) -> PP.Doc - aux (i, evs) = PP.parens . PP.hsep . PP.punctuate PP.comma $ [PP.int i, verticalList (map (PP.text . show) evs)] +data Variable a = Value a | Variable Unique + deriving Eq +instance Show a => Show (Variable a) where + show (Value x) = show x + show (Variable u) = "<<" ++ show (hashUnique u) ++ ">>" -instance Show Script where - show = ("\n" ++) . show . verticalList . map (PP.text . show) +data ExpEvent = + ExpConnectionOpened (Variable ConnectionId) + | ExpConnectionClosed (Variable ConnectionId) + | ExpReceived (Variable ConnectionId) [ByteString] + deriving Show + +type TargetAddress = EndPointAddress -instance Show (Map Int [Event]) where - show = ("\n" ++) . show . eventsDoc +data RunState = RunState { + _endPoints :: [EndPoint] + , _connections :: [(TargetAddress, Connection, Variable ConnectionId)] + , _expectedEvents :: Map EndPointAddress [ExpEvent] + , _forwardingThreads :: [ThreadId] + } + +initialRunState :: RunState +initialRunState = RunState { + _endPoints = [] + , _connections = [] + , _expectedEvents = Map.empty + , _forwardingThreads = [] + } + +verify :: (Transport, TransportInternals) -> Script -> IO (Either String ()) +verify (transport, transportInternals) script = do + allEvents <- newQ + + let runScript :: Script -> StateT RunState IO () + runScript = mapM_ runCmd + + runCmd :: ScriptCmd -> StateT RunState IO () + runCmd NewEndPoint = do + mEndPoint <- liftIO $ newEndPoint transport + case mEndPoint of + Right endPoint -> do + modify endPoints (snoc endPoint) + tid <- liftIO $ forkIO (forward endPoint) + modify forwardingThreads (tid :) + Left err -> + liftIO $ throwIO err + runCmd (Connect i j) = do + endPointA <- get (endPointAtIx i) + endPointB <- address <$> get (endPointAtIx j) + mConn <- liftIO $ connect endPointA endPointB ReliableOrdered defaultConnectHints + case mConn of + Right conn -> do + connId <- Variable <$> liftIO newUnique + modify connections (snoc (endPointB, conn, connId)) + modify (expectedEventsAt endPointB) (snoc (ExpConnectionOpened connId)) + Left err -> + liftIO $ throwIO err + runCmd (Close i) = do + (target, conn, connId) <- get (connectionAt i) + liftIO $ close conn + modify (expectedEventsAt target) (snoc (ExpConnectionClosed connId)) + runCmd (Send i payload) = do + (target, conn, connId) <- get (connectionAt i) + mResult <- liftIO $ send conn payload + case mResult of + Right () -> return () + Left err -> liftIO $ throwIO err + modify (expectedEventsAt target) (snoc (ExpReceived connId payload)) --------------------------------------------------------------------------------- --- Execution -- --------------------------------------------------------------------------------- + forward :: EndPoint -> IO () + forward endPoint = forever $ do + ev <- receive endPoint + pushL allEvents (address endPoint, ev) --- | Execute a script --- --- Execute ignores error codes reported back. Instead, we verify the events --- that are posted -execScript :: (Transport, TransportInternals) -> Script -> IO (Map EndPointIx [Event], VerificationState) -execScript (transport, transportInternals) script = do - chan <- newChan - vst <- runScript chan script - evs <- collectAll chan - return (evs, vst) - where - runScript :: Chan (Maybe (EndPointIx, Event)) -> Script -> IO VerificationState - runScript chan = go [] [] - where - go :: [EndPoint] -> [Either (TransportError ConnectErrorCode) Connection] -> Script -> IO VerificationState - go endPoints _conns [] = do - threadDelay 10000 - writeChan chan Nothing - return (initialVerificationState (map address endPoints)) - go endPoints conns (NewEndPoint : cmds) = do - endPoint <- throwIfLeft $ newEndPoint transport - let endPointIx = length endPoints - _tid <- forkIO $ forwardTo chan (endPointIx, endPoint) - threadDelay 10000 - go (endPoint `snoc` endPoints) conns cmds - go endPoints conns (Connect fr to : cmds) = do - conn <- connect (endPoints !! fr) (address (endPoints !! to)) ReliableOrdered defaultConnectHints + collectEvents :: RunState -> IO (Map EndPointAddress [Event]) + collectEvents st = do threadDelay 10000 - go endPoints (conn `snoc` conns) cmds - go endPoints conns (Close connIx : cmds) = do - case conns !! connIx of - Left _err -> return () - Right conn -> close conn - threadDelay 10000 - go endPoints conns cmds - go endPoints conns (Send connIx payload : cmds) = do - case conns !! connIx of - Left _err -> return () - Right conn -> void $ send conn payload - threadDelay 10000 - go endPoints conns cmds - go endPoints conns (BreakAfterReads n i j : cmds) = do - sock <- socketBetween transportInternals (address (endPoints !! i)) (address (endPoints !! j)) - scheduleReadAction sock n (putStrLn "Closing" >> sClose sock) - go endPoints conns cmds - - forwardTo :: Chan (Maybe (EndPointIx, Event)) -> (EndPointIx, EndPoint) -> IO () - forwardTo chan (ix, endPoint) = go - where - go :: IO () - go = do - ev <- receive endPoint - case ev of - EndPointClosed -> return () - _ -> writeChan chan (Just (ix, ev)) >> go - - collectAll :: Chan (Maybe (EndPointIx, Event)) -> IO (Map EndPointIx [Event]) - collectAll chan = go Map.empty - where - go :: Map Int [Event] -> IO (Map Int [Event]) - go acc = do - mEv <- readChan chan - case mEv of - Nothing -> return $ Map.map reverse acc - Just (ix, ev) -> go (Map.alter (insertEvent ev) ix acc) - - insertEvent :: Event -> Maybe [Event] -> Maybe [Event] - insertEvent ev Nothing = Just [ev] - insertEvent ev (Just evs) = Just (ev : evs) + mapM_ killThread (st ^. forwardingThreads) + evs <- go [] + return (groupByKey evs) + where + go acc = do + mEv <- tryPopR allEvents + case mEv of + Just ev -> go (ev : acc) + Nothing -> return acc + + st <- execStateT (runScript script) initialRunState + actualEvents <- collectEvents st + + let eventsMatch = and . map (uncurry match) $ + zip (Map.elems (st ^. expectedEvents)) + (Map.elems actualEvents) + + return $ if eventsMatch + then Right () + else Left ("Could not match " ++ show (st ^. expectedEvents) + ++ " and " ++ show actualEvents) -------------------------------------------------------------------------------- --- Verification -- +-- Match expected and actual events -- -------------------------------------------------------------------------------- -data VerificationState = VerificationState { - endPointAddrs :: [EndPointAddress] - , _connections :: [(SourceEndPointIx, TargetEndPointIx, ConnectionId)] - , _mayBreak :: [(SourceEndPointIx, TargetEndPointIx)] - } - -initialVerificationState :: [EndPointAddress] -> VerificationState -initialVerificationState addrs = VerificationState { - endPointAddrs = addrs - , _connections = [] - , _mayBreak = [] - } - --- TODO: we currently have no way to verify addresses in ConnectionOpened --- or EventConnectionLost (because we don't know the addresses of the endpoints) -verify :: VerificationState -> Script -> Map EndPointIx [Event] -> Maybe String -verify _st [] evs = - -- TODO: we should compare the error events against mayBreak, but we - -- cannot because we don't know the endpoint addresses - if removeErrorEvents (concat (Map.elems evs)) == [] - then Nothing - else Just $ "Unexpected events: " ++ show evs -verify st (NewEndPoint : cmds) evs = - verify st cmds evs -verify st (Connect fr to : cmds) evs = - case destruct evs to of - Just (ConnectionOpened connId _rel _addr, evs') -> - verify (connections ^: snoc (fr, to, connId) $ st) cmds evs' - ev -> - Just $ "Missing (ConnectionOpened <> <> <>). Got " ++ show ev -verify st (Close connIx : cmds) evs = - let (_fr, to, connId) = st ^. connectionAt connIx in - case destruct evs to of - Just (ConnectionClosed connId', evs') | connId' == connId -> - verify st cmds evs' - ev -> - Just $ "Missing (ConnectionClosed " ++ show connId ++ "). Got " ++ show ev -verify st (Send connIx payload : cmds) evs = - let (fr, to, connId) = st ^. connectionAt connIx in - case destruct evs to of - Just (Received connId' payload', evs') | connId' == connId && BSS.concat payload == BSS.concat payload' -> - verify st cmds evs' - Just (ErrorEvent (TransportError (EventConnectionLost _addr) _), evs') | st ^. mayBreak fr to -> - verify st cmds evs' - ev -> - Just $ "Missing (Received " ++ show connId ++ " " ++ show payload ++ "). Got " ++ show ev -verify st (BreakAfterReads _n i j : cmds) evs = - verify (mayBreak i j ^= True $ st) cmds evs - -connections :: Accessor VerificationState [(SourceEndPointIx, TargetEndPointIx, ConnectionId)] -connections = accessor _connections (\cs st -> st { _connections = cs }) - -connectionAt :: ConnectionIx -> Accessor VerificationState (SourceEndPointIx, TargetEndPointIx, ConnectionId) -connectionAt i = connections >>> listAccessor i +-- | Match a list of expected events to a list of actual events, taking into +-- account that events may be reordered +match :: [ExpEvent] -> [Event] -> Bool +match expected actual = or (map (isJust . flip unify actual) (reorder expected)) + +-- | Match a list of expected events to a list of actual events, without doing +-- reordering +unify :: [ExpEvent] -> [Event] -> Maybe () +unify [] [] = return () +unify (ExpConnectionOpened connId : expected) (ConnectionOpened connId' _ _ : actual) = do + subst <- unifyConnectionId connId connId' + unify (apply subst expected) actual +unify (ExpConnectionClosed connId : expected) (ConnectionClosed connId' : actual) = do + subst <- unifyConnectionId connId connId' + unify (apply subst expected) actual +unify (ExpReceived connId payload : expected) (Received connId' payload' : actual) = do + guard (BSS.concat payload == BSS.concat payload') + subst <- unifyConnectionId connId connId' + unify (apply subst expected) actual +unify _ _ = fail "Cannot unify" + +type Substitution a = Map Unique a + +-- | Match two connection IDs +unifyConnectionId :: Variable ConnectionId -> ConnectionId -> Maybe (Substitution ConnectionId) +unifyConnectionId (Variable x) connId = Just $ Map.singleton x connId +unifyConnectionId (Value connId') connId | connId == connId' = Just Map.empty + | otherwise = Nothing + +-- | Apply a substitution +apply :: Substitution ConnectionId -> [ExpEvent] -> [ExpEvent] +apply subst = map applyEvent + where + applyEvent :: ExpEvent -> ExpEvent + applyEvent (ExpConnectionOpened connId) = ExpConnectionOpened (applyVar connId) + applyEvent (ExpConnectionClosed connId) = ExpConnectionClosed (applyVar connId) + applyEvent (ExpReceived connId payload) = ExpReceived (applyVar connId) payload + + applyVar :: Variable ConnectionId -> Variable ConnectionId + applyVar (Value connId) = Value connId + applyVar (Variable x) = case Map.lookup x subst of + Just connId -> Value connId + Nothing -> Variable x + +-- | Return all possible reorderings of a list of expected events +-- +-- Events from different connections can be reordered, but events from the +-- same connection cannot. +reorder :: [ExpEvent] -> [[ExpEvent]] +reorder = go + where + go :: [ExpEvent] -> [[ExpEvent]] + go [] = [[]] + go (ev : evs) = concat [insert ev evs' | evs' <- reorder evs] -mayBreak :: EndPointIx -> EndPointIx -> Accessor VerificationState Bool -mayBreak i j = accessor - (\st -> (i, j) `elem` _mayBreak st || (j, i) `elem` _mayBreak st) - (\b st -> if b then st { _mayBreak = (i, j) : _mayBreak st } - else st { _mayBreak = delete (i, j) . delete (j, i) $ _mayBreak st }) + insert :: ExpEvent -> [ExpEvent] -> [[ExpEvent]] + insert ev [] = [[ev]] + insert ev (ev' : evs') + | connectionId ev == connectionId ev' = [ev : ev' : evs'] + | otherwise = (ev : ev' : evs') : [ev' : evs'' | evs'' <- insert ev evs'] -removeErrorEvents :: [Event] -> [Event] -removeErrorEvents [] = [] -removeErrorEvents (ErrorEvent _ : evs) = removeErrorEvents evs -removeErrorEvents (ev : evs) = ev : removeErrorEvents evs + connectionId :: ExpEvent -> Variable ConnectionId + connectionId (ExpConnectionOpened connId) = connId + connectionId (ExpConnectionClosed connId) = connId + connectionId (ExpReceived connId _) = connId -------------------------------------------------------------------------------- -- Script generators -- @@ -472,21 +493,61 @@ testScriptGen transport scriptGen = forAll scriptGen $ \script -> morallyDubiousIOProperty $ do logShow script - (evs, vst) <- execScript transport script - return $ case verify vst script evs of - Nothing -> result { ok = Just True - } - Just err -> result { ok = Just False - , reason = '\n' : err ++ "\nAll events: " ++ show evs + mErr <- verify transport script + return $ case mErr of + Right () -> result { ok = Just True } + Left err -> result { ok = Just False + , reason = '\n' : err ++ "\n" } testScript :: (Transport, TransportInternals) -> Script -> Assertion testScript transport script = do logShow script - (evs, vst) <- execScript transport script - case verify vst script evs of - Just err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err ++ "\nAll events: " ++ show evs - Nothing -> return () + mErr <- verify transport script + case mErr of + Left err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err ++ "\n" + Right () -> return () + +-------------------------------------------------------------------------------- +-- Accessors -- +-------------------------------------------------------------------------------- + +endPoints :: Accessor RunState [EndPoint] +endPoints = accessor _endPoints (\es st -> st { _endPoints = es }) + +endPointAtIx :: EndPointIx -> Accessor RunState EndPoint +endPointAtIx i = endPoints >>> listAccessor i + +connections :: Accessor RunState [(TargetAddress, Connection, Variable ConnectionId)] +connections = accessor _connections (\cs st -> st { _connections = cs }) + +connectionAt :: ConnectionIx -> Accessor RunState (TargetAddress, Connection, Variable ConnectionId) +connectionAt i = connections >>> listAccessor i + +expectedEvents :: Accessor RunState (Map EndPointAddress [ExpEvent]) +expectedEvents = accessor _expectedEvents (\es st -> st { _expectedEvents = es }) + +expectedEventsAt :: EndPointAddress -> Accessor RunState [ExpEvent] +expectedEventsAt addr = expectedEvents >>> DAC.mapDefault [] addr + +forwardingThreads :: Accessor RunState [ThreadId] +forwardingThreads = accessor _forwardingThreads (\ts st -> st { _forwardingThreads = ts }) + +-------------------------------------------------------------------------------- +-- Pretty-printing -- +-------------------------------------------------------------------------------- + +verticalList :: Show a => [a] -> PP.Doc +verticalList = PP.brackets . PP.vcat . map (PP.text . show) + +instance Show Script where + show = ("\n" ++) . show . verticalList + +instance Show [Event] where + show = ("\n" ++) . show . verticalList + +instance Show [ExpEvent] where + show = ("\n" ++) . show . verticalList -------------------------------------------------------------------------------- -- Draw random values from probability distributions -- @@ -529,13 +590,6 @@ log = appendFile "log" . (++ "\n") logShow :: Show a => a -> IO () logShow = log . show -throwIfLeft :: Exception a => IO (Either a b) -> IO b -throwIfLeft p = do - mb <- p - case mb of - Left a -> throwIO a - Right b -> return b - instance Arbitrary ByteString where arbitrary = do len <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) @@ -548,8 +602,5 @@ listAccessor i = accessor (!! i) (error "listAccessor.set not defined") snoc :: a -> [a] -> [a] snoc x xs = xs ++ [x] -destruct :: Map EndPointIx [Event] -> EndPointIx -> Maybe (Event, Map EndPointIx [Event]) -destruct evs i = - case evs Map.! i of - [] -> Nothing - ev : evs' -> Just (ev, Map.insert i evs' evs) +groupByKey :: Ord a => [(a, b)] -> Map a [b] +groupByKey = Map.fromListWith (++) . map (second return) From 9b69beab9225903a7c5f3f1b94aad6af0a3f2c0c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 10:05:08 +0100 Subject: [PATCH 0250/2357] Prep for release 0.3.1. Add support for expected failures so that unimplemented unit tests show as Ok rather than Failed. Remove debugging print statement. --- ChangeLog | 5 ++++ network-transport-tcp.cabal | 3 ++- src/Network/Transport/TCP.hs | 1 - tests/TestQC.hs | 50 ++++++++++++++++++++++++++---------- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/ChangeLog b/ChangeLog index 17325a5e..d0725297 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2012-10-19 Edsko de Vries 0.3.1 + +* Bugfix. Reconnecting between endpoints did not work under certain +circumstances. + 2012-10-03 Edsko de Vries 0.3.0 * Implement new disconnection semantics diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 3e5590eb..673d7fa9 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -1,5 +1,5 @@ Name: network-transport-tcp -Version: 0.3.0 +Version: 0.3.1 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -81,3 +81,4 @@ Test-Suite TestQC FlexibleInstances OverlappingInstances OverloadedStrings + DeriveDataTypeable diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index 18ae2623..c3629611 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -946,7 +946,6 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- that case. if lastReceivedId < lastSentId vst then do - putStrLn "This really shouldn't happen" return (RemoteEndPointClosing resolved vst, Nothing) else do removeRemoteEndPoint (ourEndPoint, theirEndPoint) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 38be3804..5714f80c 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -27,11 +27,12 @@ import qualified Data.Map as Map import Control.Category ((>>>)) import Control.Arrow (second) import Control.Applicative ((<$>)) -import Control.Exception (throwIO) +import Control.Exception (Exception, throwIO, try) import Control.Concurrent (forkIO, threadDelay, ThreadId, killThread) import Control.Monad (replicateM, forever, guard) import Control.Monad.State.Lazy (StateT, execStateT) -import Control.Monad.IO.Class (liftIO) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Data.Typeable (Typeable) import Data.Maybe (isJust) import Data.List (inits) import Data.ByteString (ByteString) @@ -48,11 +49,9 @@ import Network.Transport import Network.Transport.TCP ( createTransportExposeInternals , defaultTCPParameters - , TransportInternals(socketBetween) + , TransportInternals ) -import Network.Transport.TCP.Mock.Socket (scheduleReadAction, sClose) - -------------------------------------------------------------------------------- -- Script infrastructure -- -------------------------------------------------------------------------------- @@ -124,7 +123,7 @@ initialRunState = RunState { } verify :: (Transport, TransportInternals) -> Script -> IO (Either String ()) -verify (transport, transportInternals) script = do +verify (transport, _transportInternals) script = do allEvents <- newQ let runScript :: Script -> StateT RunState IO () @@ -162,6 +161,8 @@ verify (transport, transportInternals) script = do Right () -> return () Left err -> liftIO $ throwIO err modify (expectedEventsAt target) (snoc (ExpReceived connId payload)) + runCmd (BreakAfterReads _n _i _j) = + expectedFailure "BreakAfterReads not implemented" forward :: EndPoint -> IO () forward endPoint = forever $ do @@ -493,20 +494,30 @@ testScriptGen transport scriptGen = forAll scriptGen $ \script -> morallyDubiousIOProperty $ do logShow script - mErr <- verify transport script + mErr <- try $ verify transport script return $ case mErr of - Right () -> result { ok = Just True } - Left err -> result { ok = Just False - , reason = '\n' : err ++ "\n" - } + Left (ExpectedFailure str) -> + result { ok = Nothing + , reason = str + } + Right (Left err) -> + result { ok = Just False + , reason = '\n' : err ++ "\n" + } + Right (Right ()) -> + result { ok = Just True } testScript :: (Transport, TransportInternals) -> Script -> Assertion testScript transport script = do logShow script - mErr <- verify transport script + mErr <- try $ verify transport script case mErr of - Left err -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err ++ "\n" - Right () -> return () + Left (ExpectedFailure _str) -> + return () + Right (Left err) -> + assertFailure $ "Failed with script " ++ show script ++ ": " ++ err ++ "\n" + Right (Right ()) -> + return () -------------------------------------------------------------------------------- -- Accessors -- @@ -604,3 +615,14 @@ snoc x xs = xs ++ [x] groupByKey :: Ord a => [(a, b)] -> Map a [b] groupByKey = Map.fromListWith (++) . map (second return) + +-------------------------------------------------------------------------------- +-- Expected failures (can't find explicit support for this in test-framework) -- +-------------------------------------------------------------------------------- + +data ExpectedFailure = ExpectedFailure String deriving (Typeable, Show) + +instance Exception ExpectedFailure + +expectedFailure :: MonadIO m => String -> m () +expectedFailure = liftIO . throwIO . ExpectedFailure From 6a1ca93d10a1a75d6ecac9168af1b85ea16a4683 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 10:24:05 +0100 Subject: [PATCH 0251/2357] Prep for release 0.1.0.1 --- ChangeLog | 4 ++++ network-transport-tests.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 4a414f5d..90be5897 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-10-19 Edsko de Vries 0.1.0.1 + +* Change CloseTransport test + 2012-10-03 Edsko de Vries 0.1.0 * Initial release (these tests used to be part of the individual transports) diff --git a/network-transport-tests.cabal b/network-transport-tests.cabal index c4a1de76..b4cf6f1b 100644 --- a/network-transport-tests.cabal +++ b/network-transport-tests.cabal @@ -1,5 +1,5 @@ name: network-transport-tests -version: 0.1.0.0 +version: 0.1.0.1 synopsis: Unit tests for Network.Transport implementations -- description: homepage: http://github.com/haskell-distributed/distributed-process From 792d31468795e46915ee1835bba9dd42af0f52cd Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 10:31:17 +0100 Subject: [PATCH 0252/2357] Require network-transport-tests >= 0.1.0.1 --- network-transport-tcp.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 673d7fa9..77478a73 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -45,7 +45,7 @@ Test-Suite TestTCP Type: exitcode-stdio-1.0 Main-Is: TestTCP.hs Build-Depends: base >= 4.3 && < 5, - network-transport-tests >= 0.1 && < 0.2, + network-transport-tests >= 0.1.0.1 && < 0.2, network >= 2.3 && < 2.5, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4 From 28690988a8de53918c1af7cd77497ddf0094e045 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 16:42:18 +0100 Subject: [PATCH 0253/2357] Introduce 'bundle' concept This is not quite right yet, because bundles should be per endpoint. --- network-transport-tcp.cabal | 37 +++-- tests/TestQC.hs | 292 +++++++++++++++++++++++++----------- 2 files changed, 222 insertions(+), 107 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 77478a73..7a946a46 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -59,22 +59,25 @@ Test-Suite TestTCP Test-Suite TestQC Type: exitcode-stdio-1.0 Main-Is: TestQC.hs - Build-Depends: base >= 4.3 && < 5, - test-framework, - test-framework-quickcheck2, - test-framework-hunit, - QuickCheck, - HUnit, - network-transport, - network-transport-tcp, - containers, - bytestring, - pretty, - data-accessor, - data-accessor-transformers, - mtl, - transformers, - lockfree-queue + If flag(use-mock-network) + Build-Depends: base >= 4.3 && < 5, + test-framework, + test-framework-quickcheck2, + test-framework-hunit, + QuickCheck, + HUnit, + network-transport, + network-transport-tcp, + containers, + bytestring, + pretty, + data-accessor, + data-accessor-transformers, + mtl, + transformers, + lockfree-queue + Else + Buildable: False ghc-options: -threaded -Wall -fno-warn-orphans HS-Source-Dirs: tests Extensions: TypeSynonymInstances @@ -82,3 +85,5 @@ Test-Suite TestQC OverlappingInstances OverloadedStrings DeriveDataTypeable + MultiParamTypeClasses + GeneralizedNewtypeDeriving diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 5714f80c..b334ef5e 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -5,6 +5,7 @@ module Main , logShow , forAllShrink , inits + , expectedFailure ) where import Prelude hiding (log) @@ -29,8 +30,8 @@ import Control.Arrow (second) import Control.Applicative ((<$>)) import Control.Exception (Exception, throwIO, try) import Control.Concurrent (forkIO, threadDelay, ThreadId, killThread) -import Control.Monad (replicateM, forever, guard) -import Control.Monad.State.Lazy (StateT, execStateT) +import Control.Monad (MonadPlus(..), replicateM, forever, guard) +import Control.Monad.State (StateT, execStateT) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Typeable (Typeable) import Data.Maybe (isJust) @@ -38,19 +39,22 @@ import Data.List (inits) import Data.ByteString (ByteString) import Data.ByteString.Char8 (pack) import Data.Accessor (Accessor, accessor, (^.)) -import Data.Accessor.Monad.Trans.State (get, modify) -import qualified Data.Accessor.Container as DAC (mapDefault) +import Data.Accessor.Monad.Trans.State (get, set, modify) +import qualified Data.Accessor.Container as DAC (set, mapDefault) import qualified Data.ByteString as BSS (concat) import qualified Text.PrettyPrint as PP import Data.Unique (Unique, newUnique, hashUnique) import Data.Concurrent.Queue.MichaelScott (newQ, pushL, tryPopR) +import Data.Set (Set) +import qualified Data.Set as Set import Network.Transport import Network.Transport.TCP ( createTransportExposeInternals , defaultTCPParameters - , TransportInternals + , TransportInternals(socketBetween) ) +import Network.Transport.TCP.Mock.Socket (scheduleReadAction, sClose) -------------------------------------------------------------------------------- -- Script infrastructure -- @@ -99,31 +103,46 @@ instance Show a => Show (Variable a) where show (Value x) = show x show (Variable u) = "<<" ++ show (hashUnique u) ++ ">>" +type BundleId = Int + +data ConnectionInfo = ConnectionInfo { + source :: EndPointAddress + , target :: EndPointAddress + , connectionId :: Variable ConnectionId + , bundleId :: BundleId + } + deriving Show + data ExpEvent = - ExpConnectionOpened (Variable ConnectionId) - | ExpConnectionClosed (Variable ConnectionId) - | ExpReceived (Variable ConnectionId) [ByteString] + ExpConnectionOpened ConnectionInfo + | ExpConnectionClosed ConnectionInfo + | ExpReceived ConnectionInfo [ByteString] + | ExpConnectionLost BundleId EndPointAddress deriving Show -type TargetAddress = EndPointAddress - data RunState = RunState { _endPoints :: [EndPoint] - , _connections :: [(TargetAddress, Connection, Variable ConnectionId)] + , _connections :: [(Connection, ConnectionInfo)] , _expectedEvents :: Map EndPointAddress [ExpEvent] , _forwardingThreads :: [ThreadId] + , _mayBreak :: Set BundleId + , _broken :: Set BundleId + , _currentBundle :: BundleId } initialRunState :: RunState initialRunState = RunState { - _endPoints = [] - , _connections = [] - , _expectedEvents = Map.empty + _endPoints = [] + , _connections = [] + , _expectedEvents = Map.empty , _forwardingThreads = [] + , _mayBreak = Set.empty + , _broken = Set.empty + , _currentBundle = 0 } verify :: (Transport, TransportInternals) -> Script -> IO (Either String ()) -verify (transport, _transportInternals) script = do +verify (transport, transportInternals) script = do allEvents <- newQ let runScript :: Script -> StateT RunState IO () @@ -145,24 +164,57 @@ verify (transport, _transportInternals) script = do mConn <- liftIO $ connect endPointA endPointB ReliableOrdered defaultConnectHints case mConn of Right conn -> do + bundleBroken <- get currentBundle >>= get . broken + currentBundleId <- if bundleBroken + then modify currentBundle (+ 1) >> get currentBundle + else get currentBundle connId <- Variable <$> liftIO newUnique - modify connections (snoc (endPointB, conn, connId)) - modify (expectedEventsAt endPointB) (snoc (ExpConnectionOpened connId)) - Left err -> - liftIO $ throwIO err + let connInfo = ConnectionInfo { + source = address endPointA + , target = endPointB + , connectionId = connId + , bundleId = currentBundleId + } + modify connections (snoc (conn, connInfo)) + modify (expectedEventsAt endPointB) (snoc (ExpConnectionOpened connInfo)) + Left err -> do + currentBundleId <- get currentBundle + expectingBreak <- get $ mayBreak currentBundleId + if expectingBreak + then do + set (mayBreak currentBundleId) False + set (broken currentBundleId) True + else + liftIO $ throwIO err runCmd (Close i) = do - (target, conn, connId) <- get (connectionAt i) - liftIO $ close conn - modify (expectedEventsAt target) (snoc (ExpConnectionClosed connId)) + (conn, connInfo) <- get (connectionAt i) + liftIO $ close conn + modify (expectedEventsAt (target connInfo)) (snoc (ExpConnectionClosed connInfo)) runCmd (Send i payload) = do - (target, conn, connId) <- get (connectionAt i) + (conn, connInfo) <- get (connectionAt i) mResult <- liftIO $ send conn payload case mResult of Right () -> return () - Left err -> liftIO $ throwIO err - modify (expectedEventsAt target) (snoc (ExpReceived connId payload)) - runCmd (BreakAfterReads _n _i _j) = - expectedFailure "BreakAfterReads not implemented" + Left err -> do + expectingBreak <- get $ mayBreak (bundleId connInfo) + isBroken <- get $ broken (bundleId connInfo) + if expectingBreak || isBroken + then do + set (mayBreak (bundleId connInfo)) False + set (broken (bundleId connInfo)) True + else + liftIO $ throwIO err + modify (expectedEventsAt (target connInfo)) (snoc (ExpReceived connInfo payload)) + runCmd (BreakAfterReads n i j) = do + endPointA <- address <$> get (endPointAtIx i) + endPointB <- address <$> get (endPointAtIx j) + liftIO $ do + sock <- socketBetween transportInternals endPointA endPointB + scheduleReadAction sock n (sClose sock) + currentBundleId <- get currentBundle + set (mayBreak currentBundleId) True + modify (expectedEventsAt endPointA) (snoc (ExpConnectionLost currentBundleId endPointB)) + modify (expectedEventsAt endPointB) (snoc (ExpConnectionLost currentBundleId endPointA)) forward :: EndPoint -> IO () forward endPoint = forever $ do @@ -185,7 +237,7 @@ verify (transport, _transportInternals) script = do st <- execStateT (runScript script) initialRunState actualEvents <- collectEvents st - let eventsMatch = and . map (uncurry match) $ + let eventsMatch = all (uncurry match) $ zip (Map.elems (st ^. expectedEvents)) (Map.elems actualEvents) @@ -201,68 +253,111 @@ verify (transport, _transportInternals) script = do -- | Match a list of expected events to a list of actual events, taking into -- account that events may be reordered match :: [ExpEvent] -> [Event] -> Bool -match expected actual = or (map (isJust . flip unify actual) (reorder expected)) - --- | Match a list of expected events to a list of actual events, without doing --- reordering -unify :: [ExpEvent] -> [Event] -> Maybe () -unify [] [] = return () -unify (ExpConnectionOpened connId : expected) (ConnectionOpened connId' _ _ : actual) = do - subst <- unifyConnectionId connId connId' - unify (apply subst expected) actual -unify (ExpConnectionClosed connId : expected) (ConnectionClosed connId' : actual) = do - subst <- unifyConnectionId connId connId' - unify (apply subst expected) actual -unify (ExpReceived connId payload : expected) (Received connId' payload' : actual) = do - guard (BSS.concat payload == BSS.concat payload') - subst <- unifyConnectionId connId connId' - unify (apply subst expected) actual -unify _ _ = fail "Cannot unify" - -type Substitution a = Map Unique a - --- | Match two connection IDs -unifyConnectionId :: Variable ConnectionId -> ConnectionId -> Maybe (Substitution ConnectionId) -unifyConnectionId (Variable x) connId = Just $ Map.singleton x connId -unifyConnectionId (Value connId') connId | connId == connId' = Just Map.empty - | otherwise = Nothing - --- | Apply a substitution -apply :: Substitution ConnectionId -> [ExpEvent] -> [ExpEvent] -apply subst = map applyEvent - where - applyEvent :: ExpEvent -> ExpEvent - applyEvent (ExpConnectionOpened connId) = ExpConnectionOpened (applyVar connId) - applyEvent (ExpConnectionClosed connId) = ExpConnectionClosed (applyVar connId) - applyEvent (ExpReceived connId payload) = ExpReceived (applyVar connId) payload - - applyVar :: Variable ConnectionId -> Variable ConnectionId - applyVar (Value connId) = Value connId - applyVar (Variable x) = case Map.lookup x subst of - Just connId -> Value connId - Nothing -> Variable x - --- | Return all possible reorderings of a list of expected events --- --- Events from different connections can be reordered, but events from the --- same connection cannot. -reorder :: [ExpEvent] -> [[ExpEvent]] -reorder = go +match expected actual = any (`canUnify` actual) (possibleTraces expected) + +possibleTraces :: [ExpEvent] -> [[ExpEvent]] +possibleTraces = go where - go :: [ExpEvent] -> [[ExpEvent]] - go [] = [[]] - go (ev : evs) = concat [insert ev evs' | evs' <- reorder evs] + go [] = [[]] + go (ev@(ExpConnectionLost _ _) : evs) = + [ trace | evs' <- possibleTraces evs, trace <- insertConnectionLost ev evs' ] + go (ev : evs) = + [ trace | evs' <- possibleTraces evs, trace <- insertEvent ev evs' ] + + -- We don't know when exactly the error will occur (indeed, it may never + -- happen at all), but it must occur before any future connection lost + -- event to the same destination. + -- If it occurs now, then all other events on this bundle will not happen. + insertConnectionLost :: ExpEvent -> [ExpEvent] -> [[ExpEvent]] + insertConnectionLost ev [] = [[ev], []] + insertConnectionLost ev@(ExpConnectionLost bid addr) (ev' : evs) = + (ev : removeBundle bid (ev' : evs)) : + case ev' of + ExpConnectionLost _ addr' | addr == addr' -> [] + _ -> [ev' : evs' | evs' <- insertConnectionLost ev evs] + insertConnectionLost _ _ = error "The impossible happened" + + -- All other events can be arbitrarily reordered /across/ connections, but + -- never /within/ connections + insertEvent :: ExpEvent -> [ExpEvent] -> [[ExpEvent]] + insertEvent ev [] = [[ev]] + insertEvent ev (ev' : evs) = + (ev : ev' : evs) : + if eventConnId ev == eventConnId ev' + then [] + else [ev' : evs' | evs' <- insertEvent ev evs] + + removeBundle :: BundleId -> [ExpEvent] -> [ExpEvent] + removeBundle bid = filter ((/= bid) . eventBundleId) + + eventBundleId :: ExpEvent -> BundleId + eventBundleId (ExpConnectionOpened connInfo) = bundleId connInfo + eventBundleId (ExpConnectionClosed connInfo) = bundleId connInfo + eventBundleId (ExpReceived connInfo _) = bundleId connInfo + eventBundleId (ExpConnectionLost bid _) = bid + + eventConnId :: ExpEvent -> Maybe (Variable ConnectionId) + eventConnId (ExpConnectionOpened connInfo) = Just $ connectionId connInfo + eventConnId (ExpConnectionClosed connInfo) = Just $ connectionId connInfo + eventConnId (ExpReceived connInfo _) = Just $ connectionId connInfo + eventConnId (ExpConnectionLost _ _) = Nothing - insert :: ExpEvent -> [ExpEvent] -> [[ExpEvent]] - insert ev [] = [[ev]] - insert ev (ev' : evs') - | connectionId ev == connectionId ev' = [ev : ev' : evs'] - | otherwise = (ev : ev' : evs') : [ev' : evs'' | evs'' <- insert ev evs'] +-------------------------------------------------------------------------------- +-- Unification -- +-------------------------------------------------------------------------------- - connectionId :: ExpEvent -> Variable ConnectionId - connectionId (ExpConnectionOpened connId) = connId - connectionId (ExpConnectionClosed connId) = connId - connectionId (ExpReceived connId _) = connId +type Substitution = Map Unique ConnectionId + +newtype Unifier a = Unifier { + runUnifier :: Substitution -> Maybe (a, Substitution) + } + +instance Monad Unifier where + return x = Unifier $ \subst -> Just (x, subst) + x >>= f = Unifier $ \subst -> case runUnifier x subst of + Nothing -> Nothing + Just (a, subst') -> runUnifier (f a) subst' + fail _str = mzero + +instance MonadPlus Unifier where + mzero = Unifier $ const Nothing + f `mplus` g = Unifier $ \subst -> case runUnifier f subst of + Nothing -> runUnifier g subst + Just (a, subst') -> Just (a, subst') + +class Unify a b where + unify :: a -> b -> Unifier () + +canUnify :: Unify a b => a -> b -> Bool +canUnify a b = isJust $ runUnifier (unify a b) Map.empty + +instance Unify Unique ConnectionId where + unify x cid = Unifier $ \subst -> + case Map.lookup x subst of + Just cid' -> if cid == cid' then Just ((), subst) + else Nothing + Nothing -> Just ((), Map.insert x cid subst) + +instance Unify (Variable ConnectionId) ConnectionId where + unify (Variable x) connId = unify x connId + unify (Value connId') connId = guard $ connId' == connId + +instance Unify ExpEvent Event where + unify (ExpConnectionOpened connInfo) (ConnectionOpened connId _ _) = + unify (connectionId connInfo) connId + unify (ExpConnectionClosed connInfo) (ConnectionClosed connId) = + unify (connectionId connInfo) connId + unify (ExpReceived connInfo payload) (Received connId payload') = do + guard $ BSS.concat payload == BSS.concat payload' + unify (connectionId connInfo) connId + unify (ExpConnectionLost _ addr) (ErrorEvent (TransportError (EventConnectionLost addr') _)) = + guard $ addr == addr' + unify _ _ = fail "Cannot unify" + +instance Unify a b => Unify [a] [b] where + unify [] [] = return () + unify (x:xs) (y:ys) = unify x y >> unify xs ys + unify _ _ = fail "Cannot unify" -------------------------------------------------------------------------------- -- Script generators -- @@ -360,7 +455,7 @@ withErrors numErrors gen = gen >>= insertError numErrors insert <- arbitrary if insert && n > 0 then do - numReads <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) + numReads <- chooseFrom' NormalD { mean = 5, stdDev = 10 } (0, 100) swap <- arbitrary if swap then return $ Connect i j : BreakAfterReads numReads j i : cmds @@ -529,10 +624,10 @@ endPoints = accessor _endPoints (\es st -> st { _endPoints = es }) endPointAtIx :: EndPointIx -> Accessor RunState EndPoint endPointAtIx i = endPoints >>> listAccessor i -connections :: Accessor RunState [(TargetAddress, Connection, Variable ConnectionId)] +connections :: Accessor RunState [(Connection, ConnectionInfo)] connections = accessor _connections (\cs st -> st { _connections = cs }) -connectionAt :: ConnectionIx -> Accessor RunState (TargetAddress, Connection, Variable ConnectionId) +connectionAt :: ConnectionIx -> Accessor RunState (Connection, ConnectionInfo) connectionAt i = connections >>> listAccessor i expectedEvents :: Accessor RunState (Map EndPointAddress [ExpEvent]) @@ -544,6 +639,21 @@ expectedEventsAt addr = expectedEvents >>> DAC.mapDefault [] addr forwardingThreads :: Accessor RunState [ThreadId] forwardingThreads = accessor _forwardingThreads (\ts st -> st { _forwardingThreads = ts }) +mayBreak :: BundleId -> Accessor RunState Bool +mayBreak bid = aux >>> DAC.set bid + where + aux :: Accessor RunState (Set BundleId) + aux = accessor _mayBreak (\bs st -> st { _mayBreak = bs }) + +broken :: BundleId -> Accessor RunState Bool +broken bid = aux >>> DAC.set bid + where + aux :: Accessor RunState (Set BundleId) + aux = accessor _broken (\bs st -> st { _broken = bs }) + +currentBundle :: Accessor RunState BundleId +currentBundle = accessor _currentBundle (\bid st -> st { _currentBundle = bid }) + -------------------------------------------------------------------------------- -- Pretty-printing -- -------------------------------------------------------------------------------- @@ -603,7 +713,7 @@ logShow = log . show instance Arbitrary ByteString where arbitrary = do - len <- chooseFrom' (NormalD { mean = 5, stdDev = 10 }) (0, 100) + len <- chooseFrom' NormalD { mean = 5, stdDev = 10 } (0, 100) xs <- replicateM len arbitrary return (pack xs) From 1a724101039e916bed4c8351832220d77de7545b Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 17:07:09 +0100 Subject: [PATCH 0254/2357] Bundle per pair of endpoints Not all unit tests pass yet though. --- tests/TestQC.hs | 84 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index b334ef5e..d3bcd058 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -103,13 +103,20 @@ instance Show a => Show (Variable a) where show (Value x) = show x show (Variable u) = "<<" ++ show (hashUnique u) ++ ">>" -type BundleId = Int +-- | In the implementation "bundles" are purely a conceptual idea, but in the +-- verifier we need to concretize this notion +-- +-- Invariant: first endpoint address < second endpoint address +type BundleId = (EndPointAddress, EndPointAddress, Int) + +incrementBundleId :: BundleId -> BundleId +incrementBundleId (a, b, i) = (a, b, i + 1) data ConnectionInfo = ConnectionInfo { - source :: EndPointAddress - , target :: EndPointAddress - , connectionId :: Variable ConnectionId - , bundleId :: BundleId + source :: EndPointAddress + , target :: EndPointAddress + , connectionId :: Variable ConnectionId + , connectionBundle :: BundleId } deriving Show @@ -125,9 +132,10 @@ data RunState = RunState { , _connections :: [(Connection, ConnectionInfo)] , _expectedEvents :: Map EndPointAddress [ExpEvent] , _forwardingThreads :: [ThreadId] + -- | Invariant: not mayBreak && broken , _mayBreak :: Set BundleId , _broken :: Set BundleId - , _currentBundle :: BundleId + , _currentBundle :: Map (EndPointAddress, EndPointAddress) BundleId } initialRunState :: RunState @@ -138,7 +146,7 @@ initialRunState = RunState { , _forwardingThreads = [] , _mayBreak = Set.empty , _broken = Set.empty - , _currentBundle = 0 + , _currentBundle = Map.empty } verify :: (Transport, TransportInternals) -> Script -> IO (Either String ()) @@ -153,8 +161,8 @@ verify (transport, transportInternals) script = do mEndPoint <- liftIO $ newEndPoint transport case mEndPoint of Right endPoint -> do - modify endPoints (snoc endPoint) tid <- liftIO $ forkIO (forward endPoint) + modify endPoints (snoc endPoint) modify forwardingThreads (tid :) Left err -> liftIO $ throwIO err @@ -162,23 +170,24 @@ verify (transport, transportInternals) script = do endPointA <- get (endPointAtIx i) endPointB <- address <$> get (endPointAtIx j) mConn <- liftIO $ connect endPointA endPointB ReliableOrdered defaultConnectHints + let bundleId = currentBundle (address endPointA) endPointB case mConn of Right conn -> do - bundleBroken <- get currentBundle >>= get . broken + bundleBroken <- get bundleId >>= get . broken currentBundleId <- if bundleBroken - then modify currentBundle (+ 1) >> get currentBundle - else get currentBundle + then modify bundleId incrementBundleId >> get bundleId + else get bundleId connId <- Variable <$> liftIO newUnique let connInfo = ConnectionInfo { - source = address endPointA - , target = endPointB - , connectionId = connId - , bundleId = currentBundleId + source = address endPointA + , target = endPointB + , connectionId = connId + , connectionBundle = currentBundleId } modify connections (snoc (conn, connInfo)) modify (expectedEventsAt endPointB) (snoc (ExpConnectionOpened connInfo)) Left err -> do - currentBundleId <- get currentBundle + currentBundleId <- get bundleId expectingBreak <- get $ mayBreak currentBundleId if expectingBreak then do @@ -196,12 +205,12 @@ verify (transport, transportInternals) script = do case mResult of Right () -> return () Left err -> do - expectingBreak <- get $ mayBreak (bundleId connInfo) - isBroken <- get $ broken (bundleId connInfo) + expectingBreak <- get $ mayBreak (connectionBundle connInfo) + isBroken <- get $ broken (connectionBundle connInfo) if expectingBreak || isBroken then do - set (mayBreak (bundleId connInfo)) False - set (broken (bundleId connInfo)) True + set (mayBreak (connectionBundle connInfo)) False + set (broken (connectionBundle connInfo)) True else liftIO $ throwIO err modify (expectedEventsAt (target connInfo)) (snoc (ExpReceived connInfo payload)) @@ -211,7 +220,7 @@ verify (transport, transportInternals) script = do liftIO $ do sock <- socketBetween transportInternals endPointA endPointB scheduleReadAction sock n (sClose sock) - currentBundleId <- get currentBundle + currentBundleId <- get (currentBundle endPointA endPointB) set (mayBreak currentBundleId) True modify (expectedEventsAt endPointA) (snoc (ExpConnectionLost currentBundleId endPointB)) modify (expectedEventsAt endPointB) (snoc (ExpConnectionLost currentBundleId endPointA)) @@ -291,9 +300,9 @@ possibleTraces = go removeBundle bid = filter ((/= bid) . eventBundleId) eventBundleId :: ExpEvent -> BundleId - eventBundleId (ExpConnectionOpened connInfo) = bundleId connInfo - eventBundleId (ExpConnectionClosed connInfo) = bundleId connInfo - eventBundleId (ExpReceived connInfo _) = bundleId connInfo + eventBundleId (ExpConnectionOpened connInfo) = connectionBundle connInfo + eventBundleId (ExpConnectionClosed connInfo) = connectionBundle connInfo + eventBundleId (ExpReceived connInfo _) = connectionBundle connInfo eventBundleId (ExpConnectionLost bid _) = bid eventConnId :: ExpEvent -> Maybe (Variable ConnectionId) @@ -543,6 +552,18 @@ script_BreakConnect = [ , Connect 0 1 ] +-- | Simulate broken send, then reconnect +script_BreakSendReconnect :: Script +script_BreakSendReconnect = [ + NewEndPoint + , NewEndPoint + , Connect 0 1 + , BreakAfterReads 1 1 0 + , Send 0 ["ping1"] + , Connect 0 1 + , Send 1 ["ping2"] + ] + -------------------------------------------------------------------------------- -- Main application driver -- -------------------------------------------------------------------------------- @@ -558,9 +579,10 @@ basicTests transport numEndPoints trans = [ tests :: (Transport, TransportInternals) -> [Test] tests transport = [ testGroup "Specific scripts" [ - testOne "Bug1" transport script_Bug1 - , testOne "BreakSend" transport script_BreakSend - , testOne "BreakConnect" transport script_BreakConnect + testOne "Bug1" transport script_Bug1 + , testOne "BreakSend" transport script_BreakSend + , testOne "BreakConnect" transport script_BreakConnect + , testOne "BreakSendReconnect" transport script_BreakSendReconnect ] , testGroup "One endpoint, with delays" (basicTests transport 1 id) , testGroup "Two endpoints, with delays" (basicTests transport 2 id) @@ -651,8 +673,12 @@ broken bid = aux >>> DAC.set bid aux :: Accessor RunState (Set BundleId) aux = accessor _broken (\bs st -> st { _broken = bs }) -currentBundle :: Accessor RunState BundleId -currentBundle = accessor _currentBundle (\bid st -> st { _currentBundle = bid }) +currentBundle :: EndPointAddress -> EndPointAddress -> Accessor RunState BundleId +currentBundle i j = aux >>> if i < j then DAC.mapDefault (i, j, 0) (i, j) + else DAC.mapDefault (j, i, 0) (j, i) + where + aux :: Accessor RunState (Map (EndPointAddress, EndPointAddress) BundleId) + aux = accessor _currentBundle (\mp st -> st { _currentBundle = mp }) -------------------------------------------------------------------------------- -- Pretty-printing -- From b203b22f8947a697b6ce397fe08cfaff95a9dd89 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 18:00:11 +0100 Subject: [PATCH 0255/2357] Fix groupByKey make sure that endpoints get at least an associated empty list of events, so that the actual events and expected events ahve the same set of keys (that is, the addresses of all created endpoints) --- network-transport-tcp.cabal | 1 - tests/TestQC.hs | 63 +++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 7a946a46..38ddf82c 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -86,4 +86,3 @@ Test-Suite TestQC OverloadedStrings DeriveDataTypeable MultiParamTypeClasses - GeneralizedNewtypeDeriving diff --git a/tests/TestQC.hs b/tests/TestQC.hs index d3bcd058..9750eb05 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -26,7 +26,6 @@ import Test.HUnit (Assertion, assertFailure) import Data.Map (Map) import qualified Data.Map as Map import Control.Category ((>>>)) -import Control.Arrow (second) import Control.Applicative ((<$>)) import Control.Exception (Exception, throwIO, try) import Control.Concurrent (forkIO, threadDelay, ThreadId, killThread) @@ -164,6 +163,7 @@ verify (transport, transportInternals) script = do tid <- liftIO $ forkIO (forward endPoint) modify endPoints (snoc endPoint) modify forwardingThreads (tid :) + set (expectedEventsAt (address endPoint)) [] Left err -> liftIO $ throwIO err runCmd (Connect i j) = do @@ -235,7 +235,7 @@ verify (transport, transportInternals) script = do threadDelay 10000 mapM_ killThread (st ^. forwardingThreads) evs <- go [] - return (groupByKey evs) + return $ groupByKey (map address (st ^. endPoints)) evs where go acc = do mEv <- tryPopR allEvents @@ -543,8 +543,8 @@ script_BreakSend = [ ] -- | Simulate broken network connection during connect -script_BreakConnect :: Script -script_BreakConnect = [ +script_BreakConnect1 :: Script +script_BreakConnect1 = [ NewEndPoint , NewEndPoint , Connect 0 1 @@ -552,6 +552,16 @@ script_BreakConnect = [ , Connect 0 1 ] +-- | Simulate broken network connection during connect +script_BreakConnect2 :: Script +script_BreakConnect2 = [ + NewEndPoint + , NewEndPoint + , Connect 0 1 + , BreakAfterReads 1 0 1 + , Connect 0 1 + ] + -- | Simulate broken send, then reconnect script_BreakSendReconnect :: Script script_BreakSendReconnect = [ @@ -578,16 +588,24 @@ basicTests transport numEndPoints trans = [ tests :: (Transport, TransportInternals) -> [Test] tests transport = [ - testGroup "Specific scripts" [ - testOne "Bug1" transport script_Bug1 - , testOne "BreakSend" transport script_BreakSend - , testOne "BreakConnect" transport script_BreakConnect - , testOne "BreakSendReconnect" transport script_BreakSendReconnect - ] - , testGroup "One endpoint, with delays" (basicTests transport 1 id) - , testGroup "Two endpoints, with delays" (basicTests transport 2 id) - , testGroup "Three endpoints, with delays" (basicTests transport 3 id) - , testGroup "Four endpoints, with delay, single error" (basicTests transport 4 (withErrors 1)) + testGroup "Regression tests" [ + testOne "Bug1" transport script_Bug1 + ] + , testGroup "Specific scripts" [ + testOne "BreakSend" transport script_BreakSend + , testOne "BreakConnect1" transport script_BreakConnect1 + , testOne "BreakConnect2" transport script_BreakConnect2 + , testOne "BreakSendReconnect" transport script_BreakSendReconnect + ] + , testGroup "Without errors" [ + testGroup "One endpoint, with delays" (basicTests transport 1 id) + , testGroup "Two endpoints, with delays" (basicTests transport 2 id) + , testGroup "Three endpoints, with delays" (basicTests transport 3 id) + ] + , testGroup "Single error" [ + testGroup "Two endpoints, with delays" (basicTests transport 2 (withErrors 1)) + , testGroup "Three endpoints, with delays" (basicTests transport 3 (withErrors 1)) + ] ] where @@ -696,6 +714,16 @@ instance Show [Event] where instance Show [ExpEvent] where show = ("\n" ++) . show . verticalList +instance Show (Map EndPointAddress [ExpEvent]) where + show = ("\n" ++) . show . PP.brackets . PP.vcat + . map (\(addr, evs) -> PP.hcat . PP.punctuate PP.comma $ [PP.text (show addr), verticalList evs]) + . Map.toList + +instance Show (Map EndPointAddress [Event]) where + show = ("\n" ++) . show . PP.brackets . PP.vcat + . map (\(addr, evs) -> PP.hcat . PP.punctuate PP.comma $ [PP.text (show addr), verticalList evs]) + . Map.toList + -------------------------------------------------------------------------------- -- Draw random values from probability distributions -- -------------------------------------------------------------------------------- @@ -749,8 +777,11 @@ listAccessor i = accessor (!! i) (error "listAccessor.set not defined") snoc :: a -> [a] -> [a] snoc x xs = xs ++ [x] -groupByKey :: Ord a => [(a, b)] -> Map a [b] -groupByKey = Map.fromListWith (++) . map (second return) +groupByKey :: Ord a => [a] -> [(a, b)] -> Map a [b] +groupByKey keys = go (Map.fromList [(key, []) | key <- keys]) + where + go acc [] = Map.map reverse acc + go acc ((key, val) : rest) = go (Map.adjust (val :) key acc) rest -------------------------------------------------------------------------------- -- Expected failures (can't find explicit support for this in test-framework) -- From 33c0c15f9da279d1f8f823a0aa5f7804f56718bf Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 19 Oct 2012 18:07:54 +0100 Subject: [PATCH 0256/2357] Collect events in the right order (doh) --- tests/TestQC.hs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 9750eb05..9e0d4748 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -241,7 +241,7 @@ verify (transport, transportInternals) script = do mEv <- tryPopR allEvents case mEv of Just ev -> go (ev : acc) - Nothing -> return acc + Nothing -> return (reverse acc) st <- execStateT (runScript script) initialRunState actualEvents <- collectEvents st @@ -532,6 +532,18 @@ script_Bug1 = [ , Connect 1 0 ] +-- | Test ordering of sends +script_MultipleSends :: Script +script_MultipleSends = [ + NewEndPoint + , Connect 0 0 + , Send 0 ["A"] + , Send 0 ["B"] + , Send 0 ["C"] + , Send 0 ["D"] + , Send 0 ["E"] + ] + -- | Simulate broken network connection during send script_BreakSend :: Script script_BreakSend = [ @@ -592,7 +604,8 @@ tests transport = [ testOne "Bug1" transport script_Bug1 ] , testGroup "Specific scripts" [ - testOne "BreakSend" transport script_BreakSend + testOne "BreakMultipleSends" transport script_MultipleSends + , testOne "BreakSend" transport script_BreakSend , testOne "BreakConnect1" transport script_BreakConnect1 , testOne "BreakConnect2" transport script_BreakConnect2 , testOne "BreakSendReconnect" transport script_BreakSendReconnect From b3dee891d4ede717ec3f1b7e5b94f79a4808e27c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 19 Oct 2012 10:23:02 -0700 Subject: [PATCH 0257/2357] Initial commit --- .gitignore | 6 ++++++ README.md | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b6837ebd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +dist +cabal-dev +*.o +*.hi +*.chi +*.chs.h diff --git a/README.md b/README.md new file mode 100644 index 00000000..f6091398 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +distributed-process-platform +============================ \ No newline at end of file From 8c784e354370b91ed27327efd26300889805c310 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 22 Oct 2012 09:50:27 +0100 Subject: [PATCH 0258/2357] Fix .cabal file (test didn't build) --- ChangeLog | 4 +++ distributed-process-simplelocalnet.cabal | 38 ++++++++++++++---------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ChangeLog b/ChangeLog index e8721ef4..ce0d8bf4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +<> Edsko de Vries 0.2.0.7 + +* Fix cabal script so that the example program compiles + 2012-10-03 Edsko de Vries 0.2.0.6 * Use new version of network-transport diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index c74fa8e6..3cf8f481 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.6 +Version: 0.2.0.7 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -23,6 +23,10 @@ Source-Repository head Location: https://github.com/haskell-distributed/distributed-process SubDir: distributed-process-simplelocalnet +Flag build-example + Default: False + Description: Build a simple example application + Library Build-Depends: base >= 4.4 && < 5, bytestring >= 0.9 && < 0.11, @@ -43,22 +47,24 @@ Library ghc-options: -Wall HS-Source-Dirs: src --- Not a proper test, but we want to use cabal to compile it -Test-Suite TestSimpleLocalnet - Type: exitcode-stdio-1.0 +Executable TestSimpleLocalnet Main-Is: TestSimpleLocalnet.hs - Build-Depends: base >= 4.4 && < 5, - bytestring >= 0.9 && < 0.11, - network >= 2.3 && < 2.5, - network-multicast >= 0.0 && < 0.1, - data-accessor >= 0.2 && < 0.3, - binary >= 0.5 && < 0.6, - containers >= 0.4 && < 0.6, - transformers >= 0.2 && < 0.4, - network-transport >= 0.3 && < 0.4, - network-transport-tcp >= 0.3 && < 0.4, - distributed-process >= 0.2 && < 0.5 + If flag(build-example) + Build-Depends: base >= 4.4 && < 5, + bytestring >= 0.9 && < 0.11, + network >= 2.3 && < 2.5, + network-multicast >= 0.0 && < 0.1, + data-accessor >= 0.2 && < 0.3, + binary >= 0.5 && < 0.6, + containers >= 0.4 && < 0.6, + transformers >= 0.2 && < 0.4, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, + distributed-process >= 0.2 && < 0.5 + Else + Buildable: False Extensions: RankNTypes, - DeriveDataTypeable + DeriveDataTypeable, + CPP ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind HS-Source-Dirs: tests src From 0f9a714c612dbebf9e4dc3286ce3547ea5890fcb Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 22 Oct 2012 10:47:35 +0100 Subject: [PATCH 0259/2357] Suggest checking firewall when troubleshooting --- src/Control/Distributed/Process/Backend/SimpleLocalnet.hs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index 4ba0f181..d87983fe 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -73,6 +73,13 @@ -- peer nodes. In other words, if you start a node and pass hostname @localhost@ -- then peer nodes won't be able to reach it because @localhost@ will resolve -- to a different IP address for them. +-- +-- [Troubleshooting] +-- +-- If you try the above example and the master process cannot find any slaves, +-- then it might be that your firewall settings do not allow for UDP multicast +-- (in particular, the default iptables on some Linux distributions might not +-- allow it). {-# OPTIONS_GHC -fno-warn-orphans #-} module Control.Distributed.Process.Backend.SimpleLocalnet ( -- * Initialization From cb796b056e6e848cb79cadc634abe8238aeabc57 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 23 Oct 2012 09:28:36 +0100 Subject: [PATCH 0260/2357] Fix contact information --- distributed-process-simplelocalnet.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 3cf8f481..587958f2 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -6,7 +6,7 @@ License: BSD3 License-File: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com From fd27d4761d0f13107946a74c18ea0815efc76cd4 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 23 Oct 2012 09:28:36 +0100 Subject: [PATCH 0261/2357] Fix contact information --- distributed-process-azure.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index d6d5ee04..a681ccc1 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -6,7 +6,7 @@ License: BSD3 License-File: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com From 4e26e109735f6cf1b33723e7c9b06dfc10e765e3 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 23 Oct 2012 09:28:36 +0100 Subject: [PATCH 0262/2357] Fix contact information --- network-transport-tcp.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 38ddf82c..6ba726d0 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -6,7 +6,7 @@ License: BSD3 License-file: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com From 223009e8c32886315935e03da4453c83aba9059f Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 23 Oct 2012 09:28:36 +0100 Subject: [PATCH 0263/2357] Fix contact information --- network-transport.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport.cabal b/network-transport.cabal index c728ea84..c2a34d5e 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -6,7 +6,7 @@ License: BSD3 License-File: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com From 9f94aa44f76a840e79be4abf6ac1672930dc7a77 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 23 Oct 2012 09:28:36 +0100 Subject: [PATCH 0264/2357] Fix contact information --- network-transport-inmemory.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport-inmemory.cabal b/network-transport-inmemory.cabal index f4960144..ff36fa85 100644 --- a/network-transport-inmemory.cabal +++ b/network-transport-inmemory.cabal @@ -6,7 +6,7 @@ License: BSD3 License-file: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, dcoutts@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com From 1b60c7af211f8c36ccd2415e105e305d40ca68ea Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 23 Oct 2012 13:18:05 +0100 Subject: [PATCH 0265/2357] Update commit log for 0.2.0.7 --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index ce0d8bf4..91f2e230 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,4 @@ -<> Edsko de Vries 0.2.0.7 +2012-10-23 Edsko de Vries 0.2.0.7 * Fix cabal script so that the example program compiles From 8647b44b89327c912073a40d42acc80879794307 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 24 Oct 2012 11:41:26 +0100 Subject: [PATCH 0266/2357] Update package boundaries to work with released libssh2 and tls. --- distributed-process-azure.cabal | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index a681ccc1..62410fed 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -30,14 +30,13 @@ Library libssh2 >= 0.2 && < 0.3, pureMD5 >= 2.1 && < 2.2, bytestring >= 0.9 && < 0.11, - distributed-process >= 0.3.2 && < 0.4, - binary >= 0.5 && < 0.6, + distributed-process >= 0.3.2 && < 0.5, + binary >= 0.5 && < 0.7, + network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4, - optparse-applicative >= 0.2 && < 0.4, transformers >= 0.3 && < 0.4, - certificate >= 1.2.4 && < 1.3, - unix >= 2.5 && < 2.6, - network-transport >= 0.3 && < 0.4, + certificate >= 1.3 && < 1.4, + unix >= 2.5 && < 2.7, mtl >= 2.1 && < 2.2, rank1dynamic >= 0.1 && < 0.2, distributed-static >= 0.2 && < 0.3 @@ -55,7 +54,7 @@ Executable cloud-haskell-azure-echo if flag(build-demos) Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, - distributed-process >= 0.3.2 && < 0.4, + distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4 else buildable: False @@ -66,7 +65,7 @@ Executable cloud-haskell-azure-ping if flag(build-demos) Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, - distributed-process >= 0.3.2 && < 0.4, + distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6, @@ -81,7 +80,7 @@ Executable cloud-haskell-azure-fib if flag(build-demos) Build-Depends: base >= 4.4 && < 5, distributed-process-azure >= 0.1 && < 0.2, - distributed-process >= 0.3.2 && < 0.4, + distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, binary >= 0.5 && < 0.6, From 881a0b26bb85adbfddc1f175387d0d995e25ccd8 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Wed, 24 Oct 2012 13:26:36 +0100 Subject: [PATCH 0267/2357] Minor cleanup --- tests/TestQC.hs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 9e0d4748..1191c179 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -161,8 +161,8 @@ verify (transport, transportInternals) script = do case mEndPoint of Right endPoint -> do tid <- liftIO $ forkIO (forward endPoint) - modify endPoints (snoc endPoint) - modify forwardingThreads (tid :) + append endPoints endPoint + append forwardingThreads tid set (expectedEventsAt (address endPoint)) [] Left err -> liftIO $ throwIO err @@ -184,8 +184,8 @@ verify (transport, transportInternals) script = do , connectionId = connId , connectionBundle = currentBundleId } - modify connections (snoc (conn, connInfo)) - modify (expectedEventsAt endPointB) (snoc (ExpConnectionOpened connInfo)) + append connections (conn, connInfo) + append (expectedEventsAt endPointB) (ExpConnectionOpened connInfo) Left err -> do currentBundleId <- get bundleId expectingBreak <- get $ mayBreak currentBundleId @@ -198,7 +198,7 @@ verify (transport, transportInternals) script = do runCmd (Close i) = do (conn, connInfo) <- get (connectionAt i) liftIO $ close conn - modify (expectedEventsAt (target connInfo)) (snoc (ExpConnectionClosed connInfo)) + append (expectedEventsAt (target connInfo)) (ExpConnectionClosed connInfo) runCmd (Send i payload) = do (conn, connInfo) <- get (connectionAt i) mResult <- liftIO $ send conn payload @@ -213,7 +213,7 @@ verify (transport, transportInternals) script = do set (broken (connectionBundle connInfo)) True else liftIO $ throwIO err - modify (expectedEventsAt (target connInfo)) (snoc (ExpReceived connInfo payload)) + append (expectedEventsAt (target connInfo)) (ExpReceived connInfo payload) runCmd (BreakAfterReads n i j) = do endPointA <- address <$> get (endPointAtIx i) endPointB <- address <$> get (endPointAtIx j) @@ -222,8 +222,8 @@ verify (transport, transportInternals) script = do scheduleReadAction sock n (sClose sock) currentBundleId <- get (currentBundle endPointA endPointB) set (mayBreak currentBundleId) True - modify (expectedEventsAt endPointA) (snoc (ExpConnectionLost currentBundleId endPointB)) - modify (expectedEventsAt endPointB) (snoc (ExpConnectionLost currentBundleId endPointA)) + append (expectedEventsAt endPointA) (ExpConnectionLost currentBundleId endPointB) + append (expectedEventsAt endPointB) (ExpConnectionLost currentBundleId endPointA) forward :: EndPoint -> IO () forward endPoint = forever $ do @@ -586,6 +586,16 @@ script_BreakSendReconnect = [ , Send 1 ["ping2"] ] +script_Foo :: Script +script_Foo = [ + NewEndPoint + , NewEndPoint + , Connect 1 0 + , BreakAfterReads 2 0 1 + , Send 0 ["pingpong"] + , Connect 0 1 + ] + -------------------------------------------------------------------------------- -- Main application driver -- -------------------------------------------------------------------------------- @@ -609,6 +619,7 @@ tests transport = [ , testOne "BreakConnect1" transport script_BreakConnect1 , testOne "BreakConnect2" transport script_BreakConnect2 , testOne "BreakSendReconnect" transport script_BreakSendReconnect + , testOne "Foo" transport script_Foo ] , testGroup "Without errors" [ testGroup "One endpoint, with delays" (basicTests transport 1 id) @@ -787,6 +798,9 @@ instance Arbitrary ByteString where listAccessor :: Int -> Accessor [a] a listAccessor i = accessor (!! i) (error "listAccessor.set not defined") +append :: Monad m => Accessor st [a] -> a -> StateT st m () +append acc x = modify acc (snoc x) + snoc :: a -> [a] -> [a] snoc x xs = xs ++ [x] From bff47af45d9a28027c6043a6cc9f6de0015bea5b Mon Sep 17 00:00:00 2001 From: Takayuki Muranushi Date: Sun, 28 Oct 2012 20:54:40 +0900 Subject: [PATCH 0268/2357] depend on binary-0.6 instead of 0.5 --- distributed-process-azure.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 62410fed..86681361 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -68,7 +68,7 @@ Executable cloud-haskell-azure-ping distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, - binary >= 0.5 && < 0.6, + binary >= 0.6 && < 0.7, mtl, libssh2 else @@ -83,7 +83,7 @@ Executable cloud-haskell-azure-fib distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, - binary >= 0.5 && < 0.6, + binary >= 0.6 && < 0.7, binary-generic >= 0.2 && < 0.3, rank1dynamic >= 0.1 && < 0.2, distributed-static >= 0.2 && < 0.3, From 927bfbb54ac9b72971790512c7788ea793da3cdd Mon Sep 17 00:00:00 2001 From: Takayuki Muranushi Date: Sun, 28 Oct 2012 20:54:40 +0900 Subject: [PATCH 0269/2357] depend on binary-0.6 instead of 0.5 --- distributed-process-simplelocalnet.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 587958f2..24809013 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -33,7 +33,7 @@ Library network >= 2.3 && < 2.5, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, - binary >= 0.5 && < 0.6, + binary >= 0.6 && < 0.7, containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, @@ -55,7 +55,7 @@ Executable TestSimpleLocalnet network >= 2.3 && < 2.5, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, - binary >= 0.5 && < 0.6, + binary >= 0.6 && < 0.7, containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, From 2b3f18955b5ee562d41559fc1a5e807f77dc9599 Mon Sep 17 00:00:00 2001 From: Takayuki Muranushi Date: Sun, 28 Oct 2012 20:54:40 +0900 Subject: [PATCH 0270/2357] depend on binary-0.6 instead of 0.5 --- network-transport.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport.cabal b/network-transport.cabal index c2a34d5e..0917477a 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -65,7 +65,7 @@ Source-Repository head Library Build-Depends: base >= 4.3 && < 5, - binary >= 0.5 && < 0.6, + binary >= 0.6 && < 0.7, bytestring >= 0.9 && < 0.11, transformers >= 0.2 && < 0.4 Exposed-Modules: Network.Transport, From 2f0c2ea6a0c7cb0a291d6330c303a62b9b2250c1 Mon Sep 17 00:00:00 2001 From: Takayuki Muranushi Date: Sun, 28 Oct 2012 20:54:40 +0900 Subject: [PATCH 0271/2357] depend on binary-0.6 instead of 0.5 --- rank1dynamic.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index 9cb22756..226028e0 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -19,7 +19,7 @@ Library Data.Rank1Typeable Build-Depends: base >= 4.4 && < 4.7, ghc-prim >= 0.2 && < 0.4, - binary >= 0.5 && < 0.6 + binary >= 0.6 && < 0.7 HS-Source-Dirs: src GHC-Options: -Wall Extensions: EmptyDataDecls, From ac37bbdf8e94fd0603d1dc9d67668a2c1d536459 Mon Sep 17 00:00:00 2001 From: Takayuki Muranushi Date: Sun, 28 Oct 2012 20:54:40 +0900 Subject: [PATCH 0272/2357] depend on binary-0.6 instead of 0.5 --- distributed-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-static.cabal b/distributed-static.cabal index 9f6634a8..df40122b 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -30,7 +30,7 @@ Library rank1dynamic >= 0.1 && < 0.2, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, - binary >= 0.5 && < 0.6 + binary >= 0.6 && < 0.7 HS-Source-Dirs: src Extensions: DeriveDataTypeable, ScopedTypeVariables From 2e4014b8b5b37cd5722d1ae7c990f21d9ad495fb Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 09:22:09 +0000 Subject: [PATCH 0273/2357] Still allow for binary 0.5 too --- distributed-process-azure.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index 86681361..e0d61bcc 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -68,7 +68,7 @@ Executable cloud-haskell-azure-ping distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, - binary >= 0.6 && < 0.7, + binary >= 0.5 && < 0.7, mtl, libssh2 else @@ -83,7 +83,7 @@ Executable cloud-haskell-azure-fib distributed-process >= 0.3.2 && < 0.5, transformers >= 0.3 && < 0.4, bytestring >= 0.9 && < 0.11, - binary >= 0.6 && < 0.7, + binary >= 0.5 && < 0.7, binary-generic >= 0.2 && < 0.3, rank1dynamic >= 0.1 && < 0.2, distributed-static >= 0.2 && < 0.3, From 2bfd16f5242aeff8c136bc09998b08fc96d2ea25 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 09:22:09 +0000 Subject: [PATCH 0274/2357] Still allow for binary 0.5 too --- distributed-process-simplelocalnet.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 24809013..2cef3b61 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -33,7 +33,7 @@ Library network >= 2.3 && < 2.5, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, - binary >= 0.6 && < 0.7, + binary >= 0.5 && < 0.7, containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, @@ -55,7 +55,7 @@ Executable TestSimpleLocalnet network >= 2.3 && < 2.5, network-multicast >= 0.0 && < 0.1, data-accessor >= 0.2 && < 0.3, - binary >= 0.6 && < 0.7, + binary >= 0.5 && < 0.7, containers >= 0.4 && < 0.6, transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, From 4789f6c5e181eac06220632a7709edd2bbdb97e8 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 09:22:09 +0000 Subject: [PATCH 0275/2357] Still allow for binary 0.5 too --- network-transport.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network-transport.cabal b/network-transport.cabal index 0917477a..d2b9bb30 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -65,7 +65,7 @@ Source-Repository head Library Build-Depends: base >= 4.3 && < 5, - binary >= 0.6 && < 0.7, + binary >= 0.5 && < 0.7, bytestring >= 0.9 && < 0.11, transformers >= 0.2 && < 0.4 Exposed-Modules: Network.Transport, From 588ac59a720b9fd927b64b0b752b9b37e9d08755 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 09:22:09 +0000 Subject: [PATCH 0276/2357] Still allow for binary 0.5 too --- rank1dynamic.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index 226028e0..3c1d05c2 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -19,7 +19,7 @@ Library Data.Rank1Typeable Build-Depends: base >= 4.4 && < 4.7, ghc-prim >= 0.2 && < 0.4, - binary >= 0.6 && < 0.7 + binary >= 0.5 && < 0.7 HS-Source-Dirs: src GHC-Options: -Wall Extensions: EmptyDataDecls, From 55e39a48c98695f3441ae40f6b04028a740dad6e Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 09:22:09 +0000 Subject: [PATCH 0277/2357] Still allow for binary 0.5 too --- distributed-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-static.cabal b/distributed-static.cabal index df40122b..55ce19db 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -30,7 +30,7 @@ Library rank1dynamic >= 0.1 && < 0.2, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11, - binary >= 0.6 && < 0.7 + binary >= 0.5 && < 0.7 HS-Source-Dirs: src Extensions: DeriveDataTypeable, ScopedTypeVariables From 77bd4d73ecf9f833a20ffea7e8e7503fd380d0fe Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 14:31:27 +0000 Subject: [PATCH 0278/2357] Update example with changed API --- src/Control/Distributed/Process/Backend/Azure.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 53a82b15..bd814a7e 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -312,7 +312,7 @@ -- > -- > import System.Environment (getArgs) -- > import Data.Binary (encode, decode) --- > import Control.Monad (forever) +-- > import Control.Monad (void, forever) -- > import Control.Monad.IO.Class (liftIO) -- > import Control.Exception (try, IOException) -- > import Control.Distributed.Process @@ -386,7 +386,7 @@ -- > -- The same binary can behave as the client or the server, -- > -- depending on the command line arguments -- > case cmd of --- > "server" -> spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) +-- > "server" -> void $ spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) -- > "client" -> callOnVM backend vm port $ -- > ProcessPair ($(mkClosure 'pingClientRemote) ()) -- > pingClientLocal From f73c447b968e8bd372cf9047710050cafa58abf0 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 5 Nov 2012 14:45:23 +0000 Subject: [PATCH 0279/2357] Add module description --- distributed-process-azure.cabal | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index e0d61bcc..d2e6561d 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -11,6 +11,14 @@ Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process Bug-Reports: mailto:edsko@well-typed.com Synopsis: Microsoft Azure backend for Cloud Haskell +Description: This is a proof of concept Azure backend for Cloud Haskell. It + provides just enough functionality to run Cloud Haskell + applications on Azure virtual machines. You need to create your + virtual machines in the Azure management portal; you can then + use this backend to copy or verify your executable to the + virtual machine, start or terminate Cloud Haskell nodes on those + virtual machines, and communicate with those virtual machines + from your local machine. Category: Control Source-Repository head From ba24798392ea8100c8cd8f5422aaa18dc28429c5 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Tue, 6 Nov 2012 10:44:50 +0000 Subject: [PATCH 0280/2357] Fix spelling --- src/Control/Distributed/Process/Backend/Azure.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index bd814a7e..546a7d7c 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -41,7 +41,7 @@ -- -- We have only tested Cloud Haskell with Linux based virtual machines; -- Windows based virtual machines /might/ work, but you'll be entering --- unchartered territory. Cloud Haskell assumes that all nodes run the same +-- uncharted territory. Cloud Haskell assumes that all nodes run the same -- binary code; hence, you must use the same OS on all virtual machines, -- /as well as on your local machine/. We use Ubuntu Server 12.04 LTS for our -- tests (running on VirtualBox on our local machine). From 9792a18cad84145617c88fcfaa3b64fbe83148b3 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 7 Nov 2012 07:26:19 -0500 Subject: [PATCH 0281/2357] added cabal file --- distributed-process-platform.cabal | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 distributed-process-platform.cabal diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal new file mode 100644 index 00000000..0bd0860b --- /dev/null +++ b/distributed-process-platform.cabal @@ -0,0 +1,35 @@ +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.8 +build-type: Simple +license: BSD3 +license-file: LICENSE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" + +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform + +executable platform + main-is: Main.hs + build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 + , mtl + , derive + , distributed-static + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp + buildable: True + extensions: UndecidableInstances ExistentialQuantification + ScopedTypeVariables FlexibleInstances CPP BangPatterns + GeneralizedNewtypeDeriving GADTs DeriveDataTypeable + hs-source-dirs: src + ghc-options: -Wall -threaded -rtsopts + From 951fcdbcdc1a7e25ffcea5a91255c2235fd8e1ad Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 7 Nov 2012 07:27:14 -0500 Subject: [PATCH 0282/2357] experiments with gen_server designs --- src/Control/Distributed/Examples/Counter.hs | 82 +++++++++ src/Control/Distributed/Naive/Kitty.hs | 134 ++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 165 ++++++++++++++++++ src/Main.hs | 67 +++++++ 4 files changed, 448 insertions(+) create mode 100644 src/Control/Distributed/Examples/Counter.hs create mode 100644 src/Control/Distributed/Naive/Kitty.hs create mode 100644 src/Control/Distributed/Platform/GenServer.hs create mode 100644 src/Main.hs diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs new file mode 100644 index 00000000..b7629852 --- /dev/null +++ b/src/Control/Distributed/Examples/Counter.hs @@ -0,0 +1,82 @@ +-- | Counter server example +-- +-- Uses GenServer to implement a simple Counter process +-- +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +module Control.Distributed.Examples.Counter ( + startCounter, + getCount, + resetCount + ) where +import Control.Concurrent +import Data.Binary (Binary (..), getWord8, + putWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) + +import Control.Distributed.Platform.GenServer +import Control.Distributed.Process + +-------------------------------------------------------------------------------- +-- Data Types -- +-------------------------------------------------------------------------------- + +data CounterRequest + = GetCount + | ResetCount + deriving (Typeable, Show) + +$(derive makeBinary ''CounterRequest) + +data CounterResponse + = Count Int + | CountReset + deriving (Typeable, Show) + +$(derive makeBinary ''CounterResponse) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | +startCounter :: Name -> Int -> Process ProcessId +startCounter name count = do + serverStart name (counterServer count) + +-- | getCount +getCount :: ProcessId -> Process Int +getCount pid = do + say $ "Get count for " ++ show pid + from <- getSelfPid + c <- serverCall pid (from, GetCount) NoTimeout + say $ "Count is " ++ show c + return c + +-- | resetCount +resetCount :: ProcessId -> Process () +resetCount pid = do + say $ "Reset count for " ++ show pid + from <- getSelfPid + serverCall pid (from, ResetCount) NoTimeout :: Process () + return () + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | Counter server +counterServer :: Int -> Process (Server CounterRequest CounterResponse) +counterServer count = do + count <- liftIO $ newMVar count -- initialize state + + let handleCounterRequest :: CounterRequest -> Process (Maybe CounterResponse) + handleCounterRequest GetCount = do + n <- liftIO $ readMVar count + return $ Just (Count n) + handleCounterRequest ResetCount = do + liftIO $ putMVar count 0 + return $ Just CountReset + + return defaultServer { handleCall = handleCounterRequest } diff --git a/src/Control/Distributed/Naive/Kitty.hs b/src/Control/Distributed/Naive/Kitty.hs new file mode 100644 index 00000000..eea91a11 --- /dev/null +++ b/src/Control/Distributed/Naive/Kitty.hs @@ -0,0 +1,134 @@ +{-| +Attemp to tranlsate a basic, naive (non-OTP based) server from Erland to Cloud Haskell + +This is a naive Erlang server implementation in CloudHaskell whose main purpose is to ground +the evolution of the API into a proper form that is typed and leverages Haskell strenghts. + +This sample was taken from here: + +See: http://learnyousomeerlang.com/what-is-otp#the-basic-server +-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +-- +-- -module(kitty_server). +-- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). +module Control.Distributed.Naive.Kitty + ( + startKitty, + orderCat, + returnCat, + closeShop, + Cat(..) + ) where +import Prelude hiding (catch) +import Control.Exception(SomeException) +import Data.Binary (Binary (..), putWord8, getWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) +import Control.Monad (liftM3, void) +import Control.Distributed.Process (Process, getSelfPid, ProcessId, liftIO, spawnLocal, catch, say, send, expect) + +-- +-- % Records/Data Types +-- -record(cat, {name, color=green, description}). +type Color = String +type Description = String +type Name = String + +newtype Id a = Id ProcessId deriving Show + +data Cat = Cat { + catName :: Name, + catColor :: Color, + catDescr :: Description } + deriving (Show, Typeable) + +$( derive makeBinary ''Cat ) + +newtype CatId = CatId ProcessId deriving Show + +data CatCmd + = OrderCat String String String + | ReturnCat Cat + | CloseShop + deriving (Show, Typeable) + +$( derive makeBinary ''CatCmd ) + +data CatEv + = CatOrdered Cat + | ShopClosed + deriving (Show, Typeable) + +$( derive makeBinary ''CatEv ) + + +-- +-- %% Client API +-- start_link() -> spawn_link(fun init/0). +startKitty :: [Cat] -> Process ProcessId +startKitty cats = spawnLocal $ initCat cats `catch` \e -> do + say $ show (e :: SomeException) + initCat cats -- restart ... likely a bad idea! + +-- %% Synchronous call +orderCat :: ProcessId -> Name -> Color -> Description -> Process Cat +orderCat pid name color descr = do + say "-- Ordering cat ..." + from <- getSelfPid + send pid (from, OrderCat name color descr) + r@(CatOrdered cat) <- expect + say $ "-- Got REPLY: " ++ show r + return cat + +-- %% async call +returnCat :: ProcessId -> Cat -> Process () +returnCat pid cat = do + say $ "-- ReturnCat: " ++ show cat + send pid ((), ReturnCat cat) + +-- %% sync call +closeShop :: ProcessId -> Process () +closeShop pid = do + say "-- Closing shop ..." + from <- getSelfPid + send pid (from, CloseShop) + reply <- expect :: Process CatEv + say $ "-- Got REPLY: " ++ show reply + return () + +-- +-- %%% Server functions +initCat :: [Cat] -> Process () +initCat args = do + say "Starting Kitty ..." + loopCat args + +loopCat :: [Cat] -> Process () +loopCat cats = do + say $ "Kitty inventory: " ++ show cats + (from, cmd) <- expect + say $ "Got CMD from " ++ show from ++ " : " ++ show cmd + case cmd of + cmd@(OrderCat n c d) -> case cats of + [] -> do + send from $ CatOrdered (Cat n c d) + loopCat [] + x:xs -> do + send from $ CatOrdered x + loopCat xs + cmd@(ReturnCat cat) -> loopCat (cat:cats) + cmd@CloseShop -> do + send from ShopClosed + terminateKitty cats + _ -> do + say $ "Unknown CMD: " ++ show cmd + loopCat cats + +-- %%% Private functions +terminateKitty :: [Cat] -> Process () +terminateKitty cats = do + mapM_ (\c -> say $ show c ++ " set free!") cats + return () diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs new file mode 100644 index 00000000..ec77f10d --- /dev/null +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -0,0 +1,165 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Control.Distributed.Platform.GenServer ( + Name, + Timeout(..), + InitResult(..), + CallResult(..), + CastResult(..), + Info(..), + InfoResult(..), + TerminateReason(..), + serverStart, + serverNCall, + serverCall, + serverReply, + Server(..), + defaultServer + ) where + +import Control.Distributed.Process +import Control.Distributed.Process.Serializable +import Control.Monad (forever) +import Prelude hiding (catch, init) + +-------------------------------------------------------------------------------- +-- Data Types -- +-------------------------------------------------------------------------------- +data InitResult + = InitOk Timeout + | InitStop String + | InitIgnore + +data CallResult r + = CallOk r + | CallStop String + | CallDeferred + +data CastResult + = CastOk + | CastStop String + +data Info + = InfoTimeout Timeout + | Info String + +data InfoResult + = InfoNoReply Timeout + | InfoStop String + +data TerminateReason + = TerminateNormal + | TerminateShutdown + | TerminateReason + +-- | Server record of callbacks +data Server rq rs = Server { + handleInit :: Process InitResult, -- ^ initialization callback + handleCall :: rq -> Process (Maybe rs), -- ^ call callback + handleCast :: rq -> Process (), -- ^ cast callback + handleInfo :: Info -> Process InfoResult, -- ^ info callback + handleTerminate :: TerminateReason -> Process () -- ^ termination callback + } + +defaultServer :: Server rq rs +defaultServer = Server { + handleInit = return $ InitOk NoTimeout, + handleCall = \_ -> return Nothing, + handleCast = \_ -> return (), + handleInfo = \_ -> return $ InfoNoReply NoTimeout, + handleTerminate = \_ -> return () +} + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Process name +type Name = String + +-- | Process name +data Timeout = Timeout Int + | NoTimeout + +-- | Start server +-- +serverStart :: (Serializable rq, Serializable rs) + => Name + -> Process (Server rq rs) + -> Process ProcessId +serverStart name createServer = do + say $ "Starting server " ++ name + from <- getSelfPid + server <- createServer + pid <- spawnLocal $ do + say $ "Initializing " ++ name + initResult <- handleInit server + case initResult of + InitIgnore -> do + return () + InitStop reason -> do + say $ "Initialization stopped: " ++ reason + return () + InitOk timeout -> do + send from () -- let them know we are ready + forever $ do + case timeout of + Timeout value -> do + say $ "Waiting for call to " ++ name ++ " with timeout " ++ show value + maybeMsg <- expectTimeout value + case maybeMsg of + Just msg -> handle server msg + Nothing -> return () + NoTimeout -> do + say $ "Waiting for call to " ++ name + msg <- expect -- :: Process (ProcessId, rq) + --msg <- receiveWait [ matchAny return ] + handle server msg + return () + say $ "Waiting for " ++ name ++ " to start" + expect :: Process () + say $ "Process " ++ name ++ " initialized" + register name pid + return pid + where + handle :: (Serializable rs) => Server rq rs -> (ProcessId, rq) -> Process () + handle server (them, rq) = do + say $ "Handling call for " ++ name + maybeReply <- handleCall server rq + case maybeReply of + Just reply -> do + say $ "Sending reply from " ++ name + send them reply + Nothing -> do + say $ "Not sending reply from " ++ name + return () + +-- | Call a process using it's name +-- nsend doesnt seem to support timeouts? +serverNCall :: (Serializable a, Serializable b) => Name -> a -> Process b +serverNCall name rq = do + us <- getSelfPid + nsend name (us, rq) + expect + +-- | call a process using it's process id +serverCall :: (Serializable a, Serializable b) => ProcessId -> a -> Timeout -> Process b +serverCall to rq timeout = do + from <- getSelfPid + send to (from, rq) + case timeout of + Timeout value -> do + maybeMsg <- expectTimeout value + case maybeMsg of + Just msg -> return msg + Nothing -> error "timeout!" + NoTimeout -> expect + +-- | out of band reply to a client +serverReply :: (Serializable a) => ProcessId -> a -> Process () +serverReply pid reply = do + send pid reply diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 00000000..01c4b9ab --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,67 @@ +module Main where + +import Control.Distributed.Naive.Kitty +import Control.Distributed.Platform.GenServer +import Control.Distributed.Examples.Counter + +--import Prelude hiding (catch) +import Control.Exception (SomeException) +import Control.Monad (void) +import System.IO.Error (IOError) + +import Control.Distributed.Static (initRemoteTable) +import Network.Transport.TCP (createTransport, + defaultTCPParameters) +import System.IO +import Control.Distributed.Process (Process, ProcessId, + getSelfPid, liftIO, say, + spawnLocal, newChan) +import Control.Distributed.Process.Node (LocalNode, newLocalNode, + runProcess) + +host :: String +host = "::ffff:127.0.0.1" + +port :: String +port = "8000" + +main :: IO () +main = do + hSetBuffering stdout NoBuffering + putStrLn "Starting server ... " + t <- createTransport host port defaultTCPParameters + case t of + Left ex -> error $ show ex + Right transport -> do + putStrLn "Transport created." + localNode <- newLocalNode transport initRemoteTable + putStrLn "Local node created." + --runProcess localNode startApp `catch` \e -> print (e :: IOError) + runProcess localNode counterTest `catch` \e -> print (e :: IOError) + + putStrLn "Server done! Press key to exit ..." + void getChar + +counterTest :: Process () +counterTest = do + pid <- startCounter "TestCounter" 10 + c <- getCount pid + resetCount pid + c2 <- getCount pid + return () + +startApp :: Process () +startApp = do + say "-- Starting app ..." + kPid <- startKitty [Cat "c1" "black" "a black cat"] + orders kPid 1000 + closeShop kPid + return () + +orders kPid 0 = return () +orders kPid n = do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + orders kPid (n - 1) From 77cf0871311ecfc4d5e4296ade76c574ad82b5fc Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 7 Nov 2012 08:16:15 -0500 Subject: [PATCH 0283/2357] changed impl to use channels instead - ATTN: not compiling --- src/Control/Distributed/Examples/Counter.hs | 11 +-- src/Control/Distributed/Platform/GenServer.hs | 72 +++++++++++-------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index b7629852..695373ec 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -71,12 +71,15 @@ counterServer :: Int -> Process (Server CounterRequest CounterResponse) counterServer count = do count <- liftIO $ newMVar count -- initialize state - let handleCounterRequest :: CounterRequest -> Process (Maybe CounterResponse) + let handleCounterRequest :: CounterRequest -> Process (CallResult CounterResponse) handleCounterRequest GetCount = do n <- liftIO $ readMVar count - return $ Just (Count n) + return $ CallOk (Count n) handleCounterRequest ResetCount = do liftIO $ putMVar count 0 - return $ Just CountReset + return $ CallOk CountReset - return defaultServer { handleCall = handleCounterRequest } + return defaultServer { + serverPorts = newChan + handleCall = handleCounterRequest + } diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index ec77f10d..05ae70d9 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -58,18 +58,20 @@ data TerminateReason -- | Server record of callbacks data Server rq rs = Server { + serverPorts :: Process (SendPort rq, ReceivePort rq), handleInit :: Process InitResult, -- ^ initialization callback - handleCall :: rq -> Process (Maybe rs), -- ^ call callback - handleCast :: rq -> Process (), -- ^ cast callback + handleCall :: rq -> Process (CallResult rs), -- ^ call callback + handleCast :: rq -> Process CastResult, -- ^ cast callback handleInfo :: Info -> Process InfoResult, -- ^ info callback handleTerminate :: TerminateReason -> Process () -- ^ termination callback } defaultServer :: Server rq rs defaultServer = Server { + serverPorts = undefined, handleInit = return $ InitOk NoTimeout, - handleCall = \_ -> return Nothing, - handleCast = \_ -> return (), + handleCall = undefined, + handleCast = \_ -> return $ CastOk, handleInfo = \_ -> return $ InfoNoReply NoTimeout, handleTerminate = \_ -> return () } @@ -90,22 +92,24 @@ data Timeout = Timeout Int serverStart :: (Serializable rq, Serializable rs) => Name -> Process (Server rq rs) - -> Process ProcessId + -> Process (SendPort rq) serverStart name createServer = do say $ "Starting server " ++ name - from <- getSelfPid server <- createServer - pid <- spawnLocal $ do + + sreq <- spawnChannelLocal $ \rreq -> do + -- server process say $ "Initializing " ++ name + -- init initResult <- handleInit server case initResult of InitIgnore -> do - return () + return () -- ??? InitStop reason -> do say $ "Initialization stopped: " ++ reason return () InitOk timeout -> do - send from () -- let them know we are ready + -- loop forever $ do case timeout of Timeout value -> do @@ -116,50 +120,56 @@ serverStart name createServer = do Nothing -> return () NoTimeout -> do say $ "Waiting for call to " ++ name - msg <- expect -- :: Process (ProcessId, rq) - --msg <- receiveWait [ matchAny return ] + msg <- receiveChan rreq -- :: Process (ProcessId, rq) handle server msg return () - say $ "Waiting for " ++ name ++ " to start" - expect :: Process () + -- terminate + handleTerminate server TerminateNormal + --say $ "Waiting for " ++ name ++ " to start" + --sreq <- expect say $ "Process " ++ name ++ " initialized" - register name pid - return pid + register name $ sendPortProcessId . sendPortId $ sreq + return sreq where handle :: (Serializable rs) => Server rq rs -> (ProcessId, rq) -> Process () handle server (them, rq) = do say $ "Handling call for " ++ name - maybeReply <- handleCall server rq - case maybeReply of - Just reply -> do + callResult <- handleCall server rq + case callResult of + CallOk reply -> do say $ "Sending reply from " ++ name send them reply - Nothing -> do + CallDeferred -> say $ "Not sending reply from " ++ name - return () + CallStop reason -> + say $ "Not implemented!" -- | Call a process using it's name -- nsend doesnt seem to support timeouts? serverNCall :: (Serializable a, Serializable b) => Name -> a -> Process b serverNCall name rq = do - us <- getSelfPid - nsend name (us, rq) - expect + (sport, rport) <- newChan + nsend name (sport, rq) + receiveChan rport + --us <- getSelfPid + --nsend name (us, rq) + --expect -- | call a process using it's process id serverCall :: (Serializable a, Serializable b) => ProcessId -> a -> Timeout -> Process b -serverCall to rq timeout = do - from <- getSelfPid - send to (from, rq) +serverCall pid rq timeout = do + (sport, rport) <- newChan + send pid (sport, rq) case timeout of Timeout value -> do - maybeMsg <- expectTimeout value + receiveChan rport + maybeMsg <- error "not implemented" -- expectTimeout value case maybeMsg of Just msg -> return msg Nothing -> error "timeout!" - NoTimeout -> expect + NoTimeout -> receiveChan rport -- | out of band reply to a client -serverReply :: (Serializable a) => ProcessId -> a -> Process () -serverReply pid reply = do - send pid reply +serverReply :: (Serializable a) => SendPort a -> a -> Process () +serverReply sport reply = do + sendChan sport reply From 621d3c8d98e920be1ca52ce2a201a1334377950f Mon Sep 17 00:00:00 2001 From: RodLogic Date: Thu, 8 Nov 2012 09:56:00 -0500 Subject: [PATCH 0284/2357] changed the implementation to use channels instead and it compiles now --- src/Control/Distributed/Examples/Counter.hs | 49 ++++---- src/Control/Distributed/Platform/GenServer.hs | 119 +++++++++++------- src/Main.hs | 9 +- 3 files changed, 107 insertions(+), 70 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 695373ec..cb71a049 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -10,13 +10,13 @@ module Control.Distributed.Examples.Counter ( resetCount ) where import Control.Concurrent -import Data.Binary (Binary (..), getWord8, - putWord8) -import Data.DeriveTH import Data.Typeable (Typeable) import Control.Distributed.Platform.GenServer import Control.Distributed.Process +import Data.Binary (Binary (..), getWord8, + putWord8) +import Data.DeriveTH -------------------------------------------------------------------------------- -- Data Types -- @@ -40,27 +40,33 @@ $(derive makeBinary ''CounterResponse) -- API -- -------------------------------------------------------------------------------- +-- | The Counter id +type CounterId = ServerId CounterRequest CounterResponse + -- | -startCounter :: Name -> Int -> Process ProcessId -startCounter name count = do +startCounter :: Name -> Int -> Process CounterId +startCounter name count = serverStart name (counterServer count) -- | getCount -getCount :: ProcessId -> Process Int -getCount pid = do - say $ "Get count for " ++ show pid - from <- getSelfPid - c <- serverCall pid (from, GetCount) NoTimeout - say $ "Count is " ++ show c - return c +getCount :: CounterId -> Process Int +getCount counterId = do + say $ "Get count for " ++ show counterId + reply <- serverCall counterId GetCount NoTimeout + case reply of + Count value -> do + say $ "Count is " ++ show value + return value + _ -> error "Shouldnt be here!" -- TODO tighten the types to avoid this -- | resetCount -resetCount :: ProcessId -> Process () -resetCount pid = do - say $ "Reset count for " ++ show pid - from <- getSelfPid - serverCall pid (from, ResetCount) NoTimeout :: Process () - return () +resetCount :: CounterId -> Process () +resetCount counterId = do + say $ "Reset count for " ++ show counterId + reply <- serverCall counterId ResetCount NoTimeout + case reply of + CountReset -> return () + _ -> error "Shouldn't be here!" -- TODO tighten the types to avoid this -------------------------------------------------------------------------------- -- Implementation -- @@ -79,7 +85,6 @@ counterServer count = do liftIO $ putMVar count 0 return $ CallOk CountReset - return defaultServer { - serverPorts = newChan - handleCall = handleCounterRequest - } + return defaultServer { + handleCall = handleCounterRequest + } :: Process (Server CounterRequest CounterResponse) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 05ae70d9..f5a0537c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -13,19 +13,26 @@ module Control.Distributed.Platform.GenServer ( Info(..), InfoResult(..), TerminateReason(..), + Request(..), + Reply(..), serverStart, - serverNCall, + --serverNCall, serverCall, serverReply, Server(..), + ServerId(..), defaultServer ) where import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Monad (forever) +import Data.Typeable (Typeable) import Prelude hiding (catch, init) +import Data.Binary (Binary (..)) +import Data.DeriveTH + -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- @@ -58,17 +65,17 @@ data TerminateReason -- | Server record of callbacks data Server rq rs = Server { - serverPorts :: Process (SendPort rq, ReceivePort rq), - handleInit :: Process InitResult, -- ^ initialization callback - handleCall :: rq -> Process (CallResult rs), -- ^ call callback - handleCast :: rq -> Process CastResult, -- ^ cast callback - handleInfo :: Info -> Process InfoResult, -- ^ info callback - handleTerminate :: TerminateReason -> Process () -- ^ termination callback + handleInit :: Process InitResult, -- ^ initialization callback + handleCall :: rq -> Process (CallResult rs), -- ^ call callback + handleCast :: rq -> Process CastResult, -- ^ cast callback + handleInfo :: Info -> Process InfoResult, -- ^ info callback + handleTerminate :: TerminateReason -> Process () -- ^ termination callback } +-- | Default record +-- Starting point for creating new servers defaultServer :: Server rq rs defaultServer = Server { - serverPorts = undefined, handleInit = return $ InitOk NoTimeout, handleCall = undefined, handleCast = \_ -> return $ CastOk, @@ -87,20 +94,47 @@ type Name = String data Timeout = Timeout Int | NoTimeout +-- | Typed server Id +data ServerId rq rep + = ServerId String (SendPort (Request rq rep)) + +instance (Serializable rq, Serializable rep) => Show (ServerId rq rep) where + show (ServerId serverId sport) = serverId ++ ":" ++ show (sendPortId sport) + +-- | Request +newtype Request req reply = Request (SendPort reply, req) + deriving (Typeable, Show) + +$(derive makeBinary ''Request) + +-- | Reply +newtype Reply reply = Reply reply + deriving (Typeable, Show) + +$(derive makeBinary ''Reply) + -- | Start server -- serverStart :: (Serializable rq, Serializable rs) => Name -> Process (Server rq rs) - -> Process (SendPort rq) + -> Process (ServerId rq rs) serverStart name createServer = do say $ "Starting server " ++ name - server <- createServer - sreq <- spawnChannelLocal $ \rreq -> do + -- spawnChannelLocal :: Serializable a + -- => (ReceivePort a -> Process ()) + -- -> Process (SendPort a) + sreq <- spawnChannelLocal $ serverProcess + return $ ServerId name sreq + where + serverProcess rreq = do + -- server process - say $ "Initializing " ++ name + server <- createServer + -- init + say $ "Initializing " ++ name initResult <- handleInit server case initResult of InitIgnore -> do @@ -109,65 +143,62 @@ serverStart name createServer = do say $ "Initialization stopped: " ++ reason return () InitOk timeout -> do + -- loop forever $ do case timeout of Timeout value -> do say $ "Waiting for call to " ++ name ++ " with timeout " ++ show value - maybeMsg <- expectTimeout value - case maybeMsg of - Just msg -> handle server msg + tryRequest <- expectTimeout value + case tryRequest of + Just req -> handleRequest server req Nothing -> return () NoTimeout -> do say $ "Waiting for call to " ++ name - msg <- receiveChan rreq -- :: Process (ProcessId, rq) - handle server msg + req <- receiveChan rreq -- :: Process (ProcessId, rq) + + handleRequest server req + return () - -- terminate - handleTerminate server TerminateNormal - --say $ "Waiting for " ++ name ++ " to start" - --sreq <- expect - say $ "Process " ++ name ++ " initialized" - register name $ sendPortProcessId . sendPortId $ sreq - return sreq - where - handle :: (Serializable rs) => Server rq rs -> (ProcessId, rq) -> Process () - handle server (them, rq) = do + + -- terminate + handleTerminate server TerminateNormal + + handleRequest server (Request (sreply, rq)) = do say $ "Handling call for " ++ name callResult <- handleCall server rq case callResult of CallOk reply -> do say $ "Sending reply from " ++ name - send them reply + sendChan sreply reply CallDeferred -> say $ "Not sending reply from " ++ name CallStop reason -> - say $ "Not implemented!" + say $ "Stop: " ++ reason ++ " -- Not implemented!" -- | Call a process using it's name -- nsend doesnt seem to support timeouts? -serverNCall :: (Serializable a, Serializable b) => Name -> a -> Process b -serverNCall name rq = do - (sport, rport) <- newChan - nsend name (sport, rq) - receiveChan rport - --us <- getSelfPid - --nsend name (us, rq) - --expect +--serverNCall :: (Serializable a, Serializable b) => Name -> a -> Process b +--serverNCall name rq = do +-- (sport, rport) <- newChan +-- nsend name (sport, rq) +-- receiveChan rport +-- --us <- getSelfPid +-- --nsend name (us, rq) +-- --expect -- | call a process using it's process id -serverCall :: (Serializable a, Serializable b) => ProcessId -> a -> Timeout -> Process b -serverCall pid rq timeout = do - (sport, rport) <- newChan - send pid (sport, rq) +serverCall :: (Serializable rq, Serializable rs) => ServerId rq rs -> rq -> Timeout -> Process rs +serverCall (ServerId _ sreq) rq timeout = do + (sreply, rreply) <- newChan + sendChan sreq $ Request (sreply, rq) case timeout of + NoTimeout -> receiveChan rreply Timeout value -> do - receiveChan rport maybeMsg <- error "not implemented" -- expectTimeout value case maybeMsg of Just msg -> return msg - Nothing -> error "timeout!" - NoTimeout -> receiveChan rport + Nothing -> error $ "timeout! value = " ++ show value -- | out of band reply to a client serverReply :: (Serializable a) => SendPort a -> a -> Process () diff --git a/src/Main.hs b/src/Main.hs index 01c4b9ab..a32417b5 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -44,10 +44,11 @@ main = do counterTest :: Process () counterTest = do - pid <- startCounter "TestCounter" 10 - c <- getCount pid - resetCount pid - c2 <- getCount pid + cid <- startCounter "TestCounter" 10 + c <- getCount cid + + resetCount cid + c2 <- getCount cid return () startApp :: Process () From 9ec2159aa0d23a590cd8c4c473e3bae20995f376 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 8 Nov 2012 16:52:33 +0000 Subject: [PATCH 0285/2357] Record broken connections per direction (because we may detect that the connection has been broken in the direction A -> B, even if we haven't detected that the same connection has been broken in the direction B -> A). This still doesn't quite solve the problem, but it gets is a little closer (I think) --- tests/TestQC.hs | 74 +++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 1191c179..54cba29c 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -46,6 +46,7 @@ import Data.Unique (Unique, newUnique, hashUnique) import Data.Concurrent.Queue.MichaelScott (newQ, pushL, tryPopR) import Data.Set (Set) import qualified Data.Set as Set +import GHC.Stack (currentCallStack, renderStack) import Network.Transport import Network.Transport.TCP @@ -53,7 +54,7 @@ import Network.Transport.TCP , defaultTCPParameters , TransportInternals(socketBetween) ) -import Network.Transport.TCP.Mock.Socket (scheduleReadAction, sClose) +import Network.Transport.TCP.Mock.Socket (Socket, scheduleReadAction, sClose) -------------------------------------------------------------------------------- -- Script infrastructure -- @@ -104,12 +105,7 @@ instance Show a => Show (Variable a) where -- | In the implementation "bundles" are purely a conceptual idea, but in the -- verifier we need to concretize this notion --- --- Invariant: first endpoint address < second endpoint address -type BundleId = (EndPointAddress, EndPointAddress, Int) - -incrementBundleId :: BundleId -> BundleId -incrementBundleId (a, b, i) = (a, b, i + 1) +type BundleId = Int data ConnectionInfo = ConnectionInfo { source :: EndPointAddress @@ -131,9 +127,19 @@ data RunState = RunState { , _connections :: [(Connection, ConnectionInfo)] , _expectedEvents :: Map EndPointAddress [ExpEvent] , _forwardingThreads :: [ThreadId] + -- | When a connection from A to be may break, we add both (A, B, n) + -- and (B, A, n) to _mayBreak. Then once we detect that from A to B + -- has in fact broken we move (A, B, n), *and/or* (B, A, n), from _mayBreak + -- to _broken. Note that we can detect that a connection has been broken + -- in one direction even if we haven't yet detected that the connection + -- has broken in the other direction. + -- -- | Invariant: not mayBreak && broken - , _mayBreak :: Set BundleId - , _broken :: Set BundleId + , _mayBreak :: Set (EndPointAddress, EndPointAddress, BundleId) + , _broken :: Set (EndPointAddress, EndPointAddress, BundleId) + -- | Current bundle ID between two endpoints + -- + -- Invariant: For all keys (A, B), A <= B , _currentBundle :: Map (EndPointAddress, EndPointAddress) BundleId } @@ -170,12 +176,14 @@ verify (transport, transportInternals) script = do endPointA <- get (endPointAtIx i) endPointB <- address <$> get (endPointAtIx j) mConn <- liftIO $ connect endPointA endPointB ReliableOrdered defaultConnectHints - let bundleId = currentBundle (address endPointA) endPointB + let bundleId = currentBundle (address endPointA) endPointB + connBroken = broken (address endPointA) endPointB + connMayBreak = mayBreak (address endPointA) endPointB case mConn of Right conn -> do - bundleBroken <- get bundleId >>= get . broken + bundleBroken <- get bundleId >>= get . connBroken currentBundleId <- if bundleBroken - then modify bundleId incrementBundleId >> get bundleId + then modify bundleId (+ 1) >> get bundleId else get bundleId connId <- Variable <$> liftIO newUnique let connInfo = ConnectionInfo { @@ -188,12 +196,12 @@ verify (transport, transportInternals) script = do append (expectedEventsAt endPointB) (ExpConnectionOpened connInfo) Left err -> do currentBundleId <- get bundleId - expectingBreak <- get $ mayBreak currentBundleId + expectingBreak <- get $ connMayBreak currentBundleId if expectingBreak then do - set (mayBreak currentBundleId) False - set (broken currentBundleId) True - else + set (connMayBreak currentBundleId) False + set (connBroken currentBundleId) True + else liftIO $ throwIO err runCmd (Close i) = do (conn, connInfo) <- get (connectionAt i) @@ -202,15 +210,17 @@ verify (transport, transportInternals) script = do runCmd (Send i payload) = do (conn, connInfo) <- get (connectionAt i) mResult <- liftIO $ send conn payload + let connMayBreak = mayBreak (source connInfo) (target connInfo) (connectionBundle connInfo) + connBroken = broken (source connInfo) (target connInfo) (connectionBundle connInfo) case mResult of Right () -> return () Left err -> do - expectingBreak <- get $ mayBreak (connectionBundle connInfo) - isBroken <- get $ broken (connectionBundle connInfo) + expectingBreak <- get connMayBreak + isBroken <- get connBroken if expectingBreak || isBroken then do - set (mayBreak (connectionBundle connInfo)) False - set (broken (connectionBundle connInfo)) True + set connMayBreak False + set connBroken True else liftIO $ throwIO err append (expectedEventsAt (target connInfo)) (ExpReceived connInfo payload) @@ -219,9 +229,10 @@ verify (transport, transportInternals) script = do endPointB <- address <$> get (endPointAtIx j) liftIO $ do sock <- socketBetween transportInternals endPointA endPointB - scheduleReadAction sock n (sClose sock) + scheduleReadAction sock n $ breakSocket sock currentBundleId <- get (currentBundle endPointA endPointB) - set (mayBreak currentBundleId) True + set (mayBreak endPointA endPointB currentBundleId) True + set (mayBreak endPointB endPointA currentBundleId) True append (expectedEventsAt endPointA) (ExpConnectionLost currentBundleId endPointB) append (expectedEventsAt endPointB) (ExpConnectionLost currentBundleId endPointA) @@ -255,6 +266,11 @@ verify (transport, transportInternals) script = do else Left ("Could not match " ++ show (st ^. expectedEvents) ++ " and " ++ show actualEvents) +breakSocket :: Socket -> IO () +breakSocket sock = do + currentCallStack >>= putStrLn . renderStack + sClose sock + -------------------------------------------------------------------------------- -- Match expected and actual events -- -------------------------------------------------------------------------------- @@ -703,21 +719,19 @@ expectedEventsAt addr = expectedEvents >>> DAC.mapDefault [] addr forwardingThreads :: Accessor RunState [ThreadId] forwardingThreads = accessor _forwardingThreads (\ts st -> st { _forwardingThreads = ts }) -mayBreak :: BundleId -> Accessor RunState Bool -mayBreak bid = aux >>> DAC.set bid +mayBreak :: EndPointAddress -> EndPointAddress -> BundleId -> Accessor RunState Bool +mayBreak a b bid = aux >>> DAC.set (a, b, bid) where - aux :: Accessor RunState (Set BundleId) aux = accessor _mayBreak (\bs st -> st { _mayBreak = bs }) -broken :: BundleId -> Accessor RunState Bool -broken bid = aux >>> DAC.set bid +broken :: EndPointAddress -> EndPointAddress -> BundleId -> Accessor RunState Bool +broken a b bid = aux >>> DAC.set (a, b, bid) where - aux :: Accessor RunState (Set BundleId) aux = accessor _broken (\bs st -> st { _broken = bs }) currentBundle :: EndPointAddress -> EndPointAddress -> Accessor RunState BundleId -currentBundle i j = aux >>> if i < j then DAC.mapDefault (i, j, 0) (i, j) - else DAC.mapDefault (j, i, 0) (j, i) +currentBundle i j = aux >>> if i < j then DAC.mapDefault 0 (i, j) + else DAC.mapDefault 0 (j, i) where aux :: Accessor RunState (Map (EndPointAddress, EndPointAddress) BundleId) aux = accessor _currentBundle (\mp st -> st { _currentBundle = mp }) From 948be4e2ef0eb210edb039ba28ad081ec311f721 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sat, 10 Nov 2012 08:11:30 -0500 Subject: [PATCH 0286/2357] again using process instead of channel messages so we can better support info messages --- distributed-process-platform.cabal | 1 + src/Control/Distributed/Examples/Counter.hs | 6 +- src/Control/Distributed/Platform/GenServer.hs | 65 ++++++------------- 3 files changed, 25 insertions(+), 47 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0bd0860b..581a275d 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -20,6 +20,7 @@ executable platform main-is: Main.hs build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 , mtl +-- , stm , derive , distributed-static , distributed-process diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index cb71a049..a9cada66 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -41,7 +41,7 @@ $(derive makeBinary ''CounterResponse) -------------------------------------------------------------------------------- -- | The Counter id -type CounterId = ServerId CounterRequest CounterResponse +type CounterId = ServerId -- | startCounter :: Name -> Int -> Process CounterId @@ -79,10 +79,10 @@ counterServer count = do let handleCounterRequest :: CounterRequest -> Process (CallResult CounterResponse) handleCounterRequest GetCount = do - n <- liftIO $ readMVar count + n <- liftIO $ readMVar count return $ CallOk (Count n) handleCounterRequest ResetCount = do - liftIO $ putMVar count 0 + liftIO $ swapMVar count 0 return $ CallOk CountReset return defaultServer { diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index f5a0537c..8aec2fe7 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -20,8 +20,7 @@ module Control.Distributed.Platform.GenServer ( serverCall, serverReply, Server(..), - ServerId(..), - defaultServer + ServerId, defaultServer ) where import Control.Distributed.Process @@ -95,14 +94,10 @@ data Timeout = Timeout Int | NoTimeout -- | Typed server Id -data ServerId rq rep - = ServerId String (SendPort (Request rq rep)) - -instance (Serializable rq, Serializable rep) => Show (ServerId rq rep) where - show (ServerId serverId sport) = serverId ++ ":" ++ show (sendPortId sport) +type ServerId = ProcessId -- | Request -newtype Request req reply = Request (SendPort reply, req) +newtype Request req reply = Request (reply, req) deriving (Typeable, Show) $(derive makeBinary ''Request) @@ -118,18 +113,11 @@ $(derive makeBinary ''Reply) serverStart :: (Serializable rq, Serializable rs) => Name -> Process (Server rq rs) - -> Process (ServerId rq rs) + -> Process ServerId serverStart name createServer = do - say $ "Starting server " ++ name - - -- spawnChannelLocal :: Serializable a - -- => (ReceivePort a -> Process ()) - -- -> Process (SendPort a) - sreq <- spawnChannelLocal $ serverProcess - return $ ServerId name sreq + spawnLocal $ serverProcess where - serverProcess rreq = do - + serverProcess= do -- server process server <- createServer @@ -149,13 +137,13 @@ serverStart name createServer = do case timeout of Timeout value -> do say $ "Waiting for call to " ++ name ++ " with timeout " ++ show value - tryRequest <- expectTimeout value - case tryRequest of + maybeReq <- expectTimeout value + case maybeReq of Just req -> handleRequest server req Nothing -> return () NoTimeout -> do say $ "Waiting for call to " ++ name - req <- receiveChan rreq -- :: Process (ProcessId, rq) + req <- expect handleRequest server req @@ -164,43 +152,32 @@ serverStart name createServer = do -- terminate handleTerminate server TerminateNormal - handleRequest server (Request (sreply, rq)) = do + handleRequest server (Request (cid, rq)) = do say $ "Handling call for " ++ name callResult <- handleCall server rq case callResult of CallOk reply -> do say $ "Sending reply from " ++ name - sendChan sreply reply + send cid reply CallDeferred -> say $ "Not sending reply from " ++ name CallStop reason -> say $ "Stop: " ++ reason ++ " -- Not implemented!" --- | Call a process using it's name --- nsend doesnt seem to support timeouts? ---serverNCall :: (Serializable a, Serializable b) => Name -> a -> Process b ---serverNCall name rq = do --- (sport, rport) <- newChan --- nsend name (sport, rq) --- receiveChan rport --- --us <- getSelfPid --- --nsend name (us, rq) --- --expect - -- | call a process using it's process id -serverCall :: (Serializable rq, Serializable rs) => ServerId rq rs -> rq -> Timeout -> Process rs -serverCall (ServerId _ sreq) rq timeout = do - (sreply, rreply) <- newChan - sendChan sreq $ Request (sreply, rq) +serverCall :: (Serializable rq, Serializable rs) => ServerId -> rq -> Timeout -> Process rs +serverCall sid rq timeout = do + cid <- getSelfPid + send sid $ Request (cid, rq) case timeout of - NoTimeout -> receiveChan rreply + NoTimeout -> expect Timeout value -> do - maybeMsg <- error "not implemented" -- expectTimeout value - case maybeMsg of + maybeReply <- expectTimeout value + case maybeReply of Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show value -- | out of band reply to a client -serverReply :: (Serializable a) => SendPort a -> a -> Process () -serverReply sport reply = do - sendChan sport reply +serverReply :: (Serializable a) => ServerId -> a -> Process () +serverReply sid reply = do + send sid reply From df90ae7d423129a56a6a4a6264878730bb3f7b25 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 12 Nov 2012 12:06:46 +0000 Subject: [PATCH 0287/2357] Add comment describing the problem --- tests/TestQC.hs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 54cba29c..4dfdcddd 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -1,3 +1,9 @@ +-- Test the TCP transport using QuickCheck generated scripts +-- +-- TODO: This is not quite working yet. The main problem, I think, is the +-- allocation of "bundle ID"s to connections. The problem is exposed by the +-- aptly-named regression test script_Foo (to be renamed once I figure out what +-- bug that test is actually exposing :) module Main ( main -- Shush the compiler about unused definitions @@ -123,9 +129,12 @@ data ExpEvent = deriving Show data RunState = RunState { - _endPoints :: [EndPoint] - , _connections :: [(Connection, ConnectionInfo)] - , _expectedEvents :: Map EndPointAddress [ExpEvent] + _endPoints :: [EndPoint] + , _connections :: [(Connection, ConnectionInfo)] + , _expectedEvents :: Map EndPointAddress [ExpEvent] + -- | For each endpoint we create we create a thread that forwards the events + -- of that endpoint to a central channel. We collect the thread IDs so that + -- we can kill these thread when we are done. , _forwardingThreads :: [ThreadId] -- | When a connection from A to be may break, we add both (A, B, n) -- and (B, A, n) to _mayBreak. Then once we detect that from A to B @@ -135,12 +144,12 @@ data RunState = RunState { -- has broken in the other direction. -- -- | Invariant: not mayBreak && broken - , _mayBreak :: Set (EndPointAddress, EndPointAddress, BundleId) - , _broken :: Set (EndPointAddress, EndPointAddress, BundleId) + , _mayBreak :: Set (EndPointAddress, EndPointAddress, BundleId) + , _broken :: Set (EndPointAddress, EndPointAddress, BundleId) -- | Current bundle ID between two endpoints -- -- Invariant: For all keys (A, B), A <= B - , _currentBundle :: Map (EndPointAddress, EndPointAddress) BundleId + , _currentBundle :: Map (EndPointAddress, EndPointAddress) BundleId } initialRunState :: RunState @@ -177,8 +186,8 @@ verify (transport, transportInternals) script = do endPointB <- address <$> get (endPointAtIx j) mConn <- liftIO $ connect endPointA endPointB ReliableOrdered defaultConnectHints let bundleId = currentBundle (address endPointA) endPointB - connBroken = broken (address endPointA) endPointB - connMayBreak = mayBreak (address endPointA) endPointB + connBroken = broken (address endPointA) endPointB + connMayBreak = mayBreak (address endPointA) endPointB case mConn of Right conn -> do bundleBroken <- get bundleId >>= get . connBroken @@ -224,6 +233,9 @@ verify (transport, transportInternals) script = do else liftIO $ throwIO err append (expectedEventsAt (target connInfo)) (ExpReceived connInfo payload) + -- TODO: This will only work if a connection between 'i' and 'j' has + -- already been established. We would need to modify the mock network + -- layer to support breaking "future" connections runCmd (BreakAfterReads n i j) = do endPointA <- address <$> get (endPointAtIx i) endPointB <- address <$> get (endPointAtIx j) From b865c39918f946563f059e5a453516d78b5e40b3 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 14 Nov 2012 09:37:09 -0500 Subject: [PATCH 0288/2357] Redesigned GenServer completely. It now supports calls, casts, conditional calls and casts and an experimental any handler --- src/Control/Distributed/Examples/Counter.hs | 86 ++-- src/Control/Distributed/Platform/GenServer.hs | 383 +++++++++++------- src/Main.hs | 8 +- 3 files changed, 287 insertions(+), 190 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index a9cada66..ae1a7d13 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -1,58 +1,59 @@ --- | Counter server example --- --- Uses GenServer to implement a simple Counter process --- {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Examples.Counter ( +module Control.Distributed.Examples.Counter( + CounterId, startCounter, + stopCounter, getCount, resetCount ) where import Control.Concurrent -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) import Control.Distributed.Platform.GenServer import Control.Distributed.Process -import Data.Binary (Binary (..), getWord8, - putWord8) +import Data.Binary (Binary (..), getWord8, + putWord8) import Data.DeriveTH -------------------------------------------------------------------------------- --- Data Types -- +-- API -- -------------------------------------------------------------------------------- -data CounterRequest - = GetCount - | ResetCount - deriving (Typeable, Show) +-- | The Counter id +type CounterId = ServerId +-- call +data CounterRequest + = IncrementCounter + | GetCount + deriving (Show, Typeable) $(derive makeBinary ''CounterRequest) data CounterResponse - = Count Int - | CountReset - deriving (Typeable, Show) - + = CounterIncremented + | Count Int + deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | The Counter id -type CounterId = ServerId +-- cast +data ResetCount = ResetCount deriving (Show, Typeable) +$(derive makeBinary ''ResetCount) -- | -startCounter :: Name -> Int -> Process CounterId -startCounter name count = - serverStart name (counterServer count) +startCounter :: Process ServerId +startCounter = startServer $ defaultServer { msgHandlers = [ + handleCall handleCounter, + handleCast handleReset +]} + +stopCounter :: ServerId -> Process () +stopCounter sid = stopServer sid TerminateNormal -- | getCount -getCount :: CounterId -> Process Int +getCount :: ServerId -> Process Int getCount counterId = do - say $ "Get count for " ++ show counterId - reply <- serverCall counterId GetCount NoTimeout + reply <- callServer counterId GetCount NoTimeout case reply of Count value -> do say $ "Count is " ++ show value @@ -60,31 +61,16 @@ getCount counterId = do _ -> error "Shouldnt be here!" -- TODO tighten the types to avoid this -- | resetCount -resetCount :: CounterId -> Process () +resetCount :: ServerId -> Process () resetCount counterId = do say $ "Reset count for " ++ show counterId - reply <- serverCall counterId ResetCount NoTimeout - case reply of - CountReset -> return () - _ -> error "Shouldn't be here!" -- TODO tighten the types to avoid this + castServer counterId ResetCount -------------------------------------------------------------------------------- --- Implementation -- +-- IMPL -- -------------------------------------------------------------------------------- --- | Counter server -counterServer :: Int -> Process (Server CounterRequest CounterResponse) -counterServer count = do - count <- liftIO $ newMVar count -- initialize state - - let handleCounterRequest :: CounterRequest -> Process (CallResult CounterResponse) - handleCounterRequest GetCount = do - n <- liftIO $ readMVar count - return $ CallOk (Count n) - handleCounterRequest ResetCount = do - liftIO $ swapMVar count 0 - return $ CallOk CountReset +handleCounter IncrementCounter = return $ CallOk (CounterIncremented) +handleCounter GetCount = return $ CallOk (Count 0) - return defaultServer { - handleCall = handleCounterRequest - } :: Process (Server CounterRequest CounterResponse) +handleReset ResetCount = return $ CastOk diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 8aec2fe7..d113f888 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,183 +1,290 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} +-- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( Name, + ServerId, Timeout(..), InitResult(..), CallResult(..), CastResult(..), - Info(..), - InfoResult(..), TerminateReason(..), - Request(..), - Reply(..), - serverStart, - --serverNCall, - serverCall, - serverReply, - Server(..), - ServerId, defaultServer + InitHandler, + TerminateHandler, + MessageDispatcher(), + handleCall, + handleCallIf, + handleCast, + handleCastIf, + handleAny, + LocalServer(..), + defaultServer, + startServer, + callServer, + castServer, + stopServer ) where -import Control.Distributed.Process -import Control.Distributed.Process.Serializable -import Control.Monad (forever) -import Data.Typeable (Typeable) -import Prelude hiding (catch, init) +import Control.Distributed.Process (AbstractMessage (forward), + Match, MonitorRef, + Process, ProcessId, + expect, + expectTimeout, + getSelfPid, match, + matchAny, matchIf, + receiveTimeout, + receiveWait, say, + send, spawnLocal) +import Control.Distributed.Process.Serializable (Serializable) -import Data.Binary (Binary (..)) +import Data.Binary (Binary (..), + getWord8, putWord8) import Data.DeriveTH +import Data.Typeable (Typeable) + -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- + +-- | Process name +type Name = String + +-- | ServerId +type ServerId = ProcessId + +-- | Timeout +data Timeout = Timeout Int + | NoTimeout + +-- | Initialize handler result data InitResult = InitOk Timeout | InitStop String - | InitIgnore -data CallResult r - = CallOk r - | CallStop String - | CallDeferred +-- | Terminate reason +data TerminateReason + = TerminateNormal + | TerminateShutdown + | TerminateReason String + deriving (Show, Typeable) +$(derive makeBinary ''TerminateReason) + +--type Server s = StateT s Process + +-- | Handlers +type InitHandler = Process InitResult +type TerminateHandler = TerminateReason -> Process () +type CallHandler a b = a -> Process (CallResult b) +type CastHandler a = a -> Process CastResult + +-- | The result of a call +data CallResult a + = CallOk a + | CallForward ServerId + | CallStop a String + deriving (Show, Typeable) + + +-- | The result of a cast data CastResult - = CastOk - | CastStop String + = CastOk + | CastForward ServerId + | CastStop String -data Info - = InfoTimeout Timeout - | Info String +-- | General idea of a future here +-- This should hook up into the receive loop to update the result MVar automatically without blocking the server +-- data Future a = Future { result :: MVar (Either IOError a) } -data InfoResult - = InfoNoReply Timeout - | InfoStop String -data TerminateReason - = TerminateNormal - | TerminateShutdown - | TerminateReason - --- | Server record of callbacks -data Server rq rs = Server { - handleInit :: Process InitResult, -- ^ initialization callback - handleCall :: rq -> Process (CallResult rs), -- ^ call callback - handleCast :: rq -> Process CastResult, -- ^ cast callback - handleInfo :: Info -> Process InfoResult, -- ^ info callback - handleTerminate :: TerminateReason -> Process () -- ^ termination callback - } +-- | Adds routing metadata to the actual payload +data Message a = Message ProcessId a + deriving (Show, Typeable) +$(derive makeBinary ''Message) + +-- | Management message +-- TODO is there a std way of terminating a process from another process? +data ManageServer = TerminateServer TerminateReason + deriving (Show, Typeable) +$(derive makeBinary ''ManageServer) + + +-- | Matches messages using a dispatcher +class MessageMatcher d where + matchMessage :: d -> Match () + +-- | Dispatcher that knows how to dispatch messages to a handler +data MessageDispatcher + = forall a . (Serializable a) => MessageDispatcher { dispatcher :: Message a -> Process () } + | forall a . (Serializable a) => MessageDispatcherIf { dispatcher :: Message a -> Process (), dispatchIf :: Message a -> Bool } + | MessageDispatcherAny { dispatcherAny :: AbstractMessage -> Process () } + + +-- | Matches messages to a MessageDispatcher +instance MessageMatcher MessageDispatcher where + matchMessage (MessageDispatcher d) = match d + matchMessage (MessageDispatcherIf d c) = matchIf c d + matchMessage (MessageDispatcherAny d) = matchAny d --- | Default record --- Starting point for creating new servers -defaultServer :: Server rq rs -defaultServer = Server { - handleInit = return $ InitOk NoTimeout, - handleCall = undefined, - handleCast = \_ -> return $ CastOk, - handleInfo = \_ -> return $ InfoNoReply NoTimeout, - handleTerminate = \_ -> return () +-- | Constructs a call message dispatcher +-- +handleCall :: (Serializable a, Show a, Serializable b) => CallHandler a b -> MessageDispatcher +handleCall = handleCallIf (const True) + +handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler a b -> MessageDispatcher +handleCallIf pred handler = MessageDispatcherIf { + dispatcher = (\m@(Message cid req) -> do + say $ "Server got CALL: " ++ show m + result <- handler req + case result of + CallOk resp -> send cid resp + CallForward sid -> send sid m + CallStop resp reason -> return () + ), + dispatchIf = \(Message _ req) -> pred req } --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- +-- | Constructs a cast message dispatcher +-- +handleCast :: (Serializable a, Show a) => CastHandler a -> MessageDispatcher +handleCast = handleCastIf (const True) --- | Process name -type Name = String +handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler a -> MessageDispatcher +handleCastIf pred handler = MessageDispatcherIf { + dispatcher = (\m@(Message cid msg) -> do + say $ "Server got CAST: " ++ show m + result <- handler msg + case result of + CastOk -> return () + CastForward sid -> send sid m + CastStop reason -> error "TODO" + ), + dispatchIf = \(Message _ msg) -> pred msg +} --- | Process name -data Timeout = Timeout Int - | NoTimeout +-- | Constructs a dispatcher for any message +-- Note that since we don't know the type of this message it assumes the protocol of a cast +-- i.e. no reply's +handleAny :: (AbstractMessage -> Process (CastResult)) -> MessageDispatcher +handleAny handler = MessageDispatcherAny { + dispatcherAny = (\m -> do + result <- handler m + case result of + CastOk -> return () + CastForward sid -> (forward m) sid + CastStop reason -> error "TODO" + ) +} --- | Typed server Id -type ServerId = ProcessId +-- | The server callbacks +data LocalServer = LocalServer { + initHandler :: InitHandler, -- ^ initialization handler + msgHandlers :: [MessageDispatcher], + terminateHandler :: TerminateHandler -- ^ termination handler + } --- | Request -newtype Request req reply = Request (reply, req) - deriving (Typeable, Show) +---- | Default record +---- Starting point for creating new servers +defaultServer :: LocalServer +defaultServer = LocalServer { + initHandler = return $ InitOk NoTimeout, + msgHandlers = [], + terminateHandler = \_ -> return () +} -$(derive makeBinary ''Request) +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- --- | Reply -newtype Reply reply = Reply reply - deriving (Typeable, Show) +-- | Start a new server and return it's id +startServer :: LocalServer -> Process ServerId +startServer handlers = spawnLocal $ processServer handlers -$(derive makeBinary ''Reply) +-- TODO +startServerLink :: LocalServer -> Process (ServerId, MonitorRef) +startServerLink handlers = undefined + --us <- getSelfPid + --them <- spawn nid (cpLink us `seqCP` proc) + --ref <- monitor them + --return (them, ref) --- | Start server --- -serverStart :: (Serializable rq, Serializable rs) - => Name - -> Process (Server rq rs) - -> Process ServerId -serverStart name createServer = do - spawnLocal $ serverProcess - where - serverProcess= do - -- server process - server <- createServer - - -- init - say $ "Initializing " ++ name - initResult <- handleInit server - case initResult of - InitIgnore -> do - return () -- ??? - InitStop reason -> do - say $ "Initialization stopped: " ++ reason - return () - InitOk timeout -> do - - -- loop - forever $ do - case timeout of - Timeout value -> do - say $ "Waiting for call to " ++ name ++ " with timeout " ++ show value - maybeReq <- expectTimeout value - case maybeReq of - Just req -> handleRequest server req - Nothing -> return () - NoTimeout -> do - say $ "Waiting for call to " ++ name - req <- expect - - handleRequest server req - - return () - - -- terminate - handleTerminate server TerminateNormal - - handleRequest server (Request (cid, rq)) = do - say $ "Handling call for " ++ name - callResult <- handleCall server rq - case callResult of - CallOk reply -> do - say $ "Sending reply from " ++ name - send cid reply - CallDeferred -> - say $ "Not sending reply from " ++ name - CallStop reason -> - say $ "Stop: " ++ reason ++ " -- Not implemented!" - --- | call a process using it's process id -serverCall :: (Serializable rq, Serializable rs) => ServerId -> rq -> Timeout -> Process rs -serverCall sid rq timeout = do +-- | call a server identified by it's ServerId +callServer :: (Serializable rq, Serializable rs) => ServerId -> rq -> Timeout -> Process rs +callServer sid rq timeout = do cid <- getSelfPid - send sid $ Request (cid, rq) + send sid (Message cid rq) case timeout of NoTimeout -> expect - Timeout value -> do - maybeReply <- expectTimeout value - case maybeReply of + Timeout time -> do + mayResp <- expectTimeout time + case mayResp of Just msg -> return msg - Nothing -> error $ "timeout! value = " ++ show value + Nothing -> error $ "timeout! value = " ++ show time + +-- | Cast a message to a server identified by it's ServerId +castServer :: (Serializable a) => ServerId -> a -> Process () +castServer sid msg = do + cid <- getSelfPid + send sid (Message cid msg) + +-- | Stops a server identified by it's ServerId +stopServer :: ServerId -> TerminateReason -> Process () +stopServer sid reason = castServer sid (TerminateServer reason) + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | server process +processServer :: LocalServer -> Process () +processServer localServer = do + ir <- processInit localServer + tr <- case ir of + InitOk to -> do + say $ "Server ready to receive messages!" + processLoop localServer to + InitStop r -> return (TerminateReason r) + processTerminate localServer tr + +-- | initialize server +processInit :: LocalServer -> Process InitResult +processInit localServer = do + say $ "Server initializing ... " + ir <- initHandler localServer + return ir + +-- | server loop +processLoop :: LocalServer -> Timeout -> Process TerminateReason +processLoop localServer timeout = do + mayMsg <- processReceive (msgHandlers localServer) timeout + case mayMsg of + Just reason -> return reason + Nothing -> processLoop localServer timeout + +-- | +processReceive :: [MessageDispatcher] -> Timeout -> Process (Maybe TerminateReason) +processReceive ds timeout = do + case timeout of + NoTimeout -> do + receiveWait $ map matchMessage ds + return Nothing + Timeout time -> do + mayResult <- receiveTimeout time $ map matchMessage ds + case mayResult of + Just _ -> return Nothing + Nothing -> do + say "Receive timed out ..." + return Nothing --- | out of band reply to a client -serverReply :: (Serializable a) => ServerId -> a -> Process () -serverReply sid reply = do - send sid reply +-- | terminate server +processTerminate :: LocalServer -> TerminateReason -> Process () +processTerminate localServer reason = do + say $ "Server terminating ... " + (terminateHandler localServer) reason diff --git a/src/Main.hs b/src/Main.hs index a32417b5..697a5410 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -3,6 +3,7 @@ module Main where import Control.Distributed.Naive.Kitty import Control.Distributed.Platform.GenServer import Control.Distributed.Examples.Counter +--import Control.Distributed.Examples.GenServer2 --import Prelude hiding (catch) import Control.Exception (SomeException) @@ -44,11 +45,14 @@ main = do counterTest :: Process () counterTest = do - cid <- startCounter "TestCounter" 10 + cid <- startCounter c <- getCount cid - + say $ "c = " ++ show c resetCount cid c2 <- getCount cid + say $ "c2 = " ++ show c2 + + stopCounter cid return () startApp :: Process () From b8a95fab39ae234704456cc6aaaaf82b338a793c Mon Sep 17 00:00:00 2001 From: Jeff Epstein Date: Sun, 18 Nov 2012 17:20:35 -0500 Subject: [PATCH 0289/2357] Fixed de-registration of remote processes when the process terminates Added reregister and reregisterRemoteAsync (keeping in line with Erlang's approach) Changed SimpleLocalNet to use reregisterRemoteAsync to adjust logging (currently without waiting for result) --- src/Control/Distributed/Process/Backend/SimpleLocalnet.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index d87983fe..226e2398 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -115,7 +115,7 @@ import Control.Distributed.Process , WhereIsReply(..) , whereis , whereisRemoteAsync - , registerRemote + , reregisterRemoteAsync , getSelfPid , register , expect @@ -244,7 +244,7 @@ apiRedirectLogsHere backend = do mLogger <- whereis "logger" forM_ mLogger $ \logger -> do nids <- liftIO $ findPeers backend 1000000 - forM_ nids $ \nid -> registerRemote nid "logger" logger + forM_ nids $ \nid -> reregisterRemoteAsync nid "logger" logger -- ignore async response -------------------------------------------------------------------------------- -- Slaves -- From 2583c2377fad370268b52a7674aed2d1d7249aa6 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 21 Nov 2012 22:37:14 -0500 Subject: [PATCH 0290/2357] Introduced a Server monad that wraps the server's state --- src/Control/Distributed/Examples/Counter.hs | 83 ++++++--- src/Control/Distributed/Naive/Kitty.hs | 1 - src/Control/Distributed/Platform/GenServer.hs | 176 ++++++++++++------ src/Main.hs | 9 +- 4 files changed, 175 insertions(+), 94 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index ae1a7d13..43c714da 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -5,72 +5,99 @@ module Control.Distributed.Examples.Counter( startCounter, stopCounter, getCount, + incCount, resetCount ) where -import Control.Concurrent -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) import Control.Distributed.Platform.GenServer import Control.Distributed.Process -import Data.Binary (Binary (..), getWord8, - putWord8) +import Data.Binary (Binary (..), getWord8, + putWord8) import Data.DeriveTH -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- --- | The Counter id + +-- | The counter server id type CounterId = ServerId --- call + +-- Call request(s) data CounterRequest = IncrementCounter | GetCount deriving (Show, Typeable) $(derive makeBinary ''CounterRequest) +-- Call response(s) data CounterResponse = CounterIncremented | Count Int deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) --- cast + +-- Cast message(s) data ResetCount = ResetCount deriving (Show, Typeable) $(derive makeBinary ''ResetCount) --- | -startCounter :: Process ServerId -startCounter = startServer $ defaultServer { msgHandlers = [ - handleCall handleCounter, - handleCast handleReset + +-- | Start a counter server +startCounter :: Int -> Process ServerId +startCounter count = startServer count defaultServer { + msgHandlers = [ + handleCall handleCounter, + handleCast handleReset ]} + + +-- | Stop the counter server stopCounter :: ServerId -> Process () stopCounter sid = stopServer sid TerminateNormal --- | getCount + + +-- | Increment count +incCount :: ServerId -> Process () +incCount sid = do + CounterIncremented <- callServer sid NoTimeout IncrementCounter + return () + + + +-- | Get the current count getCount :: ServerId -> Process Int -getCount counterId = do - reply <- callServer counterId GetCount NoTimeout - case reply of - Count value -> do - say $ "Count is " ++ show value - return value - _ -> error "Shouldnt be here!" -- TODO tighten the types to avoid this - --- | resetCount +getCount sid = do + Count c <- callServer sid NoTimeout GetCount + return c + + + +-- | Reset the current count resetCount :: ServerId -> Process () -resetCount counterId = do - say $ "Reset count for " ++ show counterId - castServer counterId ResetCount +resetCount sid = castServer sid ResetCount + -------------------------------------------------------------------------------- -- IMPL -- -------------------------------------------------------------------------------- -handleCounter IncrementCounter = return $ CallOk (CounterIncremented) -handleCounter GetCount = return $ CallOk (Count 0) -handleReset ResetCount = return $ CastOk + +handleCounter IncrementCounter = do + modifyState (+1) + return $ CallOk (CounterIncremented) + +handleCounter GetCount = do + count <- getState + return $ CallOk (Count count) + + + +handleReset ResetCount = do + putState 0 + return $ CastOk diff --git a/src/Control/Distributed/Naive/Kitty.hs b/src/Control/Distributed/Naive/Kitty.hs index eea91a11..88db7335 100644 --- a/src/Control/Distributed/Naive/Kitty.hs +++ b/src/Control/Distributed/Naive/Kitty.hs @@ -27,7 +27,6 @@ import Control.Exception(SomeException) import Data.Binary (Binary (..), putWord8, getWord8) import Data.DeriveTH import Data.Typeable (Typeable) -import Control.Monad (liftM3, void) import Control.Distributed.Process (Process, getSelfPid, ProcessId, liftIO, spawnLocal, catch, say, send, expect) -- diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index d113f888..84c745ac 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -23,6 +23,9 @@ module Control.Distributed.Platform.GenServer ( handleCast, handleCastIf, handleAny, + putState, + getState, + modifyState, LocalServer(..), defaultServer, startServer, @@ -42,6 +45,10 @@ import Control.Distributed.Process (AbstractMessage (forw receiveWait, say, send, spawnLocal) import Control.Distributed.Process.Serializable (Serializable) +import qualified Control.Monad.State as ST (StateT, + get, lift, + modify, put, + runStateT) import Data.Binary (Binary (..), getWord8, putWord8) @@ -77,13 +84,20 @@ data TerminateReason deriving (Show, Typeable) $(derive makeBinary ''TerminateReason) ---type Server s = StateT s Process + + +-- | Server monad +type Server s = ST.StateT s Process + + -- | Handlers -type InitHandler = Process InitResult -type TerminateHandler = TerminateReason -> Process () -type CallHandler a b = a -> Process (CallResult b) -type CastHandler a = a -> Process CastResult +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type CallHandler s a b = a -> Server s (CallResult b) +type CastHandler s a = a -> Server s CastResult + + -- | The result of a call data CallResult a @@ -93,15 +107,14 @@ data CallResult a deriving (Show, Typeable) + -- | The result of a cast data CastResult = CastOk | CastForward ServerId | CastStop String --- | General idea of a future here --- This should hook up into the receive loop to update the result MVar automatically without blocking the server --- data Future a = Future { result :: MVar (Either IOError a) } + -- | Adds routing metadata to the actual payload @@ -109,6 +122,8 @@ data Message a = Message ProcessId a deriving (Show, Typeable) $(derive makeBinary ''Message) + + -- | Management message -- TODO is there a std way of terminating a process from another process? data ManageServer = TerminateServer TerminateReason @@ -116,83 +131,100 @@ data ManageServer = TerminateServer TerminateReason $(derive makeBinary ''ManageServer) --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: d -> Match () -- | Dispatcher that knows how to dispatch messages to a handler -data MessageDispatcher - = forall a . (Serializable a) => MessageDispatcher { dispatcher :: Message a -> Process () } - | forall a . (Serializable a) => MessageDispatcherIf { dispatcher :: Message a -> Process (), dispatchIf :: Message a -> Bool } - | MessageDispatcherAny { dispatcherAny :: AbstractMessage -> Process () } +-- s The server state +data MessageDispatcher s + = forall a . (Serializable a) => MessageDispatcher { + dispatcher :: s -> Message a -> Process s + } + | forall a . (Serializable a) => MessageDispatcherIf { + dispatcher :: s -> Message a -> Process s, + dispatchIf :: s -> Message a -> Bool + } + | MessageDispatcherAny { + dispatcherAny :: s -> AbstractMessage -> Process s + } + + +-- | Matches messages using a dispatcher +class MessageMatcher d where + matchMessage :: s -> d s -> Match s -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage (MessageDispatcher d) = match d - matchMessage (MessageDispatcherIf d c) = matchIf c d - matchMessage (MessageDispatcherAny d) = matchAny d + matchMessage state (MessageDispatcher dispatcher) = match (dispatcher state) + matchMessage state (MessageDispatcherIf dispatcher cond) = matchIf (cond state) (dispatcher state) + matchMessage state (MessageDispatcherAny dispatcher) = matchAny (dispatcher state) -- | Constructs a call message dispatcher -- -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler a b -> MessageDispatcher -handleCall = handleCallIf (const True) +handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s +handleCall handler = handleCallIf (const True) handler -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler a b -> MessageDispatcher -handleCallIf pred handler = MessageDispatcherIf { - dispatcher = (\m@(Message cid req) -> do +handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s +handleCallIf cond handler = MessageDispatcherIf { + dispatcher = (\state m@(Message cid req) -> do say $ "Server got CALL: " ++ show m - result <- handler req + (result, state') <- ST.runStateT (handler req) state case result of CallOk resp -> send cid resp CallForward sid -> send sid m CallStop resp reason -> return () + return state' ), - dispatchIf = \(Message _ req) -> pred req + dispatchIf = \state (Message _ req) -> cond req } -- | Constructs a cast message dispatcher -- -handleCast :: (Serializable a, Show a) => CastHandler a -> MessageDispatcher +handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s handleCast = handleCastIf (const True) -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler a -> MessageDispatcher -handleCastIf pred handler = MessageDispatcherIf { - dispatcher = (\m@(Message cid msg) -> do + +-- | +handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s +handleCastIf cond handler = MessageDispatcherIf { + dispatcher = (\state m@(Message cid msg) -> do say $ "Server got CAST: " ++ show m - result <- handler msg + (result, state') <- ST.runStateT (handler msg) state case result of - CastOk -> return () - CastForward sid -> send sid m + CastOk -> return state' + CastForward sid -> do + send sid m + return state' CastStop reason -> error "TODO" ), - dispatchIf = \(Message _ msg) -> pred msg + dispatchIf = \state (Message _ msg) -> cond msg } -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Process (CastResult)) -> MessageDispatcher +handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { - dispatcherAny = (\m -> do - result <- handler m + dispatcherAny = (\state m -> do + (result, state') <- ST.runStateT (handler m) state case result of - CastOk -> return () - CastForward sid -> (forward m) sid + CastOk -> return state' + CastForward sid -> do + (forward m) sid + return state' CastStop reason -> error "TODO" ) } -- | The server callbacks -data LocalServer = LocalServer { - initHandler :: InitHandler, -- ^ initialization handler - msgHandlers :: [MessageDispatcher], - terminateHandler :: TerminateHandler -- ^ termination handler +data LocalServer s = LocalServer { + initHandler :: InitHandler s, -- ^ initialization handler + msgHandlers :: [MessageDispatcher s], + terminateHandler :: TerminateHandler s -- ^ termination handler } ---- | Default record ---- Starting point for creating new servers -defaultServer :: LocalServer +defaultServer :: LocalServer s defaultServer = LocalServer { initHandler = return $ InitOk NoTimeout, msgHandlers = [], @@ -204,11 +236,13 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -startServer :: LocalServer -> Process ServerId -startServer handlers = spawnLocal $ processServer handlers +startServer :: s -> LocalServer s -> Process ServerId +startServer state handlers = spawnLocal $ do + ST.runStateT (processServer handlers) state + return () -- TODO -startServerLink :: LocalServer -> Process (ServerId, MonitorRef) +startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerLink handlers = undefined --us <- getSelfPid --them <- spawn nid (cpLink us `seqCP` proc) @@ -216,8 +250,8 @@ startServerLink handlers = undefined --return (them, ref) -- | call a server identified by it's ServerId -callServer :: (Serializable rq, Serializable rs) => ServerId -> rq -> Timeout -> Process rs -callServer sid rq timeout = do +callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs +callServer sid timeout rq = do cid <- getSelfPid send sid (Message cid rq) case timeout of @@ -238,30 +272,42 @@ castServer sid msg = do stopServer :: ServerId -> TerminateReason -> Process () stopServer sid reason = castServer sid (TerminateServer reason) +-- | Get the server state +getState :: Server s s +getState = ST.get + +-- | Put the server state +putState :: s -> Server s () +putState = ST.put + +-- | Modify the server state +modifyState :: (s -> s) -> Server s () +modifyState = ST.modify + -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer -> Process () +processServer :: LocalServer s -> Server s () processServer localServer = do ir <- processInit localServer tr <- case ir of InitOk to -> do - say $ "Server ready to receive messages!" + trace $ "Server ready to receive messages!" processLoop localServer to InitStop r -> return (TerminateReason r) processTerminate localServer tr -- | initialize server -processInit :: LocalServer -> Process InitResult +processInit :: LocalServer s -> Server s InitResult processInit localServer = do - say $ "Server initializing ... " + ST.lift $ say $ "Server initializing ... " ir <- initHandler localServer return ir -- | server loop -processLoop :: LocalServer -> Timeout -> Process TerminateReason +processLoop :: LocalServer s -> Timeout -> Server s TerminateReason processLoop localServer timeout = do mayMsg <- processReceive (msgHandlers localServer) timeout case mayMsg of @@ -269,22 +315,30 @@ processLoop localServer timeout = do Nothing -> processLoop localServer timeout -- | -processReceive :: [MessageDispatcher] -> Timeout -> Process (Maybe TerminateReason) +processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do + state <- ST.get case timeout of NoTimeout -> do - receiveWait $ map matchMessage ds + state <- ST.lift $ receiveWait $ map (matchMessage state) ds + putState state return Nothing Timeout time -> do - mayResult <- receiveTimeout time $ map matchMessage ds + mayResult <- ST.lift $ receiveTimeout time $ map (matchMessage state) ds case mayResult of - Just _ -> return Nothing + Just state -> do + putState state + return Nothing Nothing -> do - say "Receive timed out ..." - return Nothing + trace "Receive timed out ..." + return $ Just (TerminateReason "Receive timed out") -- | terminate server -processTerminate :: LocalServer -> TerminateReason -> Process () +processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do - say $ "Server terminating ... " + trace $ "Server terminating ... " (terminateHandler localServer) reason + +-- | Log a trace message using the underlying Process's say +trace :: String -> Server s () +trace msg = ST.lift . say $ msg diff --git a/src/Main.hs b/src/Main.hs index 697a5410..da0ad6c2 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,11 +1,8 @@ module Main where import Control.Distributed.Naive.Kitty -import Control.Distributed.Platform.GenServer import Control.Distributed.Examples.Counter ---import Control.Distributed.Examples.GenServer2 ---import Prelude hiding (catch) import Control.Exception (SomeException) import Control.Monad (void) import System.IO.Error (IOError) @@ -45,7 +42,11 @@ main = do counterTest :: Process () counterTest = do - cid <- startCounter + cid <- startCounter 10 + c <- getCount cid + say $ "c = " ++ show c + incCount cid + incCount cid c <- getCount cid say $ "c = " ++ show c resetCount cid From c263b3545e02360dff450ba215978288b034b00c Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 22 Nov 2012 09:05:29 +0000 Subject: [PATCH 0291/2357] Bumb version to 0.3.0.1 for release --- ChangeLog | 4 ++++ network-transport.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 1a8a11d8..8dd74c37 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-11-22 Edsko de Vries 0.3.0.1 + +* Relax bounds on Binary + 2012-10-03 Edsko de Vries 0.3.0 * Clarify disconnection diff --git a/network-transport.cabal b/network-transport.cabal index d2b9bb30..c4558864 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -1,5 +1,5 @@ Name: network-transport -Version: 0.3.0 +Version: 0.3.0.1 Cabal-Version: >=1.6 Build-Type: Simple License: BSD3 From e0de07dee013ad65ab6418149369983ebf79ba12 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 22 Nov 2012 09:14:11 +0000 Subject: [PATCH 0292/2357] Bumb version to 0.1.0.2 for release --- ChangeLog | 4 ++++ rank1dynamic.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index b85caa2c..0d1888db 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-11-22 Edsko de Vries 0.1.0.2 + +* Relax package bounds to allow for Binary 0.6 + 2012-09-27 Edsko de Vries 0.1.0.1 * Relax lower bound of base to 4.4 (ghc 7.2) diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index 3c1d05c2..e876821d 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -1,5 +1,5 @@ Name: rank1dynamic -Version: 0.1.0.1 +Version: 0.1.0.2 Synopsis: Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types Description: "Data.Typeable" and "Data.Dynamic" only support monomorphic types. In this package we provide similar functionality but with From b53865c19a4b5ba895651f07f24427e38fd79227 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 22 Nov 2012 09:17:10 +0000 Subject: [PATCH 0293/2357] Bumb version to 0.2.1.1 for release. --- ChangeLog | 4 ++++ distributed-static.cabal | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index e3ba0c32..dd7d106a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2012-11-22 Edsko de Vries 0.2.1.1 + +* Relax package bounds to allow for Binary 0.6 + 2012-10-03 Edsko de Vries 0.2.1 * Add support for 'staticFlip' diff --git a/distributed-static.cabal b/distributed-static.cabal index 55ce19db..19be4de4 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -1,5 +1,5 @@ Name: distributed-static -Version: 0.2.1 +Version: 0.2.1.1 Synopsis: Compositional, type-safe, polymorphic static values and closures Description: /Towards Haskell in the Cloud/ (Epstein et al, Haskell Symposium 2011) introduces the concept of /static/ values: From dd5fde7d9ce1f6aa0789fa0444ccc88370343404 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Thu, 22 Nov 2012 09:59:10 +0000 Subject: [PATCH 0294/2357] Bumb version to 0.2.0.8 --- ChangeLog | 6 ++++++ distributed-process-simplelocalnet.cabal | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 91f2e230..031ad2d3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2012-11-22 Edsko de Vries 0.2.0.8 + +* Use the new 'register' semantics (depends on distributed-process-0.4.1). +Patch by Jeff Epstein +* Relax package bounds to allow for Binary 0.6 + 2012-10-23 Edsko de Vries 0.2.0.7 * Fix cabal script so that the example program compiles diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 2cef3b61..6dd84fec 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,5 +1,5 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.7 +Version: 0.2.0.8 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 @@ -38,7 +38,7 @@ Library transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4, - distributed-process >= 0.2 && < 0.5 + distributed-process >= 0.4.1 && < 0.5 Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast Extensions: RankNTypes, @@ -60,7 +60,7 @@ Executable TestSimpleLocalnet transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4, - distributed-process >= 0.2 && < 0.5 + distributed-process >= 0.4.1 && < 0.5 Else Buildable: False Extensions: RankNTypes, From 7a1bd96a0813d6fa9592d65891f51e40a8285fd1 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Thu, 22 Nov 2012 08:19:02 -0500 Subject: [PATCH 0295/2357] Now handling CallStop and CastStop --- src/Control/Distributed/Examples/Counter.hs | 7 +- src/Control/Distributed/Platform/GenServer.hs | 93 ++++++++++--------- 2 files changed, 55 insertions(+), 45 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 43c714da..d71c72dd 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -90,14 +90,17 @@ resetCount sid = castServer sid ResetCount handleCounter IncrementCounter = do modifyState (+1) - return $ CallOk (CounterIncremented) + count <- getState + if count > 10 + then return $ CallStop CounterIncremented "Count > 10" + else return $ CallOk CounterIncremented + handleCounter GetCount = do count <- getState return $ CallOk (Count count) - handleReset ResetCount = do putState 0 return $ CastOk diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 84c745ac..198bcd0c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -136,43 +136,48 @@ $(derive makeBinary ''ManageServer) -- s The server state data MessageDispatcher s = forall a . (Serializable a) => MessageDispatcher { - dispatcher :: s -> Message a -> Process s + dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) } | forall a . (Serializable a) => MessageDispatcherIf { - dispatcher :: s -> Message a -> Process s, + dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason), dispatchIf :: s -> Message a -> Bool } | MessageDispatcherAny { - dispatcherAny :: s -> AbstractMessage -> Process s + dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) } -- | Matches messages using a dispatcher class MessageMatcher d where - matchMessage :: s -> d s -> Match s + matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage state (MessageDispatcher dispatcher) = match (dispatcher state) - matchMessage state (MessageDispatcherIf dispatcher cond) = matchIf (cond state) (dispatcher state) - matchMessage state (MessageDispatcherAny dispatcher) = matchAny (dispatcher state) + matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) + matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) + matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) -- | Constructs a call message dispatcher -- handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall handler = handleCallIf (const True) handler +handleCall = handleCallIf (const True) handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s handleCallIf cond handler = MessageDispatcherIf { dispatcher = (\state m@(Message cid req) -> do say $ "Server got CALL: " ++ show m - (result, state') <- ST.runStateT (handler req) state - case result of - CallOk resp -> send cid resp - CallForward sid -> send sid m - CallStop resp reason -> return () - return state' + (r, s') <- ST.runStateT (handler req) state + case r of + CallOk resp -> do + send cid resp + return (s', Nothing) + CallForward sid -> do + send sid m + return (s', Nothing) + CallStop resp reason -> do + send cid resp + return (s', Just (TerminateReason reason)) ), dispatchIf = \state (Message _ req) -> cond req } @@ -186,15 +191,15 @@ handleCast = handleCastIf (const True) -- | handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid msg) -> do + dispatcher = (\s m@(Message cid msg) -> do say $ "Server got CAST: " ++ show m - (result, state') <- ST.runStateT (handler msg) state - case result of - CastOk -> return state' + (r, s') <- ST.runStateT (handler msg) s + case r of + CastStop reason -> return (s', Just $ TerminateReason reason) + CastOk -> return (s', Nothing) CastForward sid -> do send sid m - return state' - CastStop reason -> error "TODO" + return (s', Nothing) ), dispatchIf = \state (Message _ msg) -> cond msg } @@ -204,14 +209,14 @@ handleCastIf cond handler = MessageDispatcherIf { -- i.e. no reply's handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { - dispatcherAny = (\state m -> do - (result, state') <- ST.runStateT (handler m) state - case result of - CastOk -> return state' + dispatcherAny = (\s m -> do + (r, s') <- ST.runStateT (handler m) s + case r of + CastStop reason -> return (s', Just $ TerminateReason reason) + CastOk -> return (s', Nothing) CastForward sid -> do (forward m) sid - return state' - CastStop reason -> error "TODO" + return (s', Nothing) ) } @@ -302,43 +307,45 @@ processServer localServer = do -- | initialize server processInit :: LocalServer s -> Server s InitResult processInit localServer = do - ST.lift $ say $ "Server initializing ... " + trace $ "Server initializing ... " ir <- initHandler localServer return ir -- | server loop processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer timeout = do - mayMsg <- processReceive (msgHandlers localServer) timeout +processLoop localServer t = do + mayMsg <- processReceive (msgHandlers localServer) t case mayMsg of - Just reason -> return reason - Nothing -> processLoop localServer timeout + Just r -> return r + Nothing -> processLoop localServer t -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do - state <- ST.get + s <- getState + let ms = map (matchMessage s) ds case timeout of NoTimeout -> do - state <- ST.lift $ receiveWait $ map (matchMessage state) ds - putState state - return Nothing - Timeout time -> do - mayResult <- ST.lift $ receiveTimeout time $ map (matchMessage state) ds + (s', r) <- ST.lift $ receiveWait ms + putState s' + return r + Timeout t -> do + mayResult <- ST.lift $ receiveTimeout t ms case mayResult of - Just state -> do - putState state - return Nothing + Just (s', r) -> do + putState s + return r Nothing -> do - trace "Receive timed out ..." - return $ Just (TerminateReason "Receive timed out") + trace "Receive timed out ..." + return $ Just (TerminateReason "Receive timed out") -- | terminate server processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do - trace $ "Server terminating ... " + trace $ "Server terminating: " ++ show reason (terminateHandler localServer) reason -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = ST.lift . say $ msg + From 9993bca72bfa4d41ffb5a9ba698ca3742b019451 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 22 Nov 2012 13:36:19 +0000 Subject: [PATCH 0296/2357] resolve merge conflicts --- src/Control/Distributed/Platform/GenServer.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 198bcd0c..2d342e38 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -130,7 +130,9 @@ data ManageServer = TerminateServer TerminateReason deriving (Show, Typeable) $(derive makeBinary ''ManageServer) - +-- | Matches messages using a dispatcher +class MessageMatcher d where + matchMessage :: d -> Match () -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state From b41db68ff47875c66c5d4e49b798392259a8951e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 22 Nov 2012 13:36:19 +0000 Subject: [PATCH 0297/2357] resolve merge conflicts --- src/Control/Distributed/Platform/GenServer.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 198bcd0c..2d342e38 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -130,7 +130,9 @@ data ManageServer = TerminateServer TerminateReason deriving (Show, Typeable) $(derive makeBinary ''ManageServer) - +-- | Matches messages using a dispatcher +class MessageMatcher d where + matchMessage :: d -> Match () -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state From b85552bbeea146e0c93d8e5da2d4b80f3c4d1498 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 26 Nov 2012 20:16:14 -0500 Subject: [PATCH 0298/2357] added an additional example plus a few minos changes --- src/Control/Distributed/Examples/Counter.hs | 19 +-- src/Control/Distributed/Examples/Kitty.hs | 127 ++++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 58 +++++++- src/Main.hs | 62 +++++---- 4 files changed, 230 insertions(+), 36 deletions(-) create mode 100644 src/Control/Distributed/Examples/Kitty.hs diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index d71c72dd..56662f27 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -1,30 +1,25 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} module Control.Distributed.Examples.Counter( - CounterId, startCounter, stopCounter, getCount, incCount, resetCount ) where -import Data.Typeable (Typeable) import Control.Distributed.Platform.GenServer -import Control.Distributed.Process + import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- --- API -- +-- Types -- -------------------------------------------------------------------------------- --- | The counter server id -type CounterId = ServerId - - -- Call request(s) data CounterRequest = IncrementCounter @@ -32,6 +27,8 @@ data CounterRequest deriving (Show, Typeable) $(derive makeBinary ''CounterRequest) + + -- Call response(s) data CounterResponse = CounterIncremented @@ -40,11 +37,16 @@ data CounterResponse $(derive makeBinary ''CounterResponse) + -- Cast message(s) data ResetCount = ResetCount deriving (Show, Typeable) $(derive makeBinary ''ResetCount) +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { @@ -87,7 +89,6 @@ resetCount sid = castServer sid ResetCount -------------------------------------------------------------------------------- - handleCounter IncrementCounter = do modifyState (+1) count <- getState diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs new file mode 100644 index 00000000..64a32daf --- /dev/null +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -0,0 +1,127 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +-- +-- -module(kitty_server). +-- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). +module Control.Distributed.Examples.Kitty + ( + startKitty, + orderCat, + returnCat, + closeShop, + Cat(..) + ) where + +import Control.Distributed.Platform.GenServer + +import Data.Binary (Binary (..), getWord8, + putWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) + +-- +-- % Records/Data Types +-- -record(cat, {name, color=green, description}). + +type Color = String +type Description = String +type Name = String + + + +data Cat = Cat { + catName :: Name, + catColor :: Color, + catDescr :: Description } + deriving (Show, Typeable) +$( derive makeBinary ''Cat ) + + + +data CatCmd + = OrderCat String String String + | CloseShop + deriving (Show, Typeable) +$( derive makeBinary ''CatCmd ) + + + +data ReturnCat + = ReturnCat Cat + deriving (Show, Typeable) +$( derive makeBinary ''ReturnCat ) + + + +data CatEv + = CatOrdered Cat + | ShopClosed + deriving (Show, Typeable) +$( derive makeBinary ''CatEv ) + + + +-- +-- %% Client API +-- start_link() -> spawn_link(fun init/0). +-- | Start a counter server +startKitty :: [Cat] -> Process ServerId +startKitty cats = startServer cats defaultServer { + msgHandlers = [ + handleCall handleKitty, + handleCast handleReturn +]} + + +-- %% Synchronous call +orderCat :: ServerId -> Name -> Color -> Description -> Process Cat +orderCat sid name color descr = do + result <- callServer sid NoTimeout (OrderCat name color descr) + case result of + CatOrdered c -> return c + _ -> error $ "Unexpected result " ++ show result + + + +-- %% async call +returnCat :: ServerId -> Cat -> Process () +returnCat sid cat = castServer sid (ReturnCat cat) + + + +-- %% sync call +closeShop :: ServerId -> Process () +closeShop sid = do + result <- callServer sid NoTimeout CloseShop + case result of + ShopClosed -> return () + _ -> error $ "Unexpected result " ++ show result + + + +-- +-- %%% Server functions + + +handleKitty (OrderCat name color descr) = do + cats <- getState + trace $ "Kitty inventory: " ++ show cats + case cats of + [] -> do + let cat = Cat name color descr + putState (cat:cats) + return $ CallOk (CatOrdered cat) + (x:xs) -> do -- TODO find cat with same features + putState xs + return $ CallOk (CatOrdered x) + +handleKitty CloseShop = do + putState [] + return $ CallOk ShopClosed + + + +handleReturn (ReturnCat cat) = do + modifyState (cat :) + return CastOk diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 198bcd0c..6ff003ad 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -8,7 +8,6 @@ -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( - Name, ServerId, Timeout(..), InitResult(..), @@ -31,7 +30,9 @@ module Control.Distributed.Platform.GenServer ( startServer, callServer, castServer, - stopServer + stopServer, + Process, + trace ) where import Control.Distributed.Process (AbstractMessage (forward), @@ -63,19 +64,26 @@ import Data.Typeable (Typeable) -- | Process name type Name = String + + -- | ServerId type ServerId = ProcessId + + -- | Timeout data Timeout = Timeout Int | NoTimeout + + -- | Initialize handler result data InitResult = InitOk Timeout | InitStop String + -- | Terminate reason data TerminateReason = TerminateNormal @@ -152,21 +160,26 @@ class MessageMatcher d where matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) + -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + + -- | Constructs a call message dispatcher -- handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s handleCall = handleCallIf (const True) + + handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s handleCallIf cond handler = MessageDispatcherIf { dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: " ++ show m + say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" (r, s') <- ST.runStateT (handler req) state case r of CallOk resp -> do @@ -182,17 +195,20 @@ handleCallIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ req) -> cond req } + + -- | Constructs a cast message dispatcher -- handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s handleCast = handleCastIf (const True) + -- | handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s handleCastIf cond handler = MessageDispatcherIf { dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: " ++ show m + say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" (r, s') <- ST.runStateT (handler msg) s case r of CastStop reason -> return (s', Just $ TerminateReason reason) @@ -204,6 +220,8 @@ handleCastIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ msg) -> cond msg } + + -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's @@ -220,6 +238,8 @@ handleAny handler = MessageDispatcherAny { ) } + + -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler @@ -227,6 +247,8 @@ data LocalServer s = LocalServer { terminateHandler :: TerminateHandler s -- ^ termination handler } + + ---- | Default record ---- Starting point for creating new servers defaultServer :: LocalServer s @@ -236,6 +258,8 @@ defaultServer = LocalServer { terminateHandler = \_ -> return () } + + -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- @@ -246,6 +270,8 @@ startServer state handlers = spawnLocal $ do ST.runStateT (processServer handlers) state return () + + -- TODO startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerLink handlers = undefined @@ -254,10 +280,13 @@ startServerLink handlers = undefined --ref <- monitor them --return (them, ref) + + -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid + say $ "Calling server " ++ show cid send sid (Message cid rq) case timeout of NoTimeout -> expect @@ -267,24 +296,35 @@ callServer sid timeout rq = do Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time + + -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid + say $ "Casting server " ++ show cid send sid (Message cid msg) + + -- | Stops a server identified by it's ServerId stopServer :: ServerId -> TerminateReason -> Process () stopServer sid reason = castServer sid (TerminateServer reason) + + -- | Get the server state getState :: Server s s getState = ST.get + + -- | Put the server state putState :: s -> Server s () putState = ST.put + + -- | Modify the server state modifyState :: (s -> s) -> Server s () modifyState = ST.modify @@ -304,6 +344,8 @@ processServer localServer = do InitStop r -> return (TerminateReason r) processTerminate localServer tr + + -- | initialize server processInit :: LocalServer s -> Server s InitResult processInit localServer = do @@ -311,6 +353,8 @@ processInit localServer = do ir <- initHandler localServer return ir + + -- | server loop processLoop :: LocalServer s -> Timeout -> Server s TerminateReason processLoop localServer t = do @@ -319,6 +363,8 @@ processLoop localServer t = do Just r -> return r Nothing -> processLoop localServer t + + -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -339,12 +385,16 @@ processReceive ds timeout = do trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") + + -- | terminate server processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do trace $ "Server terminating: " ++ show reason (terminateHandler localServer) reason + + -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = ST.lift . say $ msg diff --git a/src/Main.hs b/src/Main.hs index da0ad6c2..643dd633 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,28 +1,35 @@ module Main where -import Control.Distributed.Naive.Kitty import Control.Distributed.Examples.Counter +import Control.Distributed.Examples.Kitty -import Control.Exception (SomeException) -import Control.Monad (void) -import System.IO.Error (IOError) +import Control.Exception (AsyncException (..), + SomeException, catchJust) +import Control.Monad (void) +import System.IO.Error (IOError) -import Control.Distributed.Static (initRemoteTable) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) +import Control.Distributed.Static (initRemoteTable) +import Network.Transport (closeTransport) +import Network.Transport.TCP (createTransport, + defaultTCPParameters) + +import Control.Distributed.Process (Process, ProcessId, + getSelfPid, liftIO, + newChan, say, spawnLocal) +import Control.Distributed.Process.Node (LocalNode, newLocalNode, + runProcess) import System.IO -import Control.Distributed.Process (Process, ProcessId, - getSelfPid, liftIO, say, - spawnLocal, newChan) -import Control.Distributed.Process.Node (LocalNode, newLocalNode, - runProcess) host :: String host = "::ffff:127.0.0.1" + + port :: String port = "8000" + + main :: IO () main = do hSetBuffering stdout NoBuffering @@ -34,14 +41,17 @@ main = do putStrLn "Transport created." localNode <- newLocalNode transport initRemoteTable putStrLn "Local node created." - --runProcess localNode startApp `catch` \e -> print (e :: IOError) - runProcess localNode counterTest `catch` \e -> print (e :: IOError) + runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) + --runProcess localNode counterTest `catch` \e -> print (e :: IOError) + putStrLn "Server started!" + getChar + return () + - putStrLn "Server done! Press key to exit ..." - void getChar counterTest :: Process () counterTest = do + say "-- Starting counter test ..." cid <- startCounter 10 c <- getCount cid say $ "c = " ++ show c @@ -56,18 +66,24 @@ counterTest = do stopCounter cid return () -startApp :: Process () -startApp = do - say "-- Starting app ..." + + +kittyTest :: Int -> Process () +kittyTest n = do + say "-- Starting kitty test ..." kPid <- startKitty [Cat "c1" "black" "a black cat"] - orders kPid 1000 + say $ "-- Ordering " ++ show n ++ " cats ..." + kittyTransactions kPid n + say "-- Closing kitty shop ..." closeShop kPid return () -orders kPid 0 = return () -orders kPid n = do + + +kittyTransactions kPid 0 = return () +kittyTransactions kPid n = do cat1 <- orderCat kPid "c1" "black" "a black cat" cat2 <- orderCat kPid "c2" "black" "a black cat" returnCat kPid cat1 returnCat kPid cat2 - orders kPid (n - 1) + kittyTransactions kPid (n - 1) From 93610f3fab2722d9e0bb7b82394f379df58fa6a7 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 26 Nov 2012 21:30:13 -0500 Subject: [PATCH 0299/2357] Added smart constructors for CallResult, CastResult, InitResult that are already in the Server monad and hide the concrete constructors --- src/Control/Distributed/Examples/Counter.hs | 9 ++- src/Control/Distributed/Examples/Kitty.hs | 8 +-- src/Control/Distributed/Platform/GenServer.hs | 62 ++++++++++++++----- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 56662f27..1e850614 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -93,15 +93,14 @@ handleCounter IncrementCounter = do modifyState (+1) count <- getState if count > 10 - then return $ CallStop CounterIncremented "Count > 10" - else return $ CallOk CounterIncremented - + then callStop CounterIncremented "Count > 10" + else callOk CounterIncremented handleCounter GetCount = do count <- getState - return $ CallOk (Count count) + callOk (Count count) handleReset ResetCount = do putState 0 - return $ CastOk + castOk diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 64a32daf..9620660b 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -111,17 +111,17 @@ handleKitty (OrderCat name color descr) = do [] -> do let cat = Cat name color descr putState (cat:cats) - return $ CallOk (CatOrdered cat) + callOk (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - return $ CallOk (CatOrdered x) + callOk (CatOrdered x) handleKitty CloseShop = do putState [] - return $ CallOk ShopClosed + callOk ShopClosed handleReturn (ReturnCat cat) = do modifyState (cat :) - return CastOk + castOk diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 6ff003ad..ed95df2b 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -10,9 +10,14 @@ module Control.Distributed.Platform.GenServer ( ServerId, Timeout(..), - InitResult(..), - CallResult(..), - CastResult(..), + initOk, + initStop, + callOk, + callForward, + callStop, + castOk, + castForward, + castStop, TerminateReason(..), InitHandler, TerminateHandler, @@ -77,11 +82,22 @@ data Timeout = Timeout Int +-- | Server monad +type Server s = ST.StateT s Process + + + -- | Initialize handler result data InitResult = InitOk Timeout | InitStop String +initOk :: Timeout -> Server s InitResult +initOk t = return (InitOk t) + +initStop :: String -> Server s InitResult +initStop reason = return (InitStop reason) + -- | Terminate reason @@ -94,19 +110,6 @@ $(derive makeBinary ''TerminateReason) --- | Server monad -type Server s = ST.StateT s Process - - - --- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult - - - -- | The result of a call data CallResult a = CallOk a @@ -114,6 +117,15 @@ data CallResult a | CallStop a String deriving (Show, Typeable) +callOk :: a -> Server s (CallResult a) +callOk resp = return (CallOk resp) + +callForward :: ServerId -> Server s (CallResult a) +callForward sid = return (CallForward sid) + +callStop :: a -> String -> Server s (CallResult a) +callStop resp reason = return (CallStop resp reason) + -- | The result of a cast @@ -122,6 +134,23 @@ data CastResult | CastForward ServerId | CastStop String +castOk :: Server s CastResult +castOk = return CastOk + +castForward :: ServerId -> Server s CastResult +castForward sid = return (CastForward sid) + +castStop :: String -> Server s CastResult +castStop reason = return (CastStop reason) + + + +-- | Handlers +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type CallHandler s a b = a -> Server s (CallResult b) +type CastHandler s a = a -> Server s CastResult + @@ -329,6 +358,7 @@ putState = ST.put modifyState :: (s -> s) -> Server s () modifyState = ST.modify + -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- From 491b11d365417dcecb45a6d38e95da7a25c86d7e Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 18:45:09 -0500 Subject: [PATCH 0300/2357] added tests and benchmark files (no real content at this point) plus missing .gitignore and .ghci --- .ghci | 9 +++++++++ .gitignore | 3 +++ LICENSE | 1 + README.md | 18 ++++++++++++++++-- RELEASE-NOTES.md | 3 +++ Setup.hs | 3 +++ benchmarks/dtp-benchmarks.cabal | 14 ++++++++++++++ benchmarks/src/CounterServer.hs | 17 +++++++++++++++++ distributed-process-platform.cabal | 25 ++++++++++++++++++++++++- tests/Properties.hs | 28 ++++++++++++++++++++++++++++ 10 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 .ghci create mode 100644 LICENSE create mode 100644 RELEASE-NOTES.md create mode 100644 Setup.hs create mode 100644 benchmarks/dtp-benchmarks.cabal create mode 100644 benchmarks/src/CounterServer.hs create mode 100644 tests/Properties.hs diff --git a/.ghci b/.ghci new file mode 100644 index 00000000..5c030aa9 --- /dev/null +++ b/.ghci @@ -0,0 +1,9 @@ +:set -isrc -isrc + +:def hoogle \x -> return $ ":!hoogle " ++ x + +:def doc \x -> return $ ":!hoogle --info \"" ++ x ++ "\"" + +:set -w -fwarn-unused-binds -fwarn-unused-imports + +:load src/Main.hs \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6837ebd..de74fe65 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ cabal-dev *.hi *.chi *.chs.h +*.sublime-* +*.sublime-* +*.lksh? \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..30404ce4 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/README.md b/README.md index f6091398..46b5f36c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -distributed-process-platform -============================ \ No newline at end of file +# Welcome to Cloud Haskell Platform + +TODO + +# Join in! + +We are happy to receive bug reports, fixes, documentation enhancements, +and other improvements. + +Please report bugs via the +[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). + +Master [git repository](http://github.com/hyperthunk/distributed-process-platform): + +* `git clone git://github.com/hyperthunk/distributed-process-platform.git` + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 00000000..888379f1 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,3 @@ +# HEAD + +* Added initial GenServer module diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..5bde0de9 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,3 @@ +#!/usr/bin/env runhaskell +> import Distribution.Simple +> main = defaultMain diff --git a/benchmarks/dtp-benchmarks.cabal b/benchmarks/dtp-benchmarks.cabal new file mode 100644 index 00000000..a40d73f2 --- /dev/null +++ b/benchmarks/dtp-benchmarks.cabal @@ -0,0 +1,14 @@ +name: dtp-benchmarks +version: 0 +build-type: Simple + +cabal-version: >=1.8 + +executable dtp-benchmark + main-is: CallServer.hs + ghc-options: -Wall -O2 + build-depends: + base, + bytestring, + criterion, + distributed-process-platform diff --git a/benchmarks/src/CounterServer.hs b/benchmarks/src/CounterServer.hs new file mode 100644 index 00000000..2ddb1979 --- /dev/null +++ b/benchmarks/src/CounterServer.hs @@ -0,0 +1,17 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +import Blaze.ByteString.Builder (toLazyByteString) +import Blaze.ByteString.Builder.Char.Utf8 (fromString) +import Control.DeepSeq (NFData(rnf)) +import Criterion.Main +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.Internal as BL + +main :: IO () +main = do + defaultMain [ + --bgroup "call" [ + -- bench "incrementCount" $ nf undefined + -- bench "resetCount" $ nf undefined + --] + ] diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 581a275d..c7c8cb0b 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,4 +1,4 @@ -name: distributed-process-platform +name: distributed-process-platform version: 0.1.0 cabal-version: >=1.8 build-type: Simple @@ -16,6 +16,10 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform +flag developer + description: operate in developer mode + default: False + executable platform main-is: Main.hs build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 @@ -34,3 +38,22 @@ executable platform hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts +test-suite tests + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: + -Wall -threaded -rtsopts + build-depends: + QuickCheck, + aeson, + attoparsec, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time + diff --git a/tests/Properties.hs b/tests/Properties.hs new file mode 100644 index 00000000..ee3560f8 --- /dev/null +++ b/tests/Properties.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +import Control.Applicative +import Control.Monad +import Data.Data (Data, Typeable) +import qualified Data.Map as Map +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time (Day (..), LocalTime (..), + TimeOfDay (..), + TimeZone (..), + ZonedTime (..), + hoursToTimeZone) +import Test.Framework (Test, defaultMain, + testGroup) +import Test.Framework.Providers.QuickCheck2 (testProperty) +import Test.QuickCheck (Arbitrary (..), Gen, + choose) +main :: IO () +main = defaultMain tests + +tests :: [Test] +tests = [] From 9230b70e7f18a13564e0016531c4d9cb0a7d3122 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 18:45:09 -0500 Subject: [PATCH 0301/2357] added tests and benchmark files (no real content at this point) plus missing .gitignore and .ghci --- .ghci | 9 +++++++++ .gitignore | 3 +++ LICENSE | 1 + README.md | 18 ++++++++++++++++-- RELEASE-NOTES.md | 3 +++ Setup.hs | 3 +++ distributed-process-platform.cabal | 25 ++++++++++++++++++++++++- tests/Properties.hs | 28 ++++++++++++++++++++++++++++ 8 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 .ghci create mode 100644 LICENSE create mode 100644 RELEASE-NOTES.md create mode 100644 Setup.hs create mode 100644 tests/Properties.hs diff --git a/.ghci b/.ghci new file mode 100644 index 00000000..5c030aa9 --- /dev/null +++ b/.ghci @@ -0,0 +1,9 @@ +:set -isrc -isrc + +:def hoogle \x -> return $ ":!hoogle " ++ x + +:def doc \x -> return $ ":!hoogle --info \"" ++ x ++ "\"" + +:set -w -fwarn-unused-binds -fwarn-unused-imports + +:load src/Main.hs \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6837ebd..de74fe65 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ cabal-dev *.hi *.chi *.chs.h +*.sublime-* +*.sublime-* +*.lksh? \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..30404ce4 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/README.md b/README.md index f6091398..46b5f36c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -distributed-process-platform -============================ \ No newline at end of file +# Welcome to Cloud Haskell Platform + +TODO + +# Join in! + +We are happy to receive bug reports, fixes, documentation enhancements, +and other improvements. + +Please report bugs via the +[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). + +Master [git repository](http://github.com/hyperthunk/distributed-process-platform): + +* `git clone git://github.com/hyperthunk/distributed-process-platform.git` + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 00000000..888379f1 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,3 @@ +# HEAD + +* Added initial GenServer module diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..5bde0de9 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,3 @@ +#!/usr/bin/env runhaskell +> import Distribution.Simple +> main = defaultMain diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 581a275d..c7c8cb0b 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,4 +1,4 @@ -name: distributed-process-platform +name: distributed-process-platform version: 0.1.0 cabal-version: >=1.8 build-type: Simple @@ -16,6 +16,10 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform +flag developer + description: operate in developer mode + default: False + executable platform main-is: Main.hs build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 @@ -34,3 +38,22 @@ executable platform hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts +test-suite tests + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: + -Wall -threaded -rtsopts + build-depends: + QuickCheck, + aeson, + attoparsec, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time + diff --git a/tests/Properties.hs b/tests/Properties.hs new file mode 100644 index 00000000..ee3560f8 --- /dev/null +++ b/tests/Properties.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +import Control.Applicative +import Control.Monad +import Data.Data (Data, Typeable) +import qualified Data.Map as Map +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time (Day (..), LocalTime (..), + TimeOfDay (..), + TimeZone (..), + ZonedTime (..), + hoursToTimeZone) +import Test.Framework (Test, defaultMain, + testGroup) +import Test.Framework.Providers.QuickCheck2 (testProperty) +import Test.QuickCheck (Arbitrary (..), Gen, + choose) +main :: IO () +main = defaultMain tests + +tests :: [Test] +tests = [] From 0d894f74402c072199ebf1801804f32619752ed5 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 18:45:09 -0500 Subject: [PATCH 0302/2357] added tests and benchmark files (no real content at this point) plus missing .gitignore and .ghci --- .ghci | 9 +++++++++ .gitignore | 3 +++ LICENSE | 1 + README.md | 18 ++++++++++++++++-- RELEASE-NOTES.md | 3 +++ Setup.hs | 3 +++ distributed-process-platform.cabal | 25 ++++++++++++++++++++++++- 7 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .ghci create mode 100644 LICENSE create mode 100644 RELEASE-NOTES.md create mode 100644 Setup.hs diff --git a/.ghci b/.ghci new file mode 100644 index 00000000..5c030aa9 --- /dev/null +++ b/.ghci @@ -0,0 +1,9 @@ +:set -isrc -isrc + +:def hoogle \x -> return $ ":!hoogle " ++ x + +:def doc \x -> return $ ":!hoogle --info \"" ++ x ++ "\"" + +:set -w -fwarn-unused-binds -fwarn-unused-imports + +:load src/Main.hs \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6837ebd..de74fe65 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ cabal-dev *.hi *.chi *.chs.h +*.sublime-* +*.sublime-* +*.lksh? \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..30404ce4 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/README.md b/README.md index f6091398..46b5f36c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -distributed-process-platform -============================ \ No newline at end of file +# Welcome to Cloud Haskell Platform + +TODO + +# Join in! + +We are happy to receive bug reports, fixes, documentation enhancements, +and other improvements. + +Please report bugs via the +[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). + +Master [git repository](http://github.com/hyperthunk/distributed-process-platform): + +* `git clone git://github.com/hyperthunk/distributed-process-platform.git` + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 00000000..888379f1 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,3 @@ +# HEAD + +* Added initial GenServer module diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 00000000..5bde0de9 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,3 @@ +#!/usr/bin/env runhaskell +> import Distribution.Simple +> main = defaultMain diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 581a275d..c7c8cb0b 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,4 +1,4 @@ -name: distributed-process-platform +name: distributed-process-platform version: 0.1.0 cabal-version: >=1.8 build-type: Simple @@ -16,6 +16,10 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform +flag developer + description: operate in developer mode + default: False + executable platform main-is: Main.hs build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 @@ -34,3 +38,22 @@ executable platform hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts +test-suite tests + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: + -Wall -threaded -rtsopts + build-depends: + QuickCheck, + aeson, + attoparsec, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time + From f4df79e3915471c0852008fc73b8bdd9d56711a9 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 19:57:59 -0500 Subject: [PATCH 0303/2357] added scripts to run unit tests with hpc coverage reports, and run the executable with profiling enabled - this is just adding base infrastructure to the project --- RELEASE-NOTES.md => CHANGELOG.md | 0 README.md | 14 ++++ distributed-process-platform.cabal | 106 ++++++++++++++--------------- profiling/configure.sh | 3 + profiling/run.sh | 16 +++++ test-report.hs | 10 +++ 6 files changed, 96 insertions(+), 53 deletions(-) rename RELEASE-NOTES.md => CHANGELOG.md (100%) create mode 100755 profiling/configure.sh create mode 100755 profiling/run.sh create mode 100755 test-report.hs diff --git a/RELEASE-NOTES.md b/CHANGELOG.md similarity index 100% rename from RELEASE-NOTES.md rename to CHANGELOG.md diff --git a/README.md b/README.md index 46b5f36c..40f7aaac 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ TODO +# Tests and coverage + +The following script will configure cabal and run the unit tests with coverage enabled + +./test-report.sh + +# Profiling + +The following scripts will configure and run the executable with profiling enabled + +./profiling/configure.sh +./profiling/run.sh + + # Join in! We are happy to receive bug reports, fixes, documentation enhancements, diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index c7c8cb0b..0cbe46e7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,59 +1,59 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.8 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.10 +build-type: Simple +license: BSD3 +license-file: LICENSE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform flag developer - description: operate in developer mode - default: False + description: operate in developer mode + default: True -executable platform - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts +executable dtp + default-language: Haskell2010 + main-is: Main.hs + build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 + , mtl +-- , stm + , derive + , distributed-static + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp + buildable: True + default-extensions: UndecidableInstances ExistentialQuantification + ScopedTypeVariables FlexibleInstances CPP BangPatterns + GeneralizedNewtypeDeriving GADTs DeriveDataTypeable + hs-source-dirs: src + ghc-options: -Wall -threaded -rtsopts + if flag(developer) + ghc-options: -auto-all -test-suite tests - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: Properties.hs - ghc-options: - -Wall -threaded -rtsopts - build-depends: - QuickCheck, - aeson, - attoparsec, - base, - containers, - bytestring, - template-haskell, - test-framework, - test-framework-quickcheck2, - text, - time +test-suite unit-tests + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: -Wall -threaded -rtsopts + build-depends: QuickCheck, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time diff --git a/profiling/configure.sh b/profiling/configure.sh new file mode 100755 index 00000000..183b0912 --- /dev/null +++ b/profiling/configure.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cabal clean +cabal configure --enable-library-profiling --enable-executable-profiling diff --git a/profiling/run.sh b/profiling/run.sh new file mode 100755 index 00000000..04f5eeb1 --- /dev/null +++ b/profiling/run.sh @@ -0,0 +1,16 @@ +#!/bin/sh +PROG=dtp +VIEW=open +FLAGS= +DIST_DIR=./dist + + +cabal build +mkdir -p ${DIST_DIR}/profiling +( + cd ${DIST_DIR}/profiling + ../build/${PROG}/${PROG} ${FLAGS} +RTS -p -hc -s${PROG}.summary + hp2ps ${PROG}.hp +) +${VIEW} ${DIST_DIR}/profiling/${PROG}.ps +cat ${DIST_DIR}/profiling/${PROG}.summary \ No newline at end of file diff --git a/test-report.hs b/test-report.hs new file mode 100755 index 00000000..965edafa --- /dev/null +++ b/test-report.hs @@ -0,0 +1,10 @@ +#! /bin/sh + +HPC_DIR=dist/hpc + +cabal clean +cabal configure --enable-tests --enable-library-coverage +cabal build +cabal test + +open ${HPC_DIR}/html/*/hpc-index.html From 4a5d3b229cf40c9252859c75c9e7c02ce2361878 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 19:57:59 -0500 Subject: [PATCH 0304/2357] added scripts to run unit tests with hpc coverage reports, and run the executable with profiling enabled - this is just adding base infrastructure to the project --- README.md | 14 ++++ RELEASE-NOTES.md | 3 - distributed-process-platform.cabal | 106 ++++++++++++++--------------- test-report.hs | 10 +++ 4 files changed, 77 insertions(+), 56 deletions(-) delete mode 100644 RELEASE-NOTES.md create mode 100755 test-report.hs diff --git a/README.md b/README.md index 46b5f36c..40f7aaac 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ TODO +# Tests and coverage + +The following script will configure cabal and run the unit tests with coverage enabled + +./test-report.sh + +# Profiling + +The following scripts will configure and run the executable with profiling enabled + +./profiling/configure.sh +./profiling/run.sh + + # Join in! We are happy to receive bug reports, fixes, documentation enhancements, diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md deleted file mode 100644 index 888379f1..00000000 --- a/RELEASE-NOTES.md +++ /dev/null @@ -1,3 +0,0 @@ -# HEAD - -* Added initial GenServer module diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index c7c8cb0b..0cbe46e7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,59 +1,59 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.8 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.10 +build-type: Simple +license: BSD3 +license-file: LICENSE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform flag developer - description: operate in developer mode - default: False + description: operate in developer mode + default: True -executable platform - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts +executable dtp + default-language: Haskell2010 + main-is: Main.hs + build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 + , mtl +-- , stm + , derive + , distributed-static + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp + buildable: True + default-extensions: UndecidableInstances ExistentialQuantification + ScopedTypeVariables FlexibleInstances CPP BangPatterns + GeneralizedNewtypeDeriving GADTs DeriveDataTypeable + hs-source-dirs: src + ghc-options: -Wall -threaded -rtsopts + if flag(developer) + ghc-options: -auto-all -test-suite tests - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: Properties.hs - ghc-options: - -Wall -threaded -rtsopts - build-depends: - QuickCheck, - aeson, - attoparsec, - base, - containers, - bytestring, - template-haskell, - test-framework, - test-framework-quickcheck2, - text, - time +test-suite unit-tests + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: -Wall -threaded -rtsopts + build-depends: QuickCheck, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time diff --git a/test-report.hs b/test-report.hs new file mode 100755 index 00000000..965edafa --- /dev/null +++ b/test-report.hs @@ -0,0 +1,10 @@ +#! /bin/sh + +HPC_DIR=dist/hpc + +cabal clean +cabal configure --enable-tests --enable-library-coverage +cabal build +cabal test + +open ${HPC_DIR}/html/*/hpc-index.html From b10935c1d785271e8e82620321472a4403486da9 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 19:57:59 -0500 Subject: [PATCH 0305/2357] added scripts to run unit tests with hpc coverage reports, and run the executable with profiling enabled - this is just adding base infrastructure to the project --- RELEASE-NOTES.md => CHANGELOG.md | 0 README.md | 14 ++++ distributed-process-platform.cabal | 106 ++++++++++++++--------------- test-report.hs | 10 +++ 4 files changed, 77 insertions(+), 53 deletions(-) rename RELEASE-NOTES.md => CHANGELOG.md (100%) create mode 100755 test-report.hs diff --git a/RELEASE-NOTES.md b/CHANGELOG.md similarity index 100% rename from RELEASE-NOTES.md rename to CHANGELOG.md diff --git a/README.md b/README.md index 46b5f36c..40f7aaac 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ TODO +# Tests and coverage + +The following script will configure cabal and run the unit tests with coverage enabled + +./test-report.sh + +# Profiling + +The following scripts will configure and run the executable with profiling enabled + +./profiling/configure.sh +./profiling/run.sh + + # Join in! We are happy to receive bug reports, fixes, documentation enhancements, diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index c7c8cb0b..0cbe46e7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,59 +1,59 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.8 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.10 +build-type: Simple +license: BSD3 +license-file: LICENSE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform flag developer - description: operate in developer mode - default: False + description: operate in developer mode + default: True -executable platform - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts +executable dtp + default-language: Haskell2010 + main-is: Main.hs + build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 + , mtl +-- , stm + , derive + , distributed-static + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp + buildable: True + default-extensions: UndecidableInstances ExistentialQuantification + ScopedTypeVariables FlexibleInstances CPP BangPatterns + GeneralizedNewtypeDeriving GADTs DeriveDataTypeable + hs-source-dirs: src + ghc-options: -Wall -threaded -rtsopts + if flag(developer) + ghc-options: -auto-all -test-suite tests - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: Properties.hs - ghc-options: - -Wall -threaded -rtsopts - build-depends: - QuickCheck, - aeson, - attoparsec, - base, - containers, - bytestring, - template-haskell, - test-framework, - test-framework-quickcheck2, - text, - time +test-suite unit-tests + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: -Wall -threaded -rtsopts + build-depends: QuickCheck, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time diff --git a/test-report.hs b/test-report.hs new file mode 100755 index 00000000..965edafa --- /dev/null +++ b/test-report.hs @@ -0,0 +1,10 @@ +#! /bin/sh + +HPC_DIR=dist/hpc + +cabal clean +cabal configure --enable-tests --enable-library-coverage +cabal build +cabal test + +open ${HPC_DIR}/html/*/hpc-index.html From 375a1993a75588ec7f9366ca73a090a52d5027af Mon Sep 17 00:00:00 2001 From: RodLogic Date: Tue, 27 Nov 2012 19:57:59 -0500 Subject: [PATCH 0306/2357] added scripts to run unit tests with hpc coverage reports, and run the executable with profiling enabled - this is just adding base infrastructure to the project --- RELEASE-NOTES.md => CHANGELOG.md | 0 README.md | 14 ++++ distributed-process-platform.cabal | 106 ++++++++++++++--------------- profiling/configure.sh | 3 + profiling/run.sh | 16 +++++ test-report.hs | 10 +++ 6 files changed, 96 insertions(+), 53 deletions(-) rename RELEASE-NOTES.md => CHANGELOG.md (100%) create mode 100755 profiling/configure.sh create mode 100755 profiling/run.sh create mode 100755 test-report.hs diff --git a/RELEASE-NOTES.md b/CHANGELOG.md similarity index 100% rename from RELEASE-NOTES.md rename to CHANGELOG.md diff --git a/README.md b/README.md index 46b5f36c..40f7aaac 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ TODO +# Tests and coverage + +The following script will configure cabal and run the unit tests with coverage enabled + +./test-report.sh + +# Profiling + +The following scripts will configure and run the executable with profiling enabled + +./profiling/configure.sh +./profiling/run.sh + + # Join in! We are happy to receive bug reports, fixes, documentation enhancements, diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index c7c8cb0b..0cbe46e7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,59 +1,59 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.8 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.10 +build-type: Simple +license: BSD3 +license-file: LICENSE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform flag developer - description: operate in developer mode - default: False + description: operate in developer mode + default: True -executable platform - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts +executable dtp + default-language: Haskell2010 + main-is: Main.hs + build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 + , mtl +-- , stm + , derive + , distributed-static + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp + buildable: True + default-extensions: UndecidableInstances ExistentialQuantification + ScopedTypeVariables FlexibleInstances CPP BangPatterns + GeneralizedNewtypeDeriving GADTs DeriveDataTypeable + hs-source-dirs: src + ghc-options: -Wall -threaded -rtsopts + if flag(developer) + ghc-options: -auto-all -test-suite tests - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: Properties.hs - ghc-options: - -Wall -threaded -rtsopts - build-depends: - QuickCheck, - aeson, - attoparsec, - base, - containers, - bytestring, - template-haskell, - test-framework, - test-framework-quickcheck2, - text, - time +test-suite unit-tests + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Properties.hs + ghc-options: -Wall -threaded -rtsopts + build-depends: QuickCheck, + base, + containers, + bytestring, + template-haskell, + test-framework, + test-framework-quickcheck2, + text, + time diff --git a/profiling/configure.sh b/profiling/configure.sh new file mode 100755 index 00000000..183b0912 --- /dev/null +++ b/profiling/configure.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cabal clean +cabal configure --enable-library-profiling --enable-executable-profiling diff --git a/profiling/run.sh b/profiling/run.sh new file mode 100755 index 00000000..04f5eeb1 --- /dev/null +++ b/profiling/run.sh @@ -0,0 +1,16 @@ +#!/bin/sh +PROG=dtp +VIEW=open +FLAGS= +DIST_DIR=./dist + + +cabal build +mkdir -p ${DIST_DIR}/profiling +( + cd ${DIST_DIR}/profiling + ../build/${PROG}/${PROG} ${FLAGS} +RTS -p -hc -s${PROG}.summary + hp2ps ${PROG}.hp +) +${VIEW} ${DIST_DIR}/profiling/${PROG}.ps +cat ${DIST_DIR}/profiling/${PROG}.summary \ No newline at end of file diff --git a/test-report.hs b/test-report.hs new file mode 100755 index 00000000..965edafa --- /dev/null +++ b/test-report.hs @@ -0,0 +1,10 @@ +#! /bin/sh + +HPC_DIR=dist/hpc + +cabal clean +cabal configure --enable-tests --enable-library-coverage +cabal build +cabal test + +open ${HPC_DIR}/html/*/hpc-index.html From 1021020e84203d53542925fb92bb50f8ee18ba15 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0307/2357] Remove trailing whitespace --- .../Process/Backend/SimpleLocalnet.hs | 104 +++++++++--------- .../SimpleLocalnet/Internal/Multicast.hs | 38 +++---- tests/TestSimpleLocalnet.hs | 6 +- tests/runTestSimpleLocalnet.hs | 8 +- 4 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index 226e2398..bff4ca25 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -3,41 +3,41 @@ -- get you going with Cloud Haskell quickly without imposing any structure -- on your application. -- --- To simplify getting started we provide special support for /master/ and +-- To simplify getting started we provide special support for /master/ and -- /slave/ nodes (see 'startSlave' and 'startMaster'). Use of these functions -- is completely optional; you can use the local backend without making use -- of the predefined master and slave nodes. --- +-- -- [Minimal example] -- -- > import System.Environment (getArgs) -- > import Control.Distributed.Process -- > import Control.Distributed.Process.Node (initRemoteTable) -- > import Control.Distributed.Process.Backend.SimpleLocalnet --- > +-- > -- > master :: Backend -> [NodeId] -> Process () -- > master backend slaves = do -- > -- Do something interesting with the slaves -- > liftIO . putStrLn $ "Slaves: " ++ show slaves -- > -- Terminate the slaves when the master terminates (this is optional) -- > terminateAllSlaves backend --- > +-- > -- > main :: IO () -- > main = do -- > args <- getArgs --- > +-- > -- > case args of -- > ["master", host, port] -> do --- > backend <- initializeBackend host port initRemoteTable +-- > backend <- initializeBackend host port initRemoteTable -- > startMaster backend (master backend) -- > ["slave", host, port] -> do --- > backend <- initializeBackend host port initRemoteTable +-- > backend <- initializeBackend host port initRemoteTable -- > startSlave backend --- +-- -- [Compiling and Running] -- -- Save to @example.hs@ and compile using --- +-- -- > ghc -threaded example.hs -- -- Fire up some slave nodes (for the example, we run them on a single machine): @@ -68,7 +68,7 @@ -- master on a fifth node (or on any of the four machines that run the slave -- nodes). -- --- It is important that every node has a unique (hostname, port number) pair, +-- It is important that every node has a unique (hostname, port number) pair, -- and that the hostname you use to initialize the node can be resolved by -- peer nodes. In other words, if you start a node and pass hostname @localhost@ -- then peer nodes won't be able to reach it because @localhost@ will resolve @@ -79,10 +79,10 @@ -- If you try the above example and the master process cannot find any slaves, -- then it might be that your firewall settings do not allow for UDP multicast -- (in particular, the default iptables on some Linux distributions might not --- allow it). +-- allow it). {-# OPTIONS_GHC -fno-warn-orphans #-} module Control.Distributed.Process.Backend.SimpleLocalnet - ( -- * Initialization + ( -- * Initialization Backend(..) , initializeBackend -- * Slave nodes @@ -128,13 +128,13 @@ import Control.Distributed.Process , unmonitor , NodeMonitorNotification(..) ) -import qualified Control.Distributed.Process.Node as Node +import qualified Control.Distributed.Process.Node as Node ( LocalNode , newLocalNode , localNodeId , runProcess ) -import qualified Network.Transport.TCP as NT +import qualified Network.Transport.TCP as NT ( createTransport , defaultTCPParameters ) @@ -142,7 +142,7 @@ import qualified Network.Transport as NT (Transport) import qualified Network.Socket as N (HostName, ServiceName, SockAddr) import Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast (initMulticast) --- | Local backend +-- | Local backend data Backend = Backend { -- | Create a new local node newLocalNode :: IO Node.LocalNode @@ -164,64 +164,64 @@ data BackendState = BackendState { -- | Initialize the backend initializeBackend :: N.HostName -> N.ServiceName -> RemoteTable -> IO Backend initializeBackend host port rtable = do - mTransport <- NT.createTransport host port NT.defaultTCPParameters + mTransport <- NT.createTransport host port NT.defaultTCPParameters (recv, send) <- initMulticast "224.0.0.99" 9999 1024 (_, backendState) <- fixIO $ \ ~(tid, _) -> do - backendState <- newMVar BackendState - { _localNodes = [] + backendState <- newMVar BackendState + { _localNodes = [] , _peers = Set.empty , discoveryDaemon = tid } - tid' <- forkIO $ peerDiscoveryDaemon backendState recv send + tid' <- forkIO $ peerDiscoveryDaemon backendState recv send return (tid', backendState) case mTransport of Left err -> throw err - Right transport -> + Right transport -> let backend = Backend { - newLocalNode = apiNewLocalNode transport rtable backendState + newLocalNode = apiNewLocalNode transport rtable backendState , findPeers = apiFindPeers send backendState - , redirectLogsHere = apiRedirectLogsHere backend + , redirectLogsHere = apiRedirectLogsHere backend } in return backend -- | Create a new local node -apiNewLocalNode :: NT.Transport - -> RemoteTable +apiNewLocalNode :: NT.Transport + -> RemoteTable -> MVar BackendState -> IO Node.LocalNode apiNewLocalNode transport rtable backendState = do - localNode <- Node.newLocalNode transport rtable + localNode <- Node.newLocalNode transport rtable modifyMVar_ backendState $ return . (localNodes ^: (localNode :)) return localNode -- | Peer discovery -apiFindPeers :: (PeerDiscoveryMsg -> IO ()) - -> MVar BackendState +apiFindPeers :: (PeerDiscoveryMsg -> IO ()) + -> MVar BackendState -> Int -> IO [NodeId] apiFindPeers send backendState delay = do - send PeerDiscoveryRequest - threadDelay delay - Set.toList . (^. peers) <$> readMVar backendState + send PeerDiscoveryRequest + threadDelay delay + Set.toList . (^. peers) <$> readMVar backendState -data PeerDiscoveryMsg = - PeerDiscoveryRequest +data PeerDiscoveryMsg = + PeerDiscoveryRequest | PeerDiscoveryReply NodeId instance Binary PeerDiscoveryMsg where put PeerDiscoveryRequest = putWord8 0 put (PeerDiscoveryReply nid) = putWord8 1 >> put nid get = do - header <- getWord8 + header <- getWord8 case header of 0 -> return PeerDiscoveryRequest 1 -> PeerDiscoveryReply <$> get _ -> fail "PeerDiscoveryMsg.get: invalid" -- | Respond to peer discovery requests sent by other nodes -peerDiscoveryDaemon :: MVar BackendState +peerDiscoveryDaemon :: MVar BackendState -> IO (PeerDiscoveryMsg, N.SockAddr) - -> (PeerDiscoveryMsg -> IO ()) + -> (PeerDiscoveryMsg -> IO ()) -> IO () peerDiscoveryDaemon backendState recv send = forever go where @@ -230,7 +230,7 @@ peerDiscoveryDaemon backendState recv send = forever go case msg of PeerDiscoveryRequest -> do nodes <- (^. localNodes) <$> readMVar backendState - forM_ nodes $ send . PeerDiscoveryReply . Node.localNodeId + forM_ nodes $ send . PeerDiscoveryReply . Node.localNodeId PeerDiscoveryReply nid -> modifyMVar_ backendState $ return . (peers ^: Set.insert nid) @@ -243,7 +243,7 @@ apiRedirectLogsHere :: Backend -> Process () apiRedirectLogsHere backend = do mLogger <- whereis "logger" forM_ mLogger $ \logger -> do - nids <- liftIO $ findPeers backend 1000000 + nids <- liftIO $ findPeers backend 1000000 forM_ nids $ \nid -> reregisterRemoteAsync nid "logger" logger -- ignore async response -------------------------------------------------------------------------------- @@ -254,12 +254,12 @@ apiRedirectLogsHere backend = do -- -- This datatype is not exposed; instead, we expose primitives for dealing -- with slaves. -data SlaveControllerMsg = +data SlaveControllerMsg = SlaveTerminate deriving (Typeable, Show) instance Binary SlaveControllerMsg where - put SlaveTerminate = putWord8 0 + put SlaveTerminate = putWord8 0 get = do header <- getWord8 case header of @@ -273,8 +273,8 @@ instance Binary SlaveControllerMsg where -- the process or call terminateSlave from another node. startSlave :: Backend -> IO () startSlave backend = do - node <- newLocalNode backend - Node.runProcess node slaveController + node <- newLocalNode backend + Node.runProcess node slaveController -- | The slave controller interprets 'SlaveControllerMsg's slaveController :: Process () @@ -295,26 +295,26 @@ terminateSlave nid = nsendRemote nid "slaveController" SlaveTerminate -- | Find slave nodes findSlaves :: Backend -> Process [NodeId] findSlaves backend = do - nodes <- liftIO $ findPeers backend 1000000 + nodes <- liftIO $ findPeers backend 1000000 -- Fire of asynchronous requests for the slave controller refs <- forM nodes $ \nid -> do - whereisRemoteAsync nid "slaveController" + whereisRemoteAsync nid "slaveController" ref <- monitorNode nid return (nid, ref) -- Wait for the replies - catMaybes <$> replicateM (length nodes) ( - receiveWait + catMaybes <$> replicateM (length nodes) ( + receiveWait [ matchIf (\(WhereIsReply label _) -> label == "slaveController") - (\(WhereIsReply _ mPid) -> + (\(WhereIsReply _ mPid) -> case mPid of - Nothing -> + Nothing -> return Nothing Just pid -> do let nid = processNodeId pid Just ref = lookup nid refs - unmonitor ref + unmonitor ref return (Just nid)) - , match (\(NodeMonitorNotification {}) -> return Nothing) + , match (\(NodeMonitorNotification {}) -> return Nothing) ]) -- | Terminate all slaves @@ -330,15 +330,15 @@ terminateAllSlaves backend = do -- | 'startMaster' finds all slaves /currently/ available on the local network, -- redirects all log messages to itself, and then calls the specified process, --- passing the list of slaves nodes. +-- passing the list of slaves nodes. -- -- Terminates when the specified process terminates. If you want to terminate --- the slaves when the master terminates, you should manually call +-- the slaves when the master terminates, you should manually call -- 'terminateAllSlaves'. -- -- If you start more slave nodes after having started the master node, you can -- discover them with later calls to 'findSlaves', but be aware that you will --- need to call 'redirectLogHere' to redirect their logs to the master node. +-- need to call 'redirectLogHere' to redirect their logs to the master node. -- -- Note that you can use functionality of "SimpleLocalnet" directly (through -- 'Backend'), instead of using 'startMaster'/'startSlave', if the master/slave diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs index 01b3e84c..03131de0 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet/Internal/Multicast.hs @@ -8,7 +8,7 @@ import qualified Data.Map as Map (empty) import Data.Binary (Binary, decode, encode) import Data.IORef (IORef, newIORef, readIORef, modifyIORef) import qualified Data.ByteString as BSS (ByteString, concat) -import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy as BSL ( ByteString , empty , append @@ -38,10 +38,10 @@ import Network.Multicast (multicastSender, multicastReceiver) -- -- NOTE: By rights the two functions should be "locally" polymorphic in 'a', -- but this requires impredicative types. -initMulticast :: forall a. Binary a +initMulticast :: forall a. Binary a => HostName -- ^ Multicast IP -> PortNumber -- ^ Port number - -> Int -- ^ Maximum message size + -> Int -- ^ Maximum message size -> IO (IO (a, SockAddr), a -> IO ()) initMulticast host port bufferSize = do (sendSock, sendAddr) <- multicastSender host port @@ -50,8 +50,8 @@ initMulticast host port bufferSize = do return (recvBinary readSock st bufferSize, writer sendSock sendAddr) where writer :: forall a. Binary a => Socket -> SockAddr -> a -> IO () - writer sock addr val = do - let bytes = encode val + writer sock addr val = do + let bytes = encode val len = encodeInt32 (BSL.length bytes) NBS.sendManyTo sock (len : BSL.toChunks bytes) addr @@ -59,7 +59,7 @@ initMulticast host port bufferSize = do -- UDP multicast read, dealing with multiple senders -- -------------------------------------------------------------------------------- -type UDPState = Map SockAddr BSL.ByteString +type UDPState = Map SockAddr BSL.ByteString #if MIN_VERSION_network(2,4,0) -- network-2.4.0 provides the Ord instance for us @@ -72,16 +72,16 @@ bufferFor :: SockAddr -> Accessor UDPState BSL.ByteString bufferFor = DAC.mapDefault BSL.empty bufferAppend :: SockAddr -> BSS.ByteString -> UDPState -> UDPState -bufferAppend addr bytes = - bufferFor addr ^: flip BSL.append (BSL.fromChunks [bytes]) +bufferAppend addr bytes = + bufferFor addr ^: flip BSL.append (BSL.fromChunks [bytes]) recvBinary :: Binary a => Socket -> IORef UDPState -> Int -> IO (a, SockAddr) recvBinary sock st bufferSize = do (bytes, addr) <- recvWithLength sock st bufferSize return (decode bytes, addr) -recvWithLength :: Socket - -> IORef UDPState +recvWithLength :: Socket + -> IORef UDPState -> Int -> IO (BSL.ByteString, SockAddr) recvWithLength sock st bufferSize = do @@ -93,13 +93,13 @@ recvWithLength sock st bufferSize = do -- Receive all bytes currently in the buffer recvAll :: Socket -> IORef UDPState -> Int -> IO SockAddr recvAll sock st bufferSize = do - (bytes, addr) <- NBS.recvFrom sock bufferSize + (bytes, addr) <- NBS.recvFrom sock bufferSize modifyIORef st $ bufferAppend addr bytes return addr - -recvExact :: Socket - -> Int - -> IORef UDPState + +recvExact :: Socket + -> Int + -> IORef UDPState -> Int -> IO (BSL.ByteString, SockAddr) recvExact sock n st bufferSize = do @@ -113,16 +113,16 @@ recvExactFrom :: SockAddr -> IORef UDPState -> Int -> IO BSL.ByteString -recvExactFrom addr sock n st bufferSize = go +recvExactFrom addr sock n st bufferSize = go where go :: IO BSL.ByteString go = do - accAddr <- (^. bufferFor addr) <$> readIORef st - if BSL.length accAddr >= fromIntegral n + accAddr <- (^. bufferFor addr) <$> readIORef st + if BSL.length accAddr >= fromIntegral n then do let (bytes, accAddr') = BSL.splitAt (fromIntegral n) accAddr modifyIORef st $ bufferFor addr ^= accAddr' return bytes - else do + else do _ <- recvAll sock st bufferSize go diff --git a/tests/TestSimpleLocalnet.hs b/tests/TestSimpleLocalnet.hs index 83b9c836..fc9d820c 100644 --- a/tests/TestSimpleLocalnet.hs +++ b/tests/TestSimpleLocalnet.hs @@ -15,10 +15,10 @@ main = do case args of ["master", host, port] -> do - backend <- initializeBackend host port initRemoteTable + backend <- initializeBackend host port initRemoteTable startMaster backend (master backend) ["slave", host, port] -> do - backend <- initializeBackend host port initRemoteTable + backend <- initializeBackend host port initRemoteTable startSlave backend - _ -> + _ -> putStrLn $ "usage: " ++ prog ++ " (master | slave) host port" diff --git a/tests/runTestSimpleLocalnet.hs b/tests/runTestSimpleLocalnet.hs index e8b3bf4c..74374302 100755 --- a/tests/runTestSimpleLocalnet.hs +++ b/tests/runTestSimpleLocalnet.hs @@ -1,11 +1,11 @@ #!/bin/bash TestSimpleLocalnet=dist/build/TestSimpleLocalnet/TestSimpleLocalnet -$TestSimpleLocalnet slave 127.0.0.1 8080 & +$TestSimpleLocalnet slave 127.0.0.1 8080 & sleep 1 -$TestSimpleLocalnet slave 127.0.0.1 8081 & +$TestSimpleLocalnet slave 127.0.0.1 8081 & sleep 1 -$TestSimpleLocalnet slave 127.0.0.1 8082 & +$TestSimpleLocalnet slave 127.0.0.1 8082 & sleep 1 -$TestSimpleLocalnet slave 127.0.0.1 8083 & +$TestSimpleLocalnet slave 127.0.0.1 8083 & sleep 1 $TestSimpleLocalnet master 127.0.0.1 8084 From 49458e9ca5ee7037f9a660fae214281a5b4617c4 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0308/2357] Remove trailing whitespace --- src/Network/Transport/Chan.hs | 76 +++++++++++++++++------------------ tests/TestInMemory.hs | 2 +- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Network/Transport/Chan.hs b/src/Network/Transport/Chan.hs index 1c194e24..025c8fc1 100644 --- a/src/Network/Transport/Chan.hs +++ b/src/Network/Transport/Chan.hs @@ -1,7 +1,7 @@ -- | In-memory implementation of the Transport API. module Network.Transport.Chan (createTransport) where -import Network.Transport +import Network.Transport import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) import Control.Applicative ((<$>)) import Control.Category ((>>>)) @@ -15,11 +15,11 @@ import qualified Data.Set as Set (empty, elems, insert, delete) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as BSC (pack) import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) -import qualified Data.Accessor.Container as DAC (mapMaybe) +import qualified Data.Accessor.Container as DAC (mapMaybe) -- Global state: next available "address", mapping from addresses to channels and next available connection -data TransportState = State { _channels :: Map EndPointAddress (Chan Event) - , _nextConnectionId :: Map EndPointAddress ConnectionId +data TransportState = State { _channels :: Map EndPointAddress (Chan Event) + , _nextConnectionId :: Map EndPointAddress ConnectionId , _multigroups :: Map MulticastAddress (MVar (Set EndPointAddress)) } @@ -29,11 +29,11 @@ data TransportState = State { _channels :: Map EndPointAddress (Chan Eve -- (threads can, and should, create their own endpoints though). createTransport :: IO Transport createTransport = do - state <- newMVar State { _channels = Map.empty + state <- newMVar State { _channels = Map.empty , _nextConnectionId = Map.empty , _multigroups = Map.empty } - return Transport { newEndPoint = apiNewEndPoint state + return Transport { newEndPoint = apiNewEndPoint state , closeTransport = throwIO (userError "closeEndPoint not implemented") } @@ -44,46 +44,46 @@ apiNewEndPoint state = do addr <- modifyMVar state $ \st -> do let addr = EndPointAddress . BSC.pack . show . Map.size $ st ^. channels return ((channelAt addr ^= chan) . (nextConnectionIdAt addr ^= 1) $ st, addr) - return . Right $ EndPoint { receive = readChan chan + return . Right $ EndPoint { receive = readChan chan , address = addr - , connect = apiConnect addr state + , connect = apiConnect addr state , closeEndPoint = throwIO (userError "closeEndPoint not implemented") , newMulticastGroup = apiNewMulticastGroup state addr , resolveMulticastGroup = apiResolveMulticastGroup state addr } - + -- | Create a new connection -apiConnect :: EndPointAddress - -> MVar TransportState - -> EndPointAddress - -> Reliability - -> ConnectHints +apiConnect :: EndPointAddress + -> MVar TransportState + -> EndPointAddress + -> Reliability + -> ConnectHints -> IO (Either (TransportError ConnectErrorCode) Connection) -apiConnect myAddress state theirAddress _reliability _hints = do +apiConnect myAddress state theirAddress _reliability _hints = do (chan, conn) <- modifyMVar state $ \st -> do let chan = st ^. channelAt theirAddress let conn = st ^. nextConnectionIdAt theirAddress return (nextConnectionIdAt theirAddress ^: (+ 1) $ st, (chan, conn)) writeChan chan $ ConnectionOpened conn ReliableOrdered myAddress connAlive <- newMVar True - return . Right $ Connection { send = apiSend chan conn connAlive - , close = apiClose chan conn connAlive - } + return . Right $ Connection { send = apiSend chan conn connAlive + , close = apiClose chan conn connAlive + } -- | Send a message over a connection apiSend :: Chan Event -> ConnectionId -> MVar Bool -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) -apiSend chan conn connAlive msg = - modifyMVar connAlive $ \alive -> - if alive - then do +apiSend chan conn connAlive msg = + modifyMVar connAlive $ \alive -> + if alive + then do writeChan chan (Received conn msg) return (alive, Right ()) - else + else return (alive, Left (TransportError SendFailed "Connection closed")) -- | Close a connection apiClose :: Chan Event -> ConnectionId -> MVar Bool -> IO () -apiClose chan conn connAlive = +apiClose chan conn connAlive = modifyMVar_ connAlive $ \alive -> do when alive . writeChan chan $ ConnectionClosed conn return False @@ -91,7 +91,7 @@ apiClose chan conn connAlive = -- | Create a new multicast group apiNewMulticastGroup :: MVar TransportState -> EndPointAddress -> IO (Either (TransportError NewMulticastGroupErrorCode) MulticastGroup) apiNewMulticastGroup state ourAddress = do - group <- newMVar Set.empty + group <- newMVar Set.empty groupAddr <- modifyMVar state $ \st -> do let addr = MulticastAddress . BSC.pack . show . Map.size $ st ^. multigroups return (multigroupAt addr ^= group $ st, addr) @@ -105,30 +105,30 @@ apiNewMulticastGroup state ourAddress = do -- deleted. createMulticastGroup :: MVar TransportState -> EndPointAddress -> MulticastAddress -> MVar (Set EndPointAddress) -> MulticastGroup createMulticastGroup state ourAddress groupAddress group = - MulticastGroup { multicastAddress = groupAddress + MulticastGroup { multicastAddress = groupAddress , deleteMulticastGroup = modifyMVar_ state $ return . (multigroups ^: Map.delete groupAddress) , maxMsgSize = Nothing - , multicastSend = \payload -> do + , multicastSend = \payload -> do cs <- (^. channels) <$> readMVar state - es <- readMVar group - forM_ (Set.elems es) $ \ep -> do + es <- readMVar group + forM_ (Set.elems es) $ \ep -> do let ch = cs ^. at ep "Invalid endpoint" - writeChan ch (ReceivedMulticast groupAddress payload) + writeChan ch (ReceivedMulticast groupAddress payload) , multicastSubscribe = modifyMVar_ group $ return . Set.insert ourAddress , multicastUnsubscribe = modifyMVar_ group $ return . Set.delete ourAddress - , multicastClose = return () + , multicastClose = return () } -- | Resolve a multicast group -apiResolveMulticastGroup :: MVar TransportState +apiResolveMulticastGroup :: MVar TransportState -> EndPointAddress - -> MulticastAddress + -> MulticastAddress -> IO (Either (TransportError ResolveMulticastGroupErrorCode) MulticastGroup) apiResolveMulticastGroup state ourAddress groupAddress = do - group <- (^. (multigroups >>> DAC.mapMaybe groupAddress)) <$> readMVar state + group <- (^. (multigroups >>> DAC.mapMaybe groupAddress)) <$> readMVar state case group of Nothing -> return . Left $ TransportError ResolveMulticastGroupNotFound ("Group " ++ show groupAddress ++ " not found") - Just mvar -> return . Right $ createMulticastGroup state ourAddress groupAddress mvar + Just mvar -> return . Right $ createMulticastGroup state ourAddress groupAddress mvar -------------------------------------------------------------------------------- -- Lens definitions -- @@ -141,12 +141,12 @@ nextConnectionId :: Accessor TransportState (Map EndPointAddress ConnectionId) nextConnectionId = accessor _nextConnectionId (\cid st -> st { _nextConnectionId = cid }) multigroups :: Accessor TransportState (Map MulticastAddress (MVar (Set EndPointAddress))) -multigroups = accessor _multigroups (\gs st -> st { _multigroups = gs }) +multigroups = accessor _multigroups (\gs st -> st { _multigroups = gs }) at :: Ord k => k -> String -> Accessor (Map k v) v -at k err = accessor (Map.findWithDefault (error err) k) (Map.insert k) +at k err = accessor (Map.findWithDefault (error err) k) (Map.insert k) -channelAt :: EndPointAddress -> Accessor TransportState (Chan Event) +channelAt :: EndPointAddress -> Accessor TransportState (Chan Event) channelAt addr = channels >>> at addr "Invalid channel" nextConnectionIdAt :: EndPointAddress -> Accessor TransportState ConnectionId diff --git a/tests/TestInMemory.hs b/tests/TestInMemory.hs index 64c274d0..853b9b09 100644 --- a/tests/TestInMemory.hs +++ b/tests/TestInMemory.hs @@ -3,6 +3,6 @@ module Main where import Network.Transport.Tests import Network.Transport.Chan import Control.Applicative ((<$>)) - + main :: IO () main = testTransport (Right <$> createTransport) From c76672ee2f6d9f65d22ff467496eb246f1f828fe Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0309/2357] Remove trailing whitespace --- src/Data/Rank1Dynamic.hs | 42 ++++++++-------- src/Data/Rank1Typeable.hs | 102 +++++++++++++++++++------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/Data/Rank1Dynamic.hs b/src/Data/Rank1Dynamic.hs index 55b7464b..2fb9d02d 100644 --- a/src/Data/Rank1Dynamic.hs +++ b/src/Data/Rank1Dynamic.hs @@ -5,42 +5,42 @@ -- These examples correspond to the 'Data.Rank1Typeable.isInstanceOf' examples -- in "Data.Rank1Typeable". -- --- > > do f <- fromDynamic (toDynamic (even :: Int -> Bool)) ; return $ (f :: Int -> Int) 0 +-- > > do f <- fromDynamic (toDynamic (even :: Int -> Bool)) ; return $ (f :: Int -> Int) 0 -- > Left "Cannot unify Int and Bool" --- > +-- > -- > > do f <- fromDynamic (toDynamic (const 1 :: ANY -> Int)) ; return $ (f :: Int -> Int) 0 -- > Right 1 --- > +-- > -- > > do f <- fromDynamic (toDynamic (unsafeCoerce :: ANY1 -> ANY2)) ; return $ (f :: Int -> Int) 0 -- > Right 0 --- > +-- > -- > > do f <- fromDynamic (toDynamic (id :: ANY -> ANY)) ; return $ (f :: Int -> Bool) 0 -- > Left "Cannot unify Bool and Int" --- > +-- > -- > > do f <- fromDynamic (toDynamic (undefined :: ANY)) ; return $ (f :: Int -> Int) 0 -- > Right *** Exception: Prelude.undefined --- > +-- > -- > > do f <- fromDynamic (toDynamic (id :: ANY -> ANY)) ; return $ (f :: Int) -- > Left "Cannot unify Int and ->" -- -- [Examples of dynApply] --- +-- -- These examples correspond to the 'Data.Rank1Typeable.funResultTy' examples -- in "Data.Rank1Typeable". -- -- > > do app <- toDynamic (id :: ANY -> ANY) `dynApply` toDynamic True ; f <- fromDynamic app ; return $ (f :: Bool) -- > Right True --- > +-- > -- > > do app <- toDynamic (const :: ANY -> ANY1 -> ANY) `dynApply` toDynamic True ; f <- fromDynamic app ; return $ (f :: Int -> Bool) 0 -- > Right True --- > +-- > -- > > do app <- toDynamic (($ True) :: (Bool -> ANY) -> ANY) `dynApply` toDynamic (id :: ANY -> ANY) ; f <- fromDynamic app ; return (f :: Bool) -- > Right True --- > +-- > -- > > app <- toDynamic (const :: ANY -> ANY1 -> ANY) `dynApply` toDynamic (id :: ANY -> ANY) ; f <- fromDynamic app ; return $ (f :: Int -> Bool -> Bool) 0 True -- > Right True --- > --- > > do app <- toDynamic ((\f -> f . f) :: (ANY -> ANY) -> ANY -> ANY) `dynApply` toDynamic (even :: Int -> Bool) ; f <- fromDynamic app ; return (f :: ()) +-- > +-- > > do app <- toDynamic ((\f -> f . f) :: (ANY -> ANY) -> ANY -> ANY) `dynApply` toDynamic (even :: Int -> Bool) ; f <- fromDynamic app ; return (f :: ()) -- > Left "Cannot unify Int and Bool" -- -- [Using toDynamic] @@ -48,30 +48,30 @@ -- When using polymorphic values you need to give an explicit type annotation: -- -- > > toDynamic id --- > +-- > -- > :46:1: -- > Ambiguous type variable `a0' in the constraint: -- > (Typeable a0) arising from a use of `toDynamic' -- > Probable fix: add a type signature that fixes these type variable(s) -- > In the expression: toDynamic id -- > In an equation for `it': it = toDynamic id --- +-- -- versus -- -- > > toDynamic (id :: ANY -> ANY) -- > < ANY>> --- +-- -- Note that these type annotation are checked by ghc like any other: -- -- > > toDynamic (id :: ANY -> ANY1) --- > +-- > -- > :45:12: -- > Couldn't match expected type `V1' with actual type `V0' -- > Expected type: ANY -> ANY1 -- > Actual type: ANY -> ANY -- > In the first argument of `toDynamic', namely `(id :: ANY -> ANY1)' -- > In the expression: toDynamic (id :: ANY -> ANY1) -module Data.Rank1Dynamic +module Data.Rank1Dynamic ( Dynamic , toDynamic , fromDynamic @@ -81,7 +81,7 @@ module Data.Rank1Dynamic ) where import qualified GHC.Prim as GHC (Any) -import Data.Rank1Typeable +import Data.Rank1Typeable ( Typeable , TypeRep , typeOf @@ -95,16 +95,16 @@ import Unsafe.Coerce (unsafeCoerce) data Dynamic = Dynamic TypeRep GHC.Any instance Show Dynamic where - showsPrec _ (Dynamic t _) = showString "<<" . shows t . showString ">>" + showsPrec _ (Dynamic t _) = showString "<<" . shows t . showString ">>" --- | Introduce a dynamic value +-- | Introduce a dynamic value toDynamic :: Typeable a => a -> Dynamic toDynamic x = Dynamic (typeOf x) (unsafeCoerce x) -- | Eliminate a dynamic value fromDynamic :: Typeable a => Dynamic -> Either TypeError a fromDynamic (Dynamic t v) = - case unsafeCoerce v of + case unsafeCoerce v of r -> case typeOf r `isInstanceOf` t of Left err -> Left err Right () -> Right r diff --git a/src/Data/Rank1Typeable.hs b/src/Data/Rank1Typeable.hs index 75a7c8ab..9e2ce077 100644 --- a/src/Data/Rank1Typeable.hs +++ b/src/Data/Rank1Typeable.hs @@ -1,7 +1,7 @@ -- | Runtime type representation of terms with support for rank-1 polymorphic -- types with type variables of kind *. -- --- The essence of this module is that we use the standard 'Typeable' +-- The essence of this module is that we use the standard 'Typeable' -- representation of "Data.Typeable" but we introduce a special (empty) data -- type 'TypVar' which represents type variables. 'TypVar' is indexed by an -- arbitrary other data type, giving you an unbounded number of type variables; @@ -12,23 +12,23 @@ -- > -- We CANNOT use a term of type 'Int -> Bool' as 'Int -> Int' -- > > typeOf (undefined :: Int -> Int) `isInstanceOf` typeOf (undefined :: Int -> Bool) -- > Left "Cannot unify Int and Bool" --- > +-- > -- > -- We CAN use a term of type 'forall a. a -> Int' as 'Int -> Int' -- > > typeOf (undefined :: Int -> Int) `isInstanceOf` typeOf (undefined :: ANY -> Int) -- > Right () --- > +-- > -- > -- We CAN use a term of type 'forall a b. a -> b' as 'forall a. a -> a' -- > > typeOf (undefined :: ANY -> ANY) `isInstanceOf` typeOf (undefined :: ANY -> ANY1) -- > Right () --- > +-- > -- > -- We CANNOT use a term of type 'forall a. a -> a' as 'forall a b. a -> b' -- > > typeOf (undefined :: ANY -> ANY1) `isInstanceOf` typeOf (undefined :: ANY -> ANY) -- > Left "Cannot unify Succ and Zero" --- > +-- > -- > -- We CAN use a term of type 'forall a. a' as 'forall a. a -> a' -- > > typeOf (undefined :: ANY -> ANY) `isInstanceOf` typeOf (undefined :: ANY) -- > Right () --- > +-- > -- > -- We CANNOT use a term of type 'forall a. a -> a' as 'forall a. a' -- > > typeOf (undefined :: ANY) `isInstanceOf` typeOf (undefined :: ANY -> ANY) -- > Left "Cannot unify Skolem and ->" @@ -37,14 +37,14 @@ -- -- [Examples of funResultTy] -- --- > -- Apply fn of type (forall a. a -> a) to arg of type Bool gives Bool +-- > -- Apply fn of type (forall a. a -> a) to arg of type Bool gives Bool -- > > funResultTy (typeOf (undefined :: ANY -> ANY)) (typeOf (undefined :: Bool)) -- > Right Bool --- > +-- > -- > -- Apply fn of type (forall a b. a -> b -> a) to arg of type Bool gives forall a. a -> Bool -- > > funResultTy (typeOf (undefined :: ANY -> ANY1 -> ANY)) (typeOf (undefined :: Bool)) -- > Right (ANY -> Bool) -- forall a. a -> Bool --- > +-- > -- > -- Apply fn of type (forall a. (Bool -> a) -> a) to argument of type (forall a. a -> a) gives Bool -- > > funResultTy (typeOf (undefined :: (Bool -> ANY) -> ANY)) (typeOf (undefined :: ANY -> ANY)) -- > Right Bool @@ -56,14 +56,14 @@ -- > -- Cannot apply function of type (forall a. (a -> a) -> a -> a) to arg of type (Int -> Bool) -- > > funResultTy (typeOf (undefined :: (ANY -> ANY) -> (ANY -> ANY))) (typeOf (undefined :: Int -> Bool)) -- > Left "Cannot unify Int and Bool" -module Data.Rank1Typeable - ( -- * Basic types +module Data.Rank1Typeable + ( -- * Basic types TypeRep , typeOf , splitTyConApp , mkTyConApp , underlyingTypeRep - -- * Operations on type representations + -- * Operations on type representations , isInstanceOf , funResultTy , TypeError @@ -105,7 +105,7 @@ import Data.Typeable (Typeable, mkTyCon3) import Data.Typeable.Internal (listTc, funTc, TyCon(TyCon), tyConName) import Data.Binary (Binary(get, put)) import GHC.Fingerprint.Type (Fingerprint(..)) -import qualified Data.Typeable as Typeable +import qualified Data.Typeable as Typeable ( TypeRep , typeOf , splitTyConApp @@ -117,15 +117,15 @@ import qualified Data.Typeable as Typeable -------------------------------------------------------------------------------- -- | Dynamic type representation with support for rank-1 types -newtype TypeRep = TypeRep { +newtype TypeRep = TypeRep { -- | Return the underlying standard ("Data.Typeable") type representation - underlyingTypeRep :: Typeable.TypeRep + underlyingTypeRep :: Typeable.TypeRep } -- | Compare two type representations -- -- For base >= 4.6 this compares fingerprints, but older versions of base --- have a bug in the fingerprint construction +-- have a bug in the fingerprint construction -- () instance Eq TypeRep where #if ! MIN_VERSION_base(4,6,0) @@ -138,7 +138,7 @@ instance Eq TypeRep where -- Binary instance for 'TypeRep', avoiding orphan instances instance Binary TypeRep where put (splitTyConApp -> (TyCon (Fingerprint hi lo) package modul name, ts)) = do - put hi + put hi put lo put package put modul @@ -164,16 +164,16 @@ typeOf = TypeRep . Typeable.typeOf -- | Split a type representation into the application of -- a type constructor and its argument splitTyConApp :: TypeRep -> (TyCon, [TypeRep]) -splitTyConApp t = +splitTyConApp t = let (c, ts) = Typeable.splitTyConApp (underlyingTypeRep t) in (c, map TypeRep ts) -- | Inverse of 'splitTyConApp' mkTyConApp :: TyCon -> [TypeRep] -> TypeRep -mkTyConApp c ts +mkTyConApp c ts = TypeRep (Typeable.mkTyConApp c (map underlyingTypeRep ts)) -isTypVar :: TypeRep -> Maybe Var +isTypVar :: TypeRep -> Maybe Var isTypVar (splitTyConApp -> (c, [t])) | c == typVar = Just t isTypVar _ = Nothing @@ -190,7 +190,7 @@ skolem = let (c, _) = splitTyConApp (typeOf (undefined :: Skolem V0)) in c -- Type variables -- -------------------------------------------------------------------------------- -data TypVar a deriving Typeable +data TypVar a deriving Typeable data Skolem a deriving Typeable data Zero deriving Typeable data Succ a deriving Typeable @@ -206,7 +206,7 @@ type V7 = Succ V6 type V8 = Succ V7 type V9 = Succ V8 -type ANY = TypVar V0 +type ANY = TypVar V0 type ANY1 = TypVar V1 type ANY2 = TypVar V2 type ANY3 = TypVar V3 @@ -224,17 +224,17 @@ type ANY9 = TypVar V9 -- | If 'isInstanceOf' fails it returns a type error type TypeError = String --- | @t1 `isInstanceOf` t2@ checks if @t1@ is an instance of @t2@ -isInstanceOf :: TypeRep -> TypeRep -> Either TypeError () +-- | @t1 `isInstanceOf` t2@ checks if @t1@ is an instance of @t2@ +isInstanceOf :: TypeRep -> TypeRep -> Either TypeError () isInstanceOf t1 t2 = void (unify (skolemize t1) t2) -- | @funResultTy t1 t2@ is the type of the result when applying a function -- of type @t1@ to an argument of type @t2@ -funResultTy :: TypeRep -> TypeRep -> Either TypeError TypeRep -funResultTy (splitTyConApp -> (fc, [farg, fres])) x | fc == funTc = do +funResultTy :: TypeRep -> TypeRep -> Either TypeError TypeRep +funResultTy (splitTyConApp -> (fc, [farg, fres])) x | fc == funTc = do s <- unify (alphaRename "f" farg) (alphaRename "x" x) return (normalize (subst s (alphaRename "f" fres))) -funResultTy f _ = +funResultTy f _ = Left $ show f ++ " is not a function" -------------------------------------------------------------------------------- @@ -251,7 +251,7 @@ tvars :: TypeRep -> [Var] tvars (isTypVar -> Just x) = [x] tvars (splitTyConApp -> (_, ts)) = concatMap tvars ts -normalize :: TypeRep -> TypeRep +normalize :: TypeRep -> TypeRep normalize t = subst (zip (tvars t) anys) t where anys :: [TypeRep] @@ -261,7 +261,7 @@ normalize t = subst (zip (tvars t) anys) t succ = mkTyConApp succTyCon . (:[]) zero :: TypeRep - zero = mkTyConApp zeroTyCon [] + zero = mkTyConApp zeroTyCon [] mkTyCon :: String -> TyCon mkTyCon = mkTyCon3 "rank1typeable" "Data.Rank1Typeable" @@ -281,38 +281,38 @@ type Equation = (TypeRep, TypeRep) type Var = TypeRep skolemize :: TypeRep -> TypeRep -skolemize (isTypVar -> Just x) = mkTyConApp skolem [x] +skolemize (isTypVar -> Just x) = mkTyConApp skolem [x] skolemize (splitTyConApp -> (c, ts)) = mkTyConApp c (map skolemize ts) occurs :: Var -> TypeRep -> Bool -occurs x (isTypVar -> Just x') = x == x' +occurs x (isTypVar -> Just x') = x == x' occurs x (splitTyConApp -> (_, ts)) = any (occurs x) ts subst :: Substitution -> TypeRep -> TypeRep subst s (isTypVar -> Just x) = fromMaybe (mkTypVar x) (lookup x s) subst s (splitTyConApp -> (c, ts)) = mkTyConApp c (map (subst s) ts) -unify :: TypeRep - -> TypeRep - -> Either TypeError Substitution +unify :: TypeRep + -> TypeRep + -> Either TypeError Substitution unify = \t1 t2 -> go [] [(t1, t2)] where - go :: Substitution - -> [Equation] - -> Either TypeError Substitution - go acc [] = + go :: Substitution + -> [Equation] + -> Either TypeError Substitution + go acc [] = return acc go acc ((t1, t2) : eqs) | t1 == t2 = -- Note: equality check is fast go acc eqs go acc ((isTypVar -> Just x, t) : eqs) = - if x `occurs` t + if x `occurs` t then Left "Occurs check" - else go ((x, t) : map (second $ subst [(x, t)]) acc) + else go ((x, t) : map (second $ subst [(x, t)]) acc) (map (subst [(x, t)] *** subst [(x, t)]) eqs) go acc ((t, isTypVar -> Just x) : eqs) = go acc ((mkTypVar x, t) : eqs) - go acc ((splitTyConApp -> (c1, ts1), splitTyConApp -> (c2, ts2)) : eqs) = - if c1 /= c2 + go acc ((splitTyConApp -> (c1, ts1), splitTyConApp -> (c2, ts2)) : eqs) = + if c1 /= c2 then Left $ "Cannot unify " ++ show c1 ++ " and " ++ show c2 else go acc (zip ts1 ts2 ++ eqs) @@ -324,14 +324,14 @@ instance Show TypeRep where showsPrec p (splitTyConApp -> (tycon, tys)) = case tys of [] -> showsPrec p tycon - [anyIdx -> Just i] | tycon == typVar -> showString "ANY" . showIdx i - [x] | tycon == listTc -> + [anyIdx -> Just i] | tycon == typVar -> showString "ANY" . showIdx i + [x] | tycon == listTc -> showChar '[' . shows x . showChar ']' - [a,r] | tycon == funTc -> - showParen (p > 8) $ showsPrec 9 a - . showString " -> " + [a,r] | tycon == funTc -> + showParen (p > 8) $ showsPrec 9 a + . showString " -> " . showsPrec 8 r - xs | isTupleTyCon tycon -> + xs | isTupleTyCon tycon -> showTuple xs _ -> showParen (p > 9) $ showsPrec p tycon @@ -344,16 +344,16 @@ instance Show TypeRep where showArgs :: Show a => [a] -> ShowS showArgs [] = id showArgs [a] = showsPrec 10 a -showArgs (a:as) = showsPrec 10 a . showString " " . showArgs as +showArgs (a:as) = showsPrec 10 a . showString " " . showArgs as -anyIdx :: TypeRep -> Maybe Int +anyIdx :: TypeRep -> Maybe Int anyIdx (splitTyConApp -> (c, [])) | c == zeroTyCon = Just 0 anyIdx (splitTyConApp -> (c, [t])) | c == succTyCon = (+1) <$> anyIdx t anyIdx _ = Nothing showTuple :: [TypeRep] -> ShowS showTuple args = showChar '(' - . foldr (.) id ( intersperse (showChar ',') + . foldr (.) id ( intersperse (showChar ',') $ map (showsPrec 10) args ) . showChar ')' From 0a0889a719d4b2a050937508e75007159c3748c1 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0310/2357] Remove trailing whitespace --- src/Network/Transport/Tests.hs | 226 +++++++++++------------ src/Network/Transport/Tests/Auxiliary.hs | 28 +-- src/Network/Transport/Tests/Multicast.hs | 30 +-- src/Network/Transport/Tests/Traced.hs | 62 +++---- 4 files changed, 173 insertions(+), 173 deletions(-) diff --git a/src/Network/Transport/Tests.hs b/src/Network/Transport/Tests.hs index f90ffb12..6c5e6d20 100644 --- a/src/Network/Transport/Tests.hs +++ b/src/Network/Transport/Tests.hs @@ -1,7 +1,7 @@ {-# LANGUAGE RebindableSyntax #-} module Network.Transport.Tests where -import Prelude hiding +import Prelude hiding ( (>>=) , return , fail @@ -16,7 +16,7 @@ import Control.Exception (evaluate, throw, throwIO, bracket) import Control.Monad (replicateM, replicateM_, when, guard, forM_, unless) import Control.Monad.Error () import Control.Applicative ((<$>)) -import Network.Transport +import Network.Transport import Network.Transport.Internal (tlog, tryIO, timeoutMaybe) import Network.Transport.Util (spawn) import System.Random (randomIO) @@ -34,22 +34,22 @@ echoServer :: EndPoint -> IO () echoServer endpoint = do go Map.empty where - go :: Map ConnectionId Connection -> IO () + go :: Map ConnectionId Connection -> IO () go cs = do event <- receive endpoint case event of ConnectionOpened cid rel addr -> do tlog $ "Opened new connection " ++ show cid Right conn <- connect endpoint addr rel defaultConnectHints - go (Map.insert cid conn cs) + go (Map.insert cid conn cs) Received cid payload -> do - send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload + send (Map.findWithDefault (error $ "Received: Invalid cid " ++ show cid) cid cs) payload go cs - ConnectionClosed cid -> do + ConnectionClosed cid -> do tlog $ "Close connection " ++ show cid close (Map.findWithDefault (error $ "ConnectionClosed: Invalid cid " ++ show cid) cid cs) - go (Map.delete cid cs) - ReceivedMulticast _ _ -> + go (Map.delete cid cs) + ReceivedMulticast _ _ -> -- Ignore go cs ErrorEvent _ -> @@ -81,49 +81,49 @@ ping endpoint server numPings msg = do -- Wait for the server to close its connection to us tlog "Wait for ConnectionClosed message" - ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' + ConnectionClosed cid' <- receive endpoint ; True <- return $ cid == cid' -- Done tlog "Ping client done" - + -- | Basic ping test -testPingPong :: Transport -> Int -> IO () +testPingPong :: Transport -> Int -> IO () testPingPong transport numPings = do tlog "Starting ping pong test" server <- spawn transport echoServer result <- newEmptyMVar - -- Client + -- Client forkTry $ do tlog "Ping client" Right endpoint <- newEndPoint transport ping endpoint server numPings "ping" - putMVar result () - + putMVar result () + takeMVar result -- | Test that endpoints don't get confused -testEndPoints :: Transport -> Int -> IO () +testEndPoints :: Transport -> Int -> IO () testEndPoints transport numPings = do server <- spawn transport echoServer dones <- replicateM 2 newEmptyMVar - forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do + forM_ (zip dones ['A'..]) $ \(done, name) -> forkTry $ do let name' :: ByteString name' = pack [name] Right endpoint <- newEndPoint transport tlog $ "Ping client " ++ show name' ++ ": " ++ show (address endpoint) - ping endpoint server numPings name' - putMVar done () + ping endpoint server numPings name' + putMVar done () forM_ dones takeMVar -- Test that connections don't get confused -testConnections :: Transport -> Int -> IO () +testConnections :: Transport -> Int -> IO () testConnections transport numPings = do server <- spawn transport echoServer result <- newEmptyMVar - + -- Client forkTry $ do Right endpoint <- newEndPoint transport @@ -131,7 +131,7 @@ testConnections transport numPings = do -- Open two connections to the server Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv1 _ _ <- receive endpoint - + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv2 _ _ <- receive endpoint @@ -141,17 +141,17 @@ testConnections transport numPings = do -- One thread to send "pingB" on the second connection forkTry $ replicateM_ numPings $ send conn2 ["pingB"] - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do event <- receive endpoint case event of Received cid [payload] -> do when (cid == serv1 && payload /= "pingA") $ error "Wrong message" when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n + verifyResponse (n - 1) + _ -> + verifyResponse n verifyResponse (2 * numPings) takeMVar result @@ -161,7 +161,7 @@ testCloseOneConnection :: Transport -> Int -> IO () testCloseOneConnection transport numPings = do server <- spawn transport echoServer result <- newEmptyMVar - + -- Client forkTry $ do Right endpoint <- newEndPoint transport @@ -169,7 +169,7 @@ testCloseOneConnection transport numPings = do -- Open two connections to the server Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv1 _ _ <- receive endpoint - + Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints ConnectionOpened serv2 _ _ <- receive endpoint @@ -177,21 +177,21 @@ testCloseOneConnection transport numPings = do forkTry $ do replicateM_ numPings $ send conn1 ["pingA"] close conn1 - + -- One thread to send "pingB" on the second connection forkTry $ replicateM_ (numPings * 2) $ send conn2 ["pingB"] - -- Verify server responses - let verifyResponse 0 = putMVar result () - verifyResponse n = do + -- Verify server responses + let verifyResponse 0 = putMVar result () + verifyResponse n = do event <- receive endpoint case event of Received cid [payload] -> do when (cid == serv1 && payload /= "pingA") $ error "Wrong message" when (cid == serv2 && payload /= "pingB") $ error "Wrong message" - verifyResponse (n - 1) - _ -> - verifyResponse n + verifyResponse (n - 1) + _ -> + verifyResponse n verifyResponse (3 * numPings) takeMVar result @@ -208,7 +208,7 @@ testCloseOneDirection transport numPings = do -- A forkTry $ do - tlog "A" + tlog "A" Right endpoint <- newEndPoint transport tlog (show (address endpoint)) putMVar addrA (address endpoint) @@ -218,25 +218,25 @@ testCloseOneDirection transport numPings = do Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints -- Wait for B to connect to us - tlog "Wait for B" + tlog "Wait for B" ConnectionOpened cid _ _ <- receive endpoint -- Send pings to B tlog "Send pings to B" - replicateM_ numPings $ send conn ["ping"] + replicateM_ numPings $ send conn ["ping"] -- Close our connection to B tlog "Close connection" close conn - + -- Wait for B's pongs - tlog "Wait for pongs from B" + tlog "Wait for pongs from B" replicateM_ numPings $ do Received _ _ <- receive endpoint ; return () -- Wait for B to close it's connection to us tlog "Wait for B to close connection" ConnectionClosed cid' <- receive endpoint - guard (cid == cid') + guard (cid == cid') -- Done tlog "Done" @@ -264,12 +264,12 @@ testCloseOneDirection transport numPings = do -- Wait for A to close it's connection to us tlog "Wait for A to close connection" ConnectionClosed cid' <- receive endpoint - guard (cid == cid') + guard (cid == cid') -- Send pongs to A tlog "Send pongs to A" replicateM_ numPings $ send conn ["pong"] - + -- Close our connection to A tlog "Close connection to A" close conn @@ -285,10 +285,10 @@ collect :: EndPoint -> Maybe Int -> Maybe Int -> IO [(ConnectionId, [[ByteString collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty where -- TODO: for more serious use of this function we'd need to make these arguments strict - go (Just 0) open closed = finish open closed + go (Just 0) open closed = finish open closed go n open closed = do - mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint - case mEvent of + mEvent <- tryIO . timeoutMaybe timeout (userError "timeout") $ receive endPoint + case mEvent of Left _ -> finish open closed Right event -> do let n' = (\x -> x - 1) <$> n @@ -307,8 +307,8 @@ collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty EndPointClosed -> fail "Unexpected endpoint closure" - finish open closed = - if Map.null open + finish open closed = + if Map.null open then return . Map.toList . Map.map reverse $ closed else fail $ "Open connections: " ++ show (map fst . Map.toList $ open) @@ -319,12 +319,12 @@ collect endPoint maxEvents timeout = go maxEvents Map.empty Map.empty -- before receiving the messages on the second connection. What might (and sometimes -- does) happen is that finishes sending all of its messages on the first connection -- (in the TCP transport, the first socket pair) while B is behind on reading _from_ --- this connection (socket pair) -- the messages are "in transit" on the network +-- this connection (socket pair) -- the messages are "in transit" on the network -- (these tests are done on localhost, so there are in some OS buffer). Then when -- A opens the second connection (socket pair) B will spawn a new thread for this -- connection, and hence might start interleaving messages from the first and second --- connection. --- +-- connection. +-- -- This is correct behaviour, however: the transport API guarantees reliability and -- ordering _per connection_, but not _across_ connections. testCloseReopen :: Transport -> Int -> IO () @@ -332,7 +332,7 @@ testCloseReopen transport numPings = do addrB <- newEmptyMVar doneB <- newEmptyMVar - let numRepeats = 2 :: Int + let numRepeats = 2 :: Int -- A forkTry $ do @@ -342,7 +342,7 @@ testCloseReopen transport numPings = do tlog "A connecting" -- Connect to B Right conn <- readMVar addrB >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints - + tlog "A pinging" -- Say hi forM_ [1 .. numPings] $ \j -> send conn [pack $ "ping" ++ show i ++ "/" ++ show j] @@ -372,12 +372,12 @@ testCloseReopen transport numPings = do testParallelConnects :: Transport -> Int -> IO () testParallelConnects transport numPings = do server <- spawn transport echoServer - done <- newEmptyMVar + done <- newEmptyMVar Right endpoint <- newEndPoint transport -- Spawn lots of clients - forM_ [1 .. numPings] $ \i -> forkTry $ do + forM_ [1 .. numPings] $ \i -> forkTry $ do Right conn <- connect endpoint server ReliableOrdered defaultConnectHints send conn [pack $ "ping" ++ show i] send conn [pack $ "ping" ++ show i] @@ -386,7 +386,7 @@ testParallelConnects transport numPings = do forkTry $ do eventss <- collect endpoint (Just (numPings * 4)) Nothing -- Check that no pings got sent to the wrong connection - forM_ eventss $ \(_, [[ping1], [ping2]]) -> + forM_ eventss $ \(_, [[ping1], [ping2]]) -> guard (ping1 == ping2) putMVar done () @@ -405,13 +405,13 @@ testSendAfterClose transport numRepeats = do replicateM numRepeats $ do Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - + -- Close the second, but leave the first open; then output on the second -- connection (i.e., on a closed connection while there is still another -- connection open) close conn2 Left (TransportError SendClosed _) <- send conn2 ["ping2"] - + -- Now close the first connection, and output on it (i.e., output while -- there are no lightweight connection at all anymore) close conn1 @@ -425,7 +425,7 @@ testSendAfterClose transport numRepeats = do -- | Test that closing the same connection twice has no effect testCloseTwice :: Transport -> Int -> IO () -testCloseTwice transport numRepeats = do +testCloseTwice transport numRepeats = do server <- spawn transport echoServer clientDone <- newEmptyMVar @@ -436,11 +436,11 @@ testCloseTwice transport numRepeats = do -- We request two lightweight connections Right conn1 <- connect endpoint server ReliableOrdered defaultConnectHints Right conn2 <- connect endpoint server ReliableOrdered defaultConnectHints - + -- Close the second one twice close conn2 close conn2 - + -- Then send a message on the first and close that twice too send conn1 ["ping"] close conn1 @@ -449,11 +449,11 @@ testCloseTwice transport numRepeats = do ConnectionOpened cid1 _ _ <- receive endpoint ConnectionOpened cid2 _ _ <- receive endpoint ConnectionClosed cid2' <- receive endpoint ; True <- return $ cid2' == cid2 - Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 + Received cid1' ["ping"] <- receive endpoint ; True <- return $ cid1' == cid1 ConnectionClosed cid1'' <- receive endpoint ; True <- return $ cid1'' == cid1 - + return () - + putMVar clientDone () takeMVar clientDone @@ -471,7 +471,7 @@ testConnectToSelf transport numPings = do -- One thread to write to the endpoint forkTry $ do - tlog $ "writing" + tlog $ "writing" tlog $ "Sending ping" replicateM_ numPings $ send conn ["ping"] @@ -513,17 +513,17 @@ testConnectToSelfTwice transport numPings = do -- One thread to write to the endpoint using the first connection forkTry $ do - tlog $ "writing" + tlog $ "writing" tlog $ "Sending ping" replicateM_ numPings $ send conn1 ["pingA"] tlog $ "Closing connection" close conn1 - + -- One thread to write to the endpoint using the second connection forkTry $ do - tlog $ "writing" + tlog $ "writing" tlog $ "Sending ping" replicateM_ numPings $ send conn2 ["pingB"] @@ -554,11 +554,11 @@ testCloseSelf newTransport = do Right conn1 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints Right conn2 <- connect endpoint1 (address endpoint1) ReliableOrdered defaultConnectHints Right conn3 <- connect endpoint2 (address endpoint2) ReliableOrdered defaultConnectHints - + -- Close the conneciton and try to send close conn1 Left (TransportError SendClosed _) <- send conn1 ["ping"] - + -- Close the first endpoint. We should not be able to use the first -- connection anymore, or open more self connections, but the self connection -- to the second endpoint should still be fine @@ -574,7 +574,7 @@ testCloseSelf newTransport = do return () --- | Test various aspects of 'closeEndPoint' +-- | Test various aspects of 'closeEndPoint' testCloseEndPoint :: Transport -> Int -> IO () testCloseEndPoint transport _ = do serverDone <- newEmptyMVar @@ -598,7 +598,7 @@ testCloseEndPoint transport _ = do -- Second test do theirAddr <- readMVar clientAddr2 - + ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr Received cid' ["ping"] <- receive endpoint ; True <- return $ cid == cid' @@ -609,7 +609,7 @@ testCloseEndPoint transport _ = do ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint ; True <- return $ addr' == theirAddr Left (TransportError SendFailed _) <- send conn ["pong2"] - + return () putMVar serverDone () @@ -621,7 +621,7 @@ testCloseEndPoint transport _ = do -- First test: close endpoint with one outgoing but no incoming connections do Right endpoint <- newEndPoint transport - putMVar clientAddr1 (address endpoint) + putMVar clientAddr1 (address endpoint) -- Connect to the server, then close the endpoint without disconnecting explicitly Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -632,7 +632,7 @@ testCloseEndPoint transport _ = do -- Second test: close endpoint with one outgoing and one incoming connection do Right endpoint <- newEndPoint transport - putMVar clientAddr2 (address endpoint) + putMVar clientAddr2 (address endpoint) Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints send conn ["ping"] @@ -641,7 +641,7 @@ testCloseEndPoint transport _ = do ConnectionOpened cid ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr Received cid' ["pong"] <- receive endpoint ; True <- return $ cid == cid' - -- Close the endpoint + -- Close the endpoint closeEndPoint endpoint EndPointClosed <- receive endpoint @@ -677,13 +677,13 @@ testCloseTransport newTransport = do Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) - -- Client sets up first endpoint + -- Client sets up first endpoint theirAddr1 <- readMVar clientAddr1 ConnectionOpened cid1 ReliableOrdered addr <- receive endpoint ; True <- return $ addr == theirAddr1 - -- Client sets up second endpoint + -- Client sets up second endpoint theirAddr2 <- readMVar clientAddr2 - + ConnectionOpened cid2 ReliableOrdered addr' <- receive endpoint ; True <- return $ addr' == theirAddr2 Received cid2' ["ping"] <- receive endpoint ; True <- return $ cid2' == cid2 @@ -702,7 +702,7 @@ testCloseTransport newTransport = do -- An attempt to send to the endpoint should now fail Left (TransportError SendFailed _) <- send conn ["pong2"] - + putMVar serverDone () -- Client @@ -712,14 +712,14 @@ testCloseTransport newTransport = do -- Set up endpoint with one outgoing but no incoming connections Right endpoint1 <- newEndPoint transport - putMVar clientAddr1 (address endpoint1) + putMVar clientAddr1 (address endpoint1) -- Connect to the server, then close the endpoint without disconnecting explicitly Right _ <- connect endpoint1 theirAddr ReliableOrdered defaultConnectHints -- Set up an endpoint with one outgoing and out incoming connection Right endpoint2 <- newEndPoint transport - putMVar clientAddr2 (address endpoint2) + putMVar clientAddr2 (address endpoint2) Right conn <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints send conn ["ping"] @@ -746,7 +746,7 @@ testCloseTransport newTransport = do Left (TransportError ConnectFailed _) <- connect endpoint2 theirAddr ReliableOrdered defaultConnectHints -- And finally, so should an attempt to create a new endpoint - Left (TransportError NewEndPointFailed _) <- newEndPoint transport + Left (TransportError NewEndPointFailed _) <- newEndPoint transport putMVar clientDone () @@ -758,7 +758,7 @@ testConnectClosedEndPoint transport = do serverAddr <- newEmptyMVar serverClosed <- newEmptyMVar clientDone <- newEmptyMVar - + -- Server forkTry $ do Right endpoint <- newEndPoint transport @@ -770,12 +770,12 @@ testConnectClosedEndPoint transport = do -- Client forkTry $ do Right endpoint <- newEndPoint transport - readMVar serverClosed + readMVar serverClosed Left (TransportError ConnectNotFound _) <- readMVar serverAddr >>= \addr -> connect endpoint addr ReliableOrdered defaultConnectHints putMVar clientDone () - + takeMVar clientDone -- | We should receive an exception when doing a 'receive' after we have been @@ -783,7 +783,7 @@ testConnectClosedEndPoint transport = do testExceptionOnReceive :: IO (Either String Transport) -> IO () testExceptionOnReceive newTransport = do Right transport <- newTransport - + -- Test one: when we close an endpoint specifically Right endpoint1 <- newEndPoint transport closeEndPoint endpoint1 @@ -804,7 +804,7 @@ testSendException newTransport = do Right transport <- newTransport Right endpoint1 <- newEndPoint transport Right endpoint2 <- newEndPoint transport - + -- Connect endpoint1 to endpoint2 Right conn <- connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints ConnectionOpened _ _ _ <- receive endpoint2 @@ -814,7 +814,7 @@ testSendException newTransport = do -- This will have been as a failure to send by endpoint1, which will -- therefore have closed the socket. In turn this will have caused endpoint2 - -- to report that the connection was lost + -- to report that the connection was lost ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint1 ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint2 @@ -831,27 +831,27 @@ testSendException newTransport = do -- | If threads get killed while executing a 'connect', 'send', or 'close', this -- should not affect other threads. --- +-- -- The intention of this test is to see what happens when a asynchronous -- exception happes _while executing a send_. This is exceedingly difficult to -- guarantee, however. Hence we run a large number of tests and insert random -- thread delays -- and even then it might not happen. Moreover, it will only --- happen when we run on multiple cores. +-- happen when we run on multiple cores. testKill :: IO (Either String Transport) -> Int -> IO () testKill newTransport numThreads = do Right transport1 <- newTransport Right transport2 <- newTransport Right endpoint1 <- newEndPoint transport1 Right endpoint2 <- newEndPoint transport2 - - threads <- replicateM numThreads . forkIO $ do - randomThreadDelay 100 + + threads <- replicateM numThreads . forkIO $ do + randomThreadDelay 100 bracket (connect endpoint1 (address endpoint2) ReliableOrdered defaultConnectHints) - -- Note that we should not insert a randomThreadDelay into the + -- Note that we should not insert a randomThreadDelay into the -- exception handler itself as this means that the exception handler -- could be interrupted and we might not close (\(Right conn) -> close conn) - (\(Right conn) -> do randomThreadDelay 100 + (\(Right conn) -> do randomThreadDelay 100 Right () <- send conn ["ping"] randomThreadDelay 100) @@ -861,26 +861,26 @@ testKill newTransport numThreads = do forkIO . forM_ threads $ \tid -> do shouldKill <- randomIO if shouldKill - then randomThreadDelay 600 >> killThread tid + then randomThreadDelay 600 >> killThread tid else modifyMVar_ numAlive (return . (+ 1)) -- Since it is impossible to predict when the kill exactly happens, we don't -- know how many connects were opened and how many pings were sent. But we -- should not have any open connections (if we do, collect will throw an -- error) and we should have at least the number of pings equal to the number - -- of threads we did *not* kill - eventss <- collect endpoint2 Nothing (Just 1000000) + -- of threads we did *not* kill + eventss <- collect endpoint2 Nothing (Just 1000000) let actualPings = sum . map (length . snd) $ eventss expectedPings <- takeMVar numAlive - unless (actualPings >= expectedPings) $ + unless (actualPings >= expectedPings) $ throwIO (userError "Missing pings") - + -- print (actualPings, expectedPings) -- | Set up conditions with a high likelyhood of "crossing" (for transports -- that multiplex lightweight connections across heavyweight connections) -testCrossing :: Transport -> Int -> IO () +testCrossing :: Transport -> Int -> IO () testCrossing transport numRepeats = do [aAddr, bAddr] <- replicateM 2 newEmptyMVar [aDone, bDone] <- replicateM 2 newEmptyMVar @@ -902,10 +902,10 @@ testCrossing transport numRepeats = do -- Because we are creating lots of connections, it's possible that -- connect times out (for instance, in the TCP transport, -- Network.Socket.connect may time out). We shouldn't regard this as an - -- error in the Transport, though. + -- error in the Transport, though. connectResult <- connect endpoint theirAddress ReliableOrdered hints case connectResult of - Right conn -> close conn + Right conn -> close conn Left (TransportError ConnectTimeout _) -> putMVar aTimeout () Left (TransportError ConnectFailed _) -> readMVar bTimeout Left err -> throwIO . userError $ "testCrossed: " ++ show err @@ -916,23 +916,23 @@ testCrossing transport numRepeats = do Right endpoint <- newEndPoint transport putMVar bAddr (address endpoint) theirAddress <- readMVar aAddr - + replicateM_ numRepeats $ do takeMVar bGo >> yield connectResult <- connect endpoint theirAddress ReliableOrdered hints case connectResult of - Right conn -> close conn - Left (TransportError ConnectTimeout _) -> putMVar bTimeout () + Right conn -> close conn + Left (TransportError ConnectTimeout _) -> putMVar bTimeout () Left (TransportError ConnectFailed _) -> readMVar aTimeout Left err -> throwIO . userError $ "testCrossed: " ++ show err putMVar bDone () - + -- Driver forM_ [1 .. numRepeats] $ \_i -> do -- putStrLn $ "Round " ++ show _i tryTakeMVar aTimeout tryTakeMVar bTimeout - b <- randomIO + b <- randomIO if b then do putMVar aGo () ; putMVar bGo () else do putMVar bGo () ; putMVar aGo () yield @@ -954,14 +954,14 @@ testTransport newTransport = do , ("SendAfterClose", testSendAfterClose transport 100) , ("Crossing", testCrossing transport 10) , ("CloseTwice", testCloseTwice transport 100) - , ("ConnectToSelf", testConnectToSelf transport numPings) + , ("ConnectToSelf", testConnectToSelf transport numPings) , ("ConnectToSelfTwice", testConnectToSelfTwice transport numPings) , ("CloseSelf", testCloseSelf newTransport) - , ("CloseEndPoint", testCloseEndPoint transport numPings) + , ("CloseEndPoint", testCloseEndPoint transport numPings) , ("CloseTransport", testCloseTransport newTransport) , ("ConnectClosedEndPoint", testConnectClosedEndPoint transport) , ("ExceptionOnReceive", testExceptionOnReceive newTransport) - , ("SendException", testSendException newTransport) + , ("SendException", testSendException newTransport) , ("Kill", testKill newTransport 1000) ] where diff --git a/src/Network/Transport/Tests/Auxiliary.hs b/src/Network/Transport/Tests/Auxiliary.hs index 792c5214..cb733f41 100644 --- a/src/Network/Transport/Tests/Auxiliary.hs +++ b/src/Network/Transport/Tests/Auxiliary.hs @@ -30,7 +30,7 @@ import Network.Transport import Network.Transport.Tests.Traced (Traceable(..), traceShow) -- | Like fork, but throw exceptions in the child thread to the parent -forkTry :: IO () -> IO ThreadId +forkTry :: IO () -> IO ThreadId forkTry p = do tid <- myThreadId forkIO $ catch p (\e -> throwTo tid (e :: SomeException)) @@ -40,17 +40,17 @@ trySome :: IO a -> IO (Either SomeException a) trySome = try -- | Run the given test, catching timeouts and exceptions -runTest :: String -> IO () -> IO Bool +runTest :: String -> IO () -> IO Bool runTest description test = do putStr $ "Running " ++ show description ++ ": " hFlush stdout done <- try . timeout 60000000 $ test -- 60 seconds case done of - Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" + Left err -> failed $ "(exception: " ++ show (err :: SomeException) ++ ")" Right Nothing -> failed $ "(timeout)" - Right (Just ()) -> ok + Right (Just ()) -> ok where - failed :: String -> IO Bool + failed :: String -> IO Bool failed err = do setSGR [SetColor Foreground Vivid Red] putStr "failed " @@ -58,43 +58,43 @@ runTest description test = do putStrLn err return False - ok :: IO Bool + ok :: IO Bool ok = do setSGR [SetColor Foreground Vivid Green] putStrLn "ok" setSGR [Reset] return True --- | Run a bunch of tests and throw an exception if any fails +-- | Run a bunch of tests and throw an exception if any fails runTests :: [(String, IO ())] -> IO () runTests tests = do success <- foldr (liftM2 (&&) . uncurry runTest) (return True) $ tests unless success $ fail "Some tests failed" --- | Random thread delay between 0 and the specified max +-- | Random thread delay between 0 and the specified max randomThreadDelay :: Int -> IO () randomThreadDelay maxDelay = do delay <- randomIO :: IO Int - threadDelay (delay `mod` maxDelay) + threadDelay (delay `mod` maxDelay) -------------------------------------------------------------------------------- --- traceShow instances -- +-- traceShow instances -- -------------------------------------------------------------------------------- instance Traceable EndPoint where trace = const Nothing instance Traceable Transport where - trace = const Nothing + trace = const Nothing instance Traceable Connection where - trace = const Nothing + trace = const Nothing instance Traceable Event where - trace = traceShow + trace = traceShow instance Show err => Traceable (TransportError err) where - trace = traceShow + trace = traceShow instance Traceable EndPointAddress where trace = traceShow . endPointAddressToByteString diff --git a/src/Network/Transport/Tests/Multicast.hs b/src/Network/Transport/Tests/Multicast.hs index f68a2cb6..cec26342 100644 --- a/src/Network/Transport/Tests/Multicast.hs +++ b/src/Network/Transport/Tests/Multicast.hs @@ -10,10 +10,10 @@ import Network.Transport.Tests.Auxiliary (runTests) -- | Node for the "No confusion" test noConfusionNode :: Transport -- ^ Transport - -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to + -> [MVar MulticastAddress] -- ^ my group : groups to subscribe to -> [MVar ()] -- ^ I'm ready : others ready -> Int -- ^ number of pings - -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') + -> [ByteString] -- ^ my message : messages from subscribed groups (same order as 'groups to subscribe to') -> MVar () -- ^ I'm done -> IO () noConfusionNode transport groups ready numPings msgs done = do @@ -25,8 +25,8 @@ noConfusionNode transport groups ready numPings msgs done = do putMVar (head groups) (multicastAddress myGroup) -- Subscribe to the given multicast groups - addrs <- mapM readMVar (tail groups) - forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr + addrs <- mapM readMVar (tail groups) + forM_ addrs $ \addr -> do Right group <- resolveMulticastGroup endpoint addr multicastSubscribe group -- Indicate that we're ready and wait for everybody else to be ready @@ -42,31 +42,31 @@ noConfusionNode transport groups ready numPings msgs done = do case event of ReceivedMulticast addr [msg] -> let mix = addr `elemIndex` addrs in - case mix of + case mix of Nothing -> error "Message from unexpected source" Just ix -> when (msgs !! (ix + 1) /= msg) $ error "Unexpected message" _ -> error "Unexpected event" -- Success - putMVar done () + putMVar done () -- | Test that distinct multicast groups are not confused -testNoConfusion :: Transport -> Int -> IO () +testNoConfusion :: Transport -> Int -> IO () testNoConfusion transport numPings = do [group1, group2, group3] <- replicateM 3 newEmptyMVar [readyA, readyB, readyC] <- replicateM 3 newEmptyMVar [doneA, doneB, doneC] <- replicateM 3 newEmptyMVar let [msgA, msgB, msgC] = ["A says hi", "B says hi", "C says hi"] - forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA - forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB - forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC - - mapM_ takeMVar [doneA, doneB, doneC] + forkIO $ noConfusionNode transport [group1, group1, group2] [readyA, readyB, readyC] numPings [msgA, msgA, msgB] doneA + forkIO $ noConfusionNode transport [group2, group1, group3] [readyB, readyC, readyA] numPings [msgB, msgA, msgC] doneB + forkIO $ noConfusionNode transport [group3, group2, group3] [readyC, readyA, readyB] numPings [msgC, msgB, msgC] doneC + + mapM_ takeMVar [doneA, doneB, doneC] -- | Test multicast -testMulticast :: Transport -> IO () -testMulticast transport = - runTests +testMulticast :: Transport -> IO () +testMulticast transport = + runTests [ ("NoConfusion", testNoConfusion transport 10000) ] diff --git a/src/Network/Transport/Tests/Traced.hs b/src/Network/Transport/Tests/Traced.hs index 69e2e4ed..c67f82f2 100644 --- a/src/Network/Transport/Tests/Traced.hs +++ b/src/Network/Transport/Tests/Traced.hs @@ -1,7 +1,7 @@ --- | Add tracing to the IO monad (see examples). --- +-- | Add tracing to the IO monad (see examples). +-- -- [Usage] --- +-- -- > {-# LANGUAGE RebindableSyntax #-} -- > import Prelude hiding (catch, (>>=), (>>), return, fail) -- > import Traced @@ -15,7 +15,7 @@ -- > Right y <- return (Left 2 :: Either Int Int) -- > return (x + y) -- --- outputs +-- outputs -- -- > Hello world -- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:187:3-9) @@ -31,7 +31,7 @@ -- > test2 = do -- > Left x <- return (Left 1 :: Either Int Int) -- > True <- return (x == 3) --- > return x +-- > return x -- -- The advantage of this idiom is that it gives you line number information when the guard fails: -- @@ -39,7 +39,7 @@ -- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:193:3-6) -- > Trace: -- > 0 Left 1 -module Network.Transport.Tests.Traced +module Network.Transport.Tests.Traced ( MonadS(..) , return , (>>=) @@ -51,7 +51,7 @@ module Network.Transport.Tests.Traced , traceShow ) where -import Prelude hiding +import Prelude hiding ( (>>=) , return , fail @@ -76,12 +76,12 @@ import Control.Concurrent.MVar (MVar) -- | Like 'Monad' but bind is only defined for 'Trace'able instances class MonadS m where - returnS :: a -> m a + returnS :: a -> m a bindS :: Traceable a => m a -> (a -> m b) -> m b failS :: String -> m a seqS :: m a -> m b -> m b --- | Redefinition of 'Prelude.>>=' +-- | Redefinition of 'Prelude.>>=' (>>=) :: (MonadS m, Traceable a) => m a -> (a -> m b) -> m b (>>=) = bindS @@ -95,7 +95,7 @@ return = returnS -- | Redefinition of 'Prelude.fail' fail :: MonadS m => String -> m a -fail = failS +fail = failS -------------------------------------------------------------------------------- -- Trace typeclass (for adding elements to a trace -- @@ -106,17 +106,17 @@ data Showable = forall a. Show a => Showable a instance Show Showable where show (Showable x) = show x -mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable -mapShowable f (Showable x) = f x +mapShowable :: (forall a. Show a => a -> Showable) -> Showable -> Showable +mapShowable f (Showable x) = f x traceShow :: Show a => a -> Maybe Showable -traceShow = Just . Showable +traceShow = Just . Showable class Traceable a where - trace :: a -> Maybe Showable + trace :: a -> Maybe Showable instance (Traceable a, Traceable b) => Traceable (Either a b) where - trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x + trace (Left x) = (mapShowable $ Showable . (Left :: forall c. c -> Either c ())) <$> trace x trace (Right y) = (mapShowable $ Showable . (Right :: forall c. c -> Either () c)) <$> trace y instance (Traceable a, Traceable b) => Traceable (a, b) where @@ -128,7 +128,7 @@ instance (Traceable a, Traceable b) => Traceable (a, b) where instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where trace (x, y, z) = case (trace x, trace y, trace z) of - (Nothing, Nothing, Nothing) -> Nothing + (Nothing, Nothing, Nothing) -> Nothing (Just t1, Nothing, Nothing) -> traceShow t1 (Nothing, Just t2, Nothing) -> traceShow t2 (Just t1, Just t2, Nothing) -> traceShow (t1, t2) @@ -139,40 +139,40 @@ instance (Traceable a, Traceable b, Traceable c) => Traceable (a, b, c) where instance Traceable a => Traceable (Maybe a) where trace Nothing = traceShow (Nothing :: Maybe ()) - trace (Just x) = mapShowable (Showable . Just) <$> trace x + trace (Just x) = mapShowable (Showable . Just) <$> trace x instance Traceable a => Traceable [a] where - trace = traceShow . catMaybes . map trace + trace = traceShow . catMaybes . map trace instance Traceable () where - trace = const Nothing + trace = const Nothing instance Traceable Int where - trace = traceShow + trace = traceShow instance Traceable Int32 where - trace = traceShow + trace = traceShow instance Traceable Int64 where - trace = traceShow + trace = traceShow instance Traceable Word32 where - trace = traceShow + trace = traceShow instance Traceable Word64 where - trace = traceShow + trace = traceShow instance Traceable Bool where - trace = const Nothing + trace = const Nothing instance Traceable ByteString where - trace = traceShow + trace = traceShow instance Traceable (MVar a) where - trace = const Nothing + trace = const Nothing instance Traceable [Char] where - trace = traceShow + trace = traceShow instance Traceable IOException where trace = traceShow @@ -186,7 +186,7 @@ data TracedException = TracedException [String] SomeException instance Exception TracedException --- | Add tracing to 'IO' (see examples) +-- | Add tracing to 'IO' (see examples) instance MonadS IO where returnS = Prelude.return bindS = \x f -> x Prelude.>>= \a -> catches (f a) (traceHandlers a) @@ -194,7 +194,7 @@ instance MonadS IO where seqS = (Prelude.>>) instance Show TracedException where - show (TracedException ts ex) = + show (TracedException ts ex) = show ex ++ "\nTrace:\n" ++ unlines (map (\(i, t) -> show i ++ "\t" ++ t) (zip ([0..] :: [Int]) (take 10 . reverse $ ts))) traceHandlers :: Traceable a => a -> [Handler b] @@ -204,7 +204,7 @@ traceHandlers a = case trace a of , Handler $ \ex -> throwIO $ TracedException [show t] (ex :: SomeException) ] --- | Definition of 'ifThenElse' for use with RebindableSyntax +-- | Definition of 'ifThenElse' for use with RebindableSyntax ifThenElse :: Bool -> a -> a -> a ifThenElse True x _ = x ifThenElse False _ y = y From 7185ee41deed48364110b517df2850d70ea8f82a Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0311/2357] Remove trailing whitespace --- benchmarks/JustPingCacheHeader.hs | 30 +- benchmarks/JustPingHaskell.hs | 18 +- benchmarks/JustPingOneRecv.hs | 28 +- benchmarks/JustPingThroughChan.hs | 30 +- benchmarks/JustPingThroughMVar.hs | 30 +- benchmarks/JustPingTransport.hs | 28 +- benchmarks/JustPingTwoSocketPairs.hs | 28 +- benchmarks/JustPingWithHeader.hs | 30 +- src/Network/Transport/TCP.hs | 736 +++++++++++------------ src/Network/Transport/TCP/Internal.hs | 44 +- src/Network/Transport/TCP/Mock/Socket.hs | 94 +-- tests/TestQC.hs | 240 ++++---- tests/TestTCP.hs | 196 +++--- 13 files changed, 766 insertions(+), 766 deletions(-) diff --git a/benchmarks/JustPingCacheHeader.hs b/benchmarks/JustPingCacheHeader.hs index e9c29e88..b57b5d3a 100644 --- a/benchmarks/JustPingCacheHeader.hs +++ b/benchmarks/JustPingCacheHeader.hs @@ -40,7 +40,7 @@ main = do -- Start the server forkIO $ do putStrLn "server: creating TCP connection" - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) Nothing (Just "8080") @@ -61,15 +61,15 @@ main = do forkIO $ do takeMVar serverReady let pings = read pingsStr - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") let serverAddr = head serverAddrs sock <- socket (addrFamily serverAddr) Stream defaultProtocol - + N.connect sock (addrAddress serverAddr) - + ping sock pings putMVar clientDone () @@ -79,20 +79,20 @@ main = do pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Int -> IO () +ping :: Socket -> Int -> IO () ping sock pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage + send sock pingMessage bs <- recv sock 8 after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) pong :: Socket -> IO () @@ -103,7 +103,7 @@ pong sock = do send sock bs pong sock --- | Wrapper around NBS.recv (for profiling) +-- | Wrapper around NBS.recv (for profiling) recv :: Socket -> Int -> IO ByteString recv sock i = do (header, payload) <- BS.splitAt 4 `fmap` NBS.recv sock (4 + i) @@ -112,23 +112,23 @@ recv sock i = do -- | Cached header header :: ByteString -header = pack "fake" +header = pack "fake" -- | Wrapper around NBS.send (for profiling) -send :: Socket -> ByteString -> IO () +send :: Socket -> ByteString -> IO () send sock bs = do NBS.sendMany sock [header, bs] -- | Encode length (manual for now) encodeLength :: Int32 -> IO ByteString -encodeLength i32 = +encodeLength i32 = BSI.create 4 $ \p -> pokeByteOff p 0 (htonl (fromIntegral i32)) -- | Decode length (manual for now) decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p 0 return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingHaskell.hs b/benchmarks/JustPingHaskell.hs index 8aaf157a..9f56b4d5 100644 --- a/benchmarks/JustPingHaskell.hs +++ b/benchmarks/JustPingHaskell.hs @@ -33,7 +33,7 @@ main = do -- Start the server forkIO $ do putStrLn "server: creating TCP connection" - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) Nothing (Just "8080") @@ -54,15 +54,15 @@ main = do forkIO $ do takeMVar serverReady let pings = read pingsStr - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") let serverAddr = head serverAddrs sock <- socket (addrFamily serverAddr) Stream defaultProtocol - + N.connect sock (addrAddress serverAddr) - + ping sock pings putMVar clientDone () @@ -72,20 +72,20 @@ main = do pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Int -> IO () +ping :: Socket -> Int -> IO () ping sock pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage + send sock pingMessage bs <- recv sock 8 after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) pong :: Socket -> IO () @@ -96,7 +96,7 @@ pong sock = do send sock bs pong sock --- | Wrapper around NBS.recv (for profiling) +-- | Wrapper around NBS.recv (for profiling) recv :: Socket -> Int -> IO ByteString recv = NBS.recv diff --git a/benchmarks/JustPingOneRecv.hs b/benchmarks/JustPingOneRecv.hs index aae81cf4..accc287d 100644 --- a/benchmarks/JustPingOneRecv.hs +++ b/benchmarks/JustPingOneRecv.hs @@ -40,7 +40,7 @@ main = do -- Start the server forkIO $ do putStrLn "server: creating TCP connection" - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) Nothing (Just "8080") @@ -61,15 +61,15 @@ main = do forkIO $ do takeMVar serverReady let pings = read pingsStr - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") let serverAddr = head serverAddrs sock <- socket (addrFamily serverAddr) Stream defaultProtocol - + N.connect sock (addrAddress serverAddr) - + ping sock pings putMVar clientDone () @@ -79,20 +79,20 @@ main = do pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Int -> IO () +ping :: Socket -> Int -> IO () ping sock pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage + send sock pingMessage bs <- recv sock 8 after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) pong :: Socket -> IO () @@ -103,7 +103,7 @@ pong sock = do send sock bs pong sock --- | Wrapper around NBS.recv (for profiling) +-- | Wrapper around NBS.recv (for profiling) recv :: Socket -> Int -> IO ByteString recv sock i = do (header, payload) <- BS.splitAt 4 `fmap` NBS.recv sock (4 + i) @@ -111,21 +111,21 @@ recv sock i = do return payload -- | Wrapper around NBS.send (for profiling) -send :: Socket -> ByteString -> IO () +send :: Socket -> ByteString -> IO () send sock bs = do length <- encodeLength (fromIntegral (BS.length bs)) NBS.sendMany sock [length, bs] -- | Encode length (manual for now) encodeLength :: Int32 -> IO ByteString -encodeLength i32 = +encodeLength i32 = BSI.create 4 $ \p -> pokeByteOff p 0 (htonl (fromIntegral i32)) -- | Decode length (manual for now) decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p 0 return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingThroughChan.hs b/benchmarks/JustPingThroughChan.hs index ad4538aa..044d5380 100644 --- a/benchmarks/JustPingThroughChan.hs +++ b/benchmarks/JustPingThroughChan.hs @@ -56,9 +56,9 @@ main = do putMVar serverReady () (clientSock, pingAddr) <- accept sock forkIO $ socketToChan clientSock multiplexChannel - - -- Reply to the client - forever $ readChan multiplexChannel >>= send clientSock + + -- Reply to the client + forever $ readChan multiplexChannel >>= send clientSock -- Start the client forkIO $ do @@ -83,45 +83,45 @@ socketToChan sock chan = go pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Int -> IO () +ping :: Socket -> Int -> IO () ping sock pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage - bs <- recv sock + send sock pingMessage + bs <- recv sock after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) --- | Receive a package +-- | Receive a package recv :: Socket -> IO ByteString recv sock = do header <- NBS.recv sock 4 - length <- decodeLength header + length <- decodeLength header NBS.recv sock (fromIntegral (length :: Int32)) -- | Send a package -send :: Socket -> ByteString -> IO () +send :: Socket -> ByteString -> IO () send sock bs = do length <- encodeLength (fromIntegral (BS.length bs)) NBS.sendMany sock [length, bs] -- | Encode length (manual for now) encodeLength :: Int32 -> IO ByteString -encodeLength i32 = +encodeLength i32 = BSI.create 4 $ \p -> pokeByteOff p 0 (htonl (fromIntegral i32)) -- | Decode length (manual for now) decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p 0 return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingThroughMVar.hs b/benchmarks/JustPingThroughMVar.hs index 5b9e75c1..c1caffe2 100644 --- a/benchmarks/JustPingThroughMVar.hs +++ b/benchmarks/JustPingThroughMVar.hs @@ -56,9 +56,9 @@ main = do putMVar serverReady () (clientSock, pingAddr) <- accept sock forkIO $ socketToMVar clientSock multiplexMVar - - -- Reply to the client - forever $ takeMVar multiplexMVar >>= send clientSock + + -- Reply to the client + forever $ takeMVar multiplexMVar >>= send clientSock -- Start the client forkIO $ do @@ -83,45 +83,45 @@ socketToMVar sock mvar = go pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Int -> IO () +ping :: Socket -> Int -> IO () ping sock pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage - bs <- recv sock + send sock pingMessage + bs <- recv sock after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) --- | Receive a package +-- | Receive a package recv :: Socket -> IO ByteString recv sock = do header <- NBS.recv sock 4 - length <- decodeLength header + length <- decodeLength header NBS.recv sock (fromIntegral (length :: Int32)) -- | Send a package -send :: Socket -> ByteString -> IO () +send :: Socket -> ByteString -> IO () send sock bs = do length <- encodeLength (fromIntegral (BS.length bs)) NBS.sendMany sock [length, bs] -- | Encode length (manual for now) encodeLength :: Int32 -> IO ByteString -encodeLength i32 = +encodeLength i32 = BSI.create 4 $ \p -> pokeByteOff p 0 (htonl (fromIntegral i32)) -- | Decode length (manual for now) decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p 0 return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingTransport.hs b/benchmarks/JustPingTransport.hs index 29b745fe..bf088975 100644 --- a/benchmarks/JustPingTransport.hs +++ b/benchmarks/JustPingTransport.hs @@ -31,18 +31,18 @@ main = do forkIO $ do -- establish transport and endpoint putStrLn "server: creating TCP connection" - Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters - Right endpoint <- newEndPoint transport + Right transport <- createTransport "127.0.0.1" "8080" defaultTCPParameters + Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) - -- Connect to the client so that we can reply + -- Connect to the client so that we can reply theirAddr <- takeMVar clientAddr - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- reply to pings with pongs putStrLn "server: awaiting client connection" ConnectionOpened _ _ _ <- receive endpoint - pong endpoint conn + pong endpoint conn -- Start the client forkIO $ do @@ -55,9 +55,9 @@ main = do -- Connect to the server to send pings theirAddr <- takeMVar serverAddr - Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints + Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - -- Send pings, waiting for a reply after every ping + -- Send pings, waiting for a reply after every ping ConnectionOpened _ _ _ <- receive endpoint ping endpoint conn pings putMVar clientDone () @@ -68,27 +68,27 @@ main = do pingMessage :: [ByteString] pingMessage = [pack "ping123"] -ping :: EndPoint -> Connection -> Int -> IO () +ping :: EndPoint -> Connection -> Int -> IO () ping endpoint conn pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime send conn pingMessage - Received _ _payload <- receive endpoint + Received _ _payload <- receive endpoint after <- getCurrentTime - -- putStrLn $ "client received " ++ show _payload + -- putStrLn $ "client received " ++ show _payload let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) pong :: EndPoint -> Connection -> IO () pong endpoint conn = go - where + where go = do - msg <- receive endpoint + msg <- receive endpoint case msg of Received _ payload -> send conn payload >> go ConnectionClosed _ -> return () diff --git a/benchmarks/JustPingTwoSocketPairs.hs b/benchmarks/JustPingTwoSocketPairs.hs index 1eea7862..1850aef5 100644 --- a/benchmarks/JustPingTwoSocketPairs.hs +++ b/benchmarks/JustPingTwoSocketPairs.hs @@ -60,7 +60,7 @@ main = do pongSock <- socket (addrFamily clientAddr) Stream defaultProtocol N.connect pongSock (addrAddress clientAddr) when ("--NoDelay" `elem` args) $ setSocketOption pongSock NoDelay 1 - forever $ readChan multiplexChannel >>= send pongSock + forever $ readChan multiplexChannel >>= send pongSock -- Wait for incoming connections (pings from the client) putMVar serverReady () @@ -89,7 +89,7 @@ main = do putMVar clientDone () -- Wait for incoming connections (pongs from the server) - putMVar clientReady () + putMVar clientReady () (pongSock, pongAddr) <- accept sock socketToChan pongSock multiplexChannel @@ -107,45 +107,45 @@ socketToChan sock chan = go pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Chan ByteString -> Int -> IO () +ping :: Socket -> Chan ByteString -> Int -> IO () ping sock chan pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage - bs <- readChan chan + send sock pingMessage + bs <- readChan chan after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) --- | Receive a package +-- | Receive a package recv :: Socket -> IO ByteString recv sock = do header <- NBS.recv sock 4 - length <- decodeLength header + length <- decodeLength header NBS.recv sock (fromIntegral (length :: Int32)) -- | Send a package -send :: Socket -> ByteString -> IO () +send :: Socket -> ByteString -> IO () send sock bs = do length <- encodeLength (fromIntegral (BS.length bs)) NBS.sendMany sock [length, bs] -- | Encode length (manual for now) encodeLength :: Int32 -> IO ByteString -encodeLength i32 = +encodeLength i32 = BSI.create 4 $ \p -> pokeByteOff p 0 (htonl (fromIntegral i32)) -- | Decode length (manual for now) decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p 0 return (fromIntegral (ntohl w32)) diff --git a/benchmarks/JustPingWithHeader.hs b/benchmarks/JustPingWithHeader.hs index 37cae85a..b33b91b7 100644 --- a/benchmarks/JustPingWithHeader.hs +++ b/benchmarks/JustPingWithHeader.hs @@ -40,7 +40,7 @@ main = do -- Start the server forkIO $ do putStrLn "server: creating TCP connection" - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo (Just (defaultHints { addrFlags = [AI_PASSIVE] } )) Nothing (Just "8080") @@ -61,15 +61,15 @@ main = do forkIO $ do takeMVar serverReady let pings = read pingsStr - serverAddrs <- getAddrInfo + serverAddrs <- getAddrInfo Nothing (Just "127.0.0.1") (Just "8080") let serverAddr = head serverAddrs sock <- socket (addrFamily serverAddr) Stream defaultProtocol - + N.connect sock (addrAddress serverAddr) - + ping sock pings putMVar clientDone () @@ -79,20 +79,20 @@ main = do pingMessage :: ByteString pingMessage = pack "ping123" -ping :: Socket -> Int -> IO () +ping :: Socket -> Int -> IO () ping sock pings = go pings where go :: Int -> IO () - go 0 = do + go 0 = do putStrLn $ "client did " ++ show pings ++ " pings" go !i = do before <- getCurrentTime - send sock pingMessage + send sock pingMessage bs <- recv sock 8 after <- getCurrentTime -- putStrLn $ "client received " ++ unpack bs let latency = (1e6 :: Double) * realToFrac (diffUTCTime after before) - hPutStrLn stderr $ show i ++ " " ++ show latency + hPutStrLn stderr $ show i ++ " " ++ show latency go (i - 1) pong :: Socket -> IO () @@ -103,29 +103,29 @@ pong sock = do send sock bs pong sock --- | Wrapper around NBS.recv (for profiling) +-- | Wrapper around NBS.recv (for profiling) recv :: Socket -> Int -> IO ByteString recv sock _ = do header <- NBS.recv sock 4 - length <- decodeLength header + length <- decodeLength header NBS.recv sock (fromIntegral (length :: Int32)) -- | Wrapper around NBS.send (for profiling) -send :: Socket -> ByteString -> IO () +send :: Socket -> ByteString -> IO () send sock bs = do length <- encodeLength (fromIntegral (BS.length bs)) NBS.sendMany sock [length, bs] -- | Encode length (manual for now) encodeLength :: Int32 -> IO ByteString -encodeLength i32 = +encodeLength i32 = BSI.create 4 $ \p -> pokeByteOff p 0 (htonl (fromIntegral i32)) -- | Decode length (manual for now) decodeLength :: ByteString -> IO Int32 -decodeLength bs = - let (fp, _, _) = BSI.toForeignPtr bs in +decodeLength bs = + let (fp, _, _) = BSI.toForeignPtr bs in withForeignPtr fp $ \p -> do - w32 <- peekByteOff p 0 + w32 <- peekByteOff p 0 return (fromIntegral (ntohl w32)) diff --git a/src/Network/Transport/TCP.hs b/src/Network/Transport/TCP.hs index c3629611..d492fc5b 100644 --- a/src/Network/Transport/TCP.hs +++ b/src/Network/Transport/TCP.hs @@ -1,22 +1,22 @@ --- | TCP implementation of the transport layer. --- +-- | TCP implementation of the transport layer. +-- -- The TCP implementation guarantees that only a single TCP connection (socket) -- will be used between endpoints, provided that the addresses specified are -- canonical. If /A/ connects to /B/ and reports its address as -- @192.168.0.1:8080@ and /B/ subsequently connects tries to connect to /A/ as -- @client1.local:http-alt@ then the transport layer will not realize that the --- TCP connection can be reused. +-- TCP connection can be reused. -- -- Applications that use the TCP transport should use -- 'Network.Socket.withSocketsDo' in their main function for Windows -- compatibility (see "Network.Socket"). -module Network.Transport.TCP +module Network.Transport.TCP ( -- * Main API createTransport , TCPParameters(..) , defaultTCPParameters - -- * Internals (exposed for unit tests) - , createTransportExposeInternals + -- * Internals (exposed for unit tests) + , createTransportExposeInternals , TransportInternals(..) , EndPointId , encodeEndPointAddress @@ -25,27 +25,27 @@ module Network.Transport.TCP , ConnectionRequestResponse(..) , firstNonReservedLightweightConnectionId , firstNonReservedHeavyweightConnectionId - , socketToEndPoint + , socketToEndPoint , LightweightConnectionId -- * Design notes -- $design ) where -import Prelude hiding +import Prelude hiding ( mapM_ #if ! MIN_VERSION_base(4,6,0) , catch #endif ) - + import Network.Transport -import Network.Transport.TCP.Internal +import Network.Transport.TCP.Internal ( forkServer , recvWithLength , recvInt32 , tryCloseSocket ) -import Network.Transport.Internal +import Network.Transport.Internal ( encodeInt32 , prependLength , mapIOException @@ -59,7 +59,7 @@ import Network.Transport.Internal #ifdef USE_MOCK_NETWORK import qualified Network.Transport.TCP.Mock.Socket as N #else -import qualified Network.Socket as N +import qualified Network.Socket as N #endif ( HostName , ServiceName @@ -71,7 +71,7 @@ import qualified Network.Socket as N , SocketType(Stream) , defaultProtocol , setSocketOption - , SocketOption(ReuseAddr) + , SocketOption(ReuseAddr) , connect , sOMAXCONN , AddrInfo @@ -85,7 +85,7 @@ import Network.Socket.ByteString (sendMany) import Control.Concurrent (forkIO, ThreadId, killThread, myThreadId) import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) -import Control.Concurrent.MVar +import Control.Concurrent.MVar ( MVar , newMVar , modifyMVar @@ -98,7 +98,7 @@ import Control.Concurrent.MVar import Control.Category ((>>>)) import Control.Applicative ((<$>)) import Control.Monad (when, unless, join) -import Control.Exception +import Control.Exception ( IOException , SomeException , AsyncException @@ -117,7 +117,7 @@ import qualified Data.ByteString.Char8 as BSC (pack, unpack) import Data.Bits (shiftL, (.|.)) import Data.Word (Word32) import Data.Set (Set) -import qualified Data.Set as Set +import qualified Data.Set as Set ( empty , insert , elems @@ -128,11 +128,11 @@ import qualified Data.Set as Set ) import Data.Map (Map) import qualified Data.Map as Map (empty) -import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) +import Data.Accessor (Accessor, accessor, (^.), (^=), (^:)) import qualified Data.Accessor.Container as DAC (mapMaybe) import Data.Foldable (forM_, mapM_) --- $design +-- $design -- -- [Goals] -- @@ -146,7 +146,7 @@ import Data.Foldable (forM_, mapM_) -- > | \~~~|~~~~~~~~~~~~~~~~~~~~~~~~~<| | -- > | |==========================| | -- > +-------+ +-------+ --- +-- -- Ignoring the complications detailed below, the TCP connection is set up is -- when the first lightweight connection is created (in either direction), and -- torn down when the last lightweight connection (in either direction) is @@ -177,7 +177,7 @@ import Data.Foldable (forM_, mapM_) -- with /B/'s connection request completes. -- -- [Disconnecting] --- +-- -- The TCP connection is created as soon as the first logical connection from -- /A/ to /B/ (or /B/ to /A/) is established. At this point a thread (@#@) is -- spawned that listens for incoming connections from /B/: @@ -193,21 +193,21 @@ import Data.Foldable (forM_, mapM_) -- The question is when the TCP connection can be closed again. Conceptually, -- we want to do reference counting: when there are no logical connections left -- between /A/ and /B/ we want to close the socket (possibly after some --- timeout). +-- timeout). -- -- However, /A/ and /B/ need to agree that the refcount has reached zero. It -- might happen that /B/ sends a connection request over the existing socket at -- the same time that /A/ closes its logical connection to /B/ and closes the -- socket. This will cause a failure in /B/ (which will have to retry) which is -- not caused by a network failure, which is unfortunate. (Note that the --- connection request from /B/ might succeed even if /A/ closes the socket.) +-- connection request from /B/ might succeed even if /A/ closes the socket.) -- -- Instead, when /A/ is ready to close the socket it sends a 'CloseSocket' -- request to /B/ and records that its connection to /B/ is closing. If /A/ -- receives a new connection request from /B/ after having sent the -- 'CloseSocket' request it simply forgets that it sent a 'CloseSocket' request -- and increments the reference count of the connection again. --- +-- -- When /B/ receives a 'CloseSocket' message and it too is ready to close the -- connection, it will respond with a reciprocal 'CloseSocket' request to /A/ -- and then actually close the socket. /A/ meanwhile will not send any more @@ -216,11 +216,11 @@ import Data.Foldable (forM_, mapM_) -- from /B/. (Since /A/ recorded that its connection to /B/ is in closing state -- after sending a 'CloseSocket' request to /B/, it knows not to reciprocate /B/ -- reciprocal 'CloseSocket' message.) --- +-- -- If there is a concurrent thread in /A/ waiting to connect to /B/ after /A/ -- has sent a 'CloseSocket' request then this thread will block until /A/ knows -- whether to reuse the old socket (if /B/ sends a new connection request --- instead of acknowledging the 'CloseSocket') or to set up a new socket. +-- instead of acknowledging the 'CloseSocket') or to set up a new socket. -------------------------------------------------------------------------------- -- Internal datatypes -- @@ -229,30 +229,30 @@ import Data.Foldable (forM_, mapM_) -- We use underscores for fields that we might update (using accessors) -- -- All data types follow the same structure: --- +-- -- * A top-level data type describing static properties (TCPTransport, -- LocalEndPoint, RemoteEndPoint) -- * The 'static' properties include an MVar containing a data structure for -- the dynamic properties (TransportState, LocalEndPointState, --- RemoteEndPointState). The state could be invalid/valid/closed,/etc. +-- RemoteEndPointState). The state could be invalid/valid/closed,/etc. -- * For the case of "valid" we use third data structure to give more details -- about the state (ValidTransportState, ValidLocalEndPointState, -- ValidRemoteEndPointState). -data TCPTransport = TCPTransport +data TCPTransport = TCPTransport { transportHost :: !N.HostName , transportPort :: !N.ServiceName , transportState :: !(MVar TransportState) , transportParams :: !TCPParameters } -data TransportState = +data TransportState = TransportValid !ValidTransportState | TransportClosed -data ValidTransportState = ValidTransportState +data ValidTransportState = ValidTransportState { _localEndPoints :: !(Map EndPointAddress LocalEndPoint) - , _nextEndPointId :: !EndPointId + , _nextEndPointId :: !EndPointId } data LocalEndPoint = LocalEndPoint @@ -261,12 +261,12 @@ data LocalEndPoint = LocalEndPoint , localState :: !(MVar LocalEndPointState) } -data LocalEndPointState = +data LocalEndPointState = LocalEndPointValid !ValidLocalEndPointState | LocalEndPointClosed -data ValidLocalEndPointState = ValidLocalEndPointState - { -- Next available ID for an outgoing lightweight self-connection +data ValidLocalEndPointState = ValidLocalEndPointState + { -- Next available ID for an outgoing lightweight self-connection -- (see also remoteNextConnOutId) _localNextConnOutId :: !LightweightConnectionId -- Next available ID for an incoming heavyweight connection @@ -275,7 +275,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState , _localConnections :: !(Map EndPointAddress RemoteEndPoint) } --- REMOTE ENDPOINTS +-- REMOTE ENDPOINTS -- -- Remote endpoints (basically, TCP connections) have the following lifecycle: -- @@ -302,7 +302,7 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- createConnectionTo when we fail to connect. -- -- Valid: This is the "normal" state for a working remote endpoint. --- +-- -- Closing: When we detect that a remote endpoint is no longer used, we send a -- CloseSocket request across the connection and put the remote endpoint in -- closing state. As with Init, 'Closing' carries an MVar () 'resolved' which @@ -335,16 +335,16 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- localConnections it is guaranteed to either find a different remote -- endpoint, or else none at all (if we don't insist in this order some -- threads might start spinning). --- +-- -- INV-RESOLVE: We should only signal on 'resolved' while the remote endpoint is -- locked, and the remote endpoint must be in Valid or Closed state once -- unlocked. This guarantees that there will not be two threads attempting to --- both signal on 'resolved'. +-- both signal on 'resolved'. -- -- INV-LOST: If a send or recv fails, or a socket is closed unexpectedly, we -- first put the remote endpoint in Closed state, and then send a -- EventConnectionLost event. This guarantees that we only send this event --- once. +-- once. -- -- INV-CLOSING: An endpoint in closing state is for all intents and purposes -- closed; that is, we shouldn't do any 'send's on it (although 'recv' is @@ -357,23 +357,23 @@ data ValidLocalEndPointState = ValidLocalEndPointState -- particular, it is okay to call removeRemoteEndPoint inside -- modifyRemoteState. -data RemoteEndPoint = RemoteEndPoint +data RemoteEndPoint = RemoteEndPoint { remoteAddress :: !EndPointAddress , remoteState :: !(MVar RemoteState) , remoteId :: !HeavyweightConnectionId , remoteScheduled :: !(Chan (IO ())) } -data RequestedBy = RequestedByUs | RequestedByThem +data RequestedBy = RequestedByUs | RequestedByThem deriving (Eq, Show) data RemoteState = -- | Invalid remote endpoint (for example, invalid address) RemoteEndPointInvalid !(TransportError ConnectErrorCode) -- | The remote endpoint is being initialized - | RemoteEndPointInit !(MVar ()) !(MVar ()) !RequestedBy + | RemoteEndPointInit !(MVar ()) !(MVar ()) !RequestedBy -- | "Normal" working endpoint - | RemoteEndPointValid !ValidRemoteEndPointState + | RemoteEndPointValid !ValidRemoteEndPointState -- | The remote endpoint is being closed (garbage collected) | RemoteEndPointClosing !(MVar ()) !ValidRemoteEndPointState -- | The remote endpoint has been closed (garbage collected) @@ -382,12 +382,12 @@ data RemoteState = -- using a closeTransport or closeEndPoint API call | RemoteEndPointFailed !IOException --- TODO: we might want to replace Set (here and elsewhere) by faster +-- TODO: we might want to replace Set (here and elsewhere) by faster -- containers -- -- TODO: we could get rid of 'remoteIncoming' (and maintain less state) if --- we introduce a new event 'AllConnectionsClosed' -data ValidRemoteEndPointState = ValidRemoteEndPointState +-- we introduce a new event 'AllConnectionsClosed' +data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int , _remoteIncoming :: !(Set LightweightConnectionId) , _remoteMaxIncoming :: !LightweightConnectionId @@ -414,24 +414,24 @@ type LightweightConnectionId = Word32 -- 'LightweightConnectionId'. type HeavyweightConnectionId = Word32 --- | Control headers -data ControlHeader = +-- | Control headers +data ControlHeader = -- | Tell the remote endpoint that we created a new connection - CreatedNewConnection + CreatedNewConnection -- | Tell the remote endpoint we will no longer be using a connection - | CloseConnection + | CloseConnection -- | Request to close the connection (see module description) - | CloseSocket + | CloseSocket deriving (Enum, Bounded, Show) -- | Response sent by /B/ to /A/ when /A/ tries to connect data ConnectionRequestResponse = -- | /B/ accepts the connection - ConnectionRequestAccepted + ConnectionRequestAccepted -- | /A/ requested an invalid endpoint - | ConnectionRequestInvalid + | ConnectionRequestInvalid -- | /A/s request crossed with a request from /B/ (see protocols) - | ConnectionRequestCrossed + | ConnectionRequestCrossed deriving (Enum, Bounded, Show) -- | Parameters for setting up the TCP transport @@ -439,7 +439,7 @@ data TCPParameters = TCPParameters { -- | Backlog for 'listen'. -- Defaults to SOMAXCONN. tcpBacklog :: Int - -- | Should we set SO_REUSEADDR on the server socket? + -- | Should we set SO_REUSEADDR on the server socket? -- Defaults to True. , tcpReuseServerAddr :: Bool -- | Should we set SO_REUSEADDR on client sockets? @@ -448,13 +448,13 @@ data TCPParameters = TCPParameters { } -- | Internal functionality we expose for unit testing -data TransportInternals = TransportInternals +data TransportInternals = TransportInternals { -- | The ID of the thread that listens for new incoming connections transportThread :: ThreadId -- | Find the socket between a local and a remote endpoint - , socketBetween :: EndPointAddress - -> EndPointAddress - -> IO N.Socket + , socketBetween :: EndPointAddress + -> EndPointAddress + -> IO N.Socket } -------------------------------------------------------------------------------- @@ -462,62 +462,62 @@ data TransportInternals = TransportInternals -------------------------------------------------------------------------------- -- | Create a TCP transport -createTransport :: N.HostName - -> N.ServiceName +createTransport :: N.HostName + -> N.ServiceName -> TCPParameters -> IO (Either IOException Transport) -createTransport host port params = +createTransport host port params = either Left (Right . fst) <$> createTransportExposeInternals host port params -- | You should probably not use this function (used for unit testing only) -createTransportExposeInternals - :: N.HostName - -> N.ServiceName +createTransportExposeInternals + :: N.HostName + -> N.ServiceName -> TCPParameters - -> IO (Either IOException (Transport, TransportInternals)) -createTransportExposeInternals host port params = do - state <- newMVar . TransportValid $ ValidTransportState - { _localEndPoints = Map.empty - , _nextEndPointId = 0 + -> IO (Either IOException (Transport, TransportInternals)) +createTransportExposeInternals host port params = do + state <- newMVar . TransportValid $ ValidTransportState + { _localEndPoints = Map.empty + , _nextEndPointId = 0 } let transport = TCPTransport { transportState = state , transportHost = host , transportPort = port , transportParams = params } - tryIO $ bracketOnError (forkServer - host - port - (tcpBacklog params) + tryIO $ bracketOnError (forkServer + host + port + (tcpBacklog params) (tcpReuseServerAddr params) - (terminationHandler transport) + (terminationHandler transport) (handleConnectionRequest transport)) killThread (mkTransport transport) where - mkTransport :: TCPTransport - -> ThreadId + mkTransport :: TCPTransport + -> ThreadId -> IO (Transport, TransportInternals) mkTransport transport tid = return - ( Transport - { newEndPoint = apiNewEndPoint transport + ( Transport + { newEndPoint = apiNewEndPoint transport , closeTransport = let evs = [ EndPointClosed , throw $ userError "Transport closed" ] in - apiCloseTransport transport (Just tid) evs - } - , TransportInternals + apiCloseTransport transport (Just tid) evs + } + , TransportInternals { transportThread = tid , socketBetween = internalSocketBetween transport } ) terminationHandler :: TCPTransport -> SomeException -> IO () - terminationHandler transport ex = do + terminationHandler transport ex = do let evs = [ ErrorEvent (TransportError EventTransportFailed (show ex)) - , throw $ userError "Transport closed" + , throw $ userError "Transport closed" ] - apiCloseTransport transport Nothing evs + apiCloseTransport transport Nothing evs -- | Default TCP parameters defaultTCPParameters :: TCPParameters @@ -533,39 +533,39 @@ defaultTCPParameters = TCPParameters { -- | Close the transport apiCloseTransport :: TCPTransport -> Maybe ThreadId -> [Event] -> IO () -apiCloseTransport transport mTransportThread evs = +apiCloseTransport transport mTransportThread evs = asyncWhenCancelled return $ do mTSt <- modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> return (TransportClosed, Just vst) TransportClosed -> return (TransportClosed, Nothing) - forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) + forM_ mTSt $ mapM_ (apiCloseEndPoint transport evs) . (^. localEndPoints) -- This will invoke the termination handler, which in turn will call -- apiCloseTransport again, but then the transport will already be closed -- and we won't be passed a transport thread, so we terminate immmediate - forM_ mTransportThread killThread - --- | Create a new endpoint -apiNewEndPoint :: TCPTransport + forM_ mTransportThread killThread + +-- | Create a new endpoint +apiNewEndPoint :: TCPTransport -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) -apiNewEndPoint transport = +apiNewEndPoint transport = try . asyncWhenCancelled closeEndPoint $ do ourEndPoint <- createLocalEndPoint transport - return EndPoint + return EndPoint { receive = readChan (localChannel ourEndPoint) , address = localAddress ourEndPoint - , connect = apiConnect (transportParams transport) ourEndPoint + , connect = apiConnect (transportParams transport) ourEndPoint , closeEndPoint = let evs = [ EndPointClosed - , throw $ userError "Endpoint closed" + , throw $ userError "Endpoint closed" ] in apiCloseEndPoint transport evs ourEndPoint - , newMulticastGroup = return . Left $ newMulticastGroupError + , newMulticastGroup = return . Left $ newMulticastGroupError , resolveMulticastGroup = return . Left . const resolveMulticastGroupError } where - newMulticastGroupError = - TransportError NewMulticastGroupUnsupported "Multicast not supported" - resolveMulticastGroupError = - TransportError ResolveMulticastGroupUnsupported "Multicast not supported" + newMulticastGroupError = + TransportError NewMulticastGroupUnsupported "Multicast not supported" + resolveMulticastGroupError = + TransportError ResolveMulticastGroupUnsupported "Multicast not supported" -- | Connnect to an endpoint apiConnect :: TCPParameters -- ^ Parameters @@ -575,35 +575,35 @@ apiConnect :: TCPParameters -- ^ Parameters -> ConnectHints -- ^ Hints -> IO (Either (TransportError ConnectErrorCode) Connection) apiConnect params ourEndPoint theirAddress _reliability hints = - try . asyncWhenCancelled close $ - if localAddress ourEndPoint == theirAddress - then connectToSelf ourEndPoint + try . asyncWhenCancelled close $ + if localAddress ourEndPoint == theirAddress + then connectToSelf ourEndPoint else do resetIfBroken ourEndPoint theirAddress - (theirEndPoint, connId) <- + (theirEndPoint, connId) <- createConnectionTo params ourEndPoint theirAddress hints -- connAlive can be an IORef rather than an MVar because it is protected -- by the remoteState MVar. We don't need the overhead of locking twice. connAlive <- newIORef True - return Connection - { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive - , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive + return Connection + { send = apiSend (ourEndPoint, theirEndPoint) connId connAlive + , close = apiClose (ourEndPoint, theirEndPoint) connId connAlive } -- | Close a connection apiClose :: EndPointPair -> LightweightConnectionId -> IORef Bool -> IO () -apiClose (ourEndPoint, theirEndPoint) connId connAlive = - void . tryIO . asyncWhenCancelled return $ do +apiClose (ourEndPoint, theirEndPoint) connId connAlive = + void . tryIO . asyncWhenCancelled return $ do mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointValid vst -> do alive <- readIORef connAlive - if alive + if alive then do writeIORef connAlive False - act <- schedule theirEndPoint $ - sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] - return ( RemoteEndPointValid - . (remoteOutgoing ^: (\x -> x - 1)) + act <- schedule theirEndPoint $ + sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId] + return ( RemoteEndPointValid + . (remoteOutgoing ^: (\x -> x - 1)) $ vst , Just act ) @@ -616,12 +616,12 @@ apiClose (ourEndPoint, theirEndPoint) connId connAlive = -- | Send data across a connection -apiSend :: EndPointPair -- ^ Local and remote endpoint +apiSend :: EndPointPair -- ^ Local and remote endpoint -> LightweightConnectionId -- ^ Connection ID -> IORef Bool -- ^ Is the connection still alive? -> [ByteString] -- ^ Payload -> IO (Either (TransportError SendErrorCode) ()) -apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = +apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = -- We don't need the overhead of asyncWhenCancelled here try . mapIOException sendFailed $ do act <- withMVar (remoteState theirEndPoint) $ \st -> case st of @@ -631,70 +631,70 @@ apiSend (ourEndPoint, theirEndPoint) connId connAlive payload = relyViolation (ourEndPoint, theirEndPoint) "apiSend" RemoteEndPointValid vst -> do alive <- readIORef connAlive - if alive - then schedule theirEndPoint $ + if alive + then schedule theirEndPoint $ sendOn vst (encodeInt32 connId : prependLength payload) else throwIO $ TransportError SendClosed "Connection closed" - RemoteEndPointClosing _ _ -> do + RemoteEndPointClosing _ _ -> do alive <- readIORef connAlive - if alive - then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" else throwIO $ TransportError SendClosed "Connection closed" RemoteEndPointClosed -> do alive <- readIORef connAlive - if alive - then relyViolation (ourEndPoint, theirEndPoint) "apiSend" + if alive + then relyViolation (ourEndPoint, theirEndPoint) "apiSend" else throwIO $ TransportError SendClosed "Connection closed" RemoteEndPointFailed err -> do alive <- readIORef connAlive - if alive - then throwIO $ TransportError SendFailed (show err) + if alive + then throwIO $ TransportError SendFailed (show err) else throwIO $ TransportError SendClosed "Connection closed" runScheduledAction (ourEndPoint, theirEndPoint) act where sendFailed = TransportError SendFailed . show -- | Force-close the endpoint -apiCloseEndPoint :: TCPTransport -- ^ Transport - -> [Event] -- ^ Events used to report closure +apiCloseEndPoint :: TCPTransport -- ^ Transport + -> [Event] -- ^ Events used to report closure -> LocalEndPoint -- ^ Local endpoint -> IO () apiCloseEndPoint transport evs ourEndPoint = asyncWhenCancelled return $ do -- Remove the reference from the transport state removeLocalEndPoint transport ourEndPoint - -- Close the local endpoint + -- Close the local endpoint mOurState <- modifyMVar (localState ourEndPoint) $ \st -> case st of - LocalEndPointValid vst -> + LocalEndPointValid vst -> return (LocalEndPointClosed, Just vst) LocalEndPointClosed -> return (LocalEndPointClosed, Nothing) forM_ mOurState $ \vst -> do forM_ (vst ^. localConnections) tryCloseRemoteSocket - forM_ evs $ writeChan (localChannel ourEndPoint) + forM_ evs $ writeChan (localChannel ourEndPoint) where -- Close the remote socket and return the set of all incoming connections - tryCloseRemoteSocket :: RemoteEndPoint -> IO () + tryCloseRemoteSocket :: RemoteEndPoint -> IO () tryCloseRemoteSocket theirEndPoint = do - -- We make an attempt to close the connection nicely + -- We make an attempt to close the connection nicely -- (by sending a CloseSocket first) let closed = RemoteEndPointFailed . userError $ "apiCloseEndPoint" mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInvalid _ -> + RemoteEndPointInvalid _ -> return (st, Nothing) RemoteEndPointInit resolved _ _ -> do putMVar resolved () return (closed, Nothing) - RemoteEndPointValid vst -> do + RemoteEndPointValid vst -> do act <- schedule theirEndPoint $ do tryIO $ sendOn vst [ encodeInt32 CloseSocket , encodeInt32 (vst ^. remoteMaxIncoming) ] tryCloseSocket (remoteSocket vst) return (closed, Just act) - RemoteEndPointClosing resolved vst -> do + RemoteEndPointClosing resolved vst -> do putMVar resolved () act <- schedule theirEndPoint $ tryCloseSocket (remoteSocket vst) return (closed, Just act) @@ -703,7 +703,7 @@ apiCloseEndPoint transport evs ourEndPoint = RemoteEndPointFailed err -> return (RemoteEndPointFailed err, Nothing) forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) - + -------------------------------------------------------------------------------- -- Incoming requests -- @@ -720,11 +720,11 @@ apiCloseEndPoint transport evs ourEndPoint = -- the transport down. We must be careful to close the socket when a (possibly -- asynchronous, ThreadKilled) exception occurs. (If an exception escapes from -- handleConnectionRequest the transport will be shut down.) -handleConnectionRequest :: TCPTransport -> N.Socket -> IO () -handleConnectionRequest transport sock = handle handleException $ do +handleConnectionRequest :: TCPTransport -> N.Socket -> IO () +handleConnectionRequest transport sock = handle handleException $ do ourEndPointId <- recvInt32 sock - theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock - let ourAddress = encodeEndPointAddress (transportHost transport) + theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock + let ourAddress = encodeEndPointAddress (transportHost transport) (transportPort transport) ourEndPointId ourEndPoint <- withMVar (transportState transport) $ \st -> case st of @@ -735,32 +735,32 @@ handleConnectionRequest transport sock = handle handleException $ do throwIO $ userError "handleConnectionRequest: Invalid endpoint" Just ourEndPoint -> return ourEndPoint - TransportClosed -> + TransportClosed -> throwIO $ userError "Transport closed" void . forkIO $ go ourEndPoint theirAddress where go :: LocalEndPoint -> EndPointAddress -> IO () - go ourEndPoint theirAddress = do + go ourEndPoint theirAddress = do -- This runs in a thread that will never be killed - mEndPoint <- handle ((>> return Nothing) . handleException) $ do + mEndPoint <- handle ((>> return Nothing) . handleException) $ do resetIfBroken ourEndPoint theirAddress - (theirEndPoint, isNew) <- + (theirEndPoint, isNew) <- findRemoteEndPoint ourEndPoint theirAddress RequestedByThem - - if not isNew + + if not isNew then do tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed] tryCloseSocket sock - return Nothing + return Nothing else do sendLock <- newMVar () - let vst = ValidRemoteEndPointState + let vst = ValidRemoteEndPointState { remoteSocket = sock , remoteSendLock = sendLock , _remoteOutgoing = 0 , _remoteIncoming = Set.empty , _remoteMaxIncoming = 0 - , _remoteNextConnOutId = firstNonReservedLightweightConnectionId + , _remoteNextConnOutId = firstNonReservedLightweightConnectionId } sendMany sock [encodeInt32 ConnectionRequestAccepted] resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) @@ -769,30 +769,30 @@ handleConnectionRequest transport sock = handle handleException $ do -- Nothing then the socket is already closed; otherwise, the socket has -- been recorded as part of the remote endpoint. Either way, we no longer -- have to worry about closing the socket on receiving an asynchronous - -- exception from this point forward. + -- exception from this point forward. forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint handleException :: SomeException -> IO () - handleException ex = do - tryCloseSocket sock + handleException ex = do + tryCloseSocket sock rethrowIfAsync (fromException ex) - rethrowIfAsync :: Maybe AsyncException -> IO () - rethrowIfAsync = mapM_ throwIO + rethrowIfAsync :: Maybe AsyncException -> IO () + rethrowIfAsync = mapM_ throwIO -- | Handle requests from a remote endpoint. --- +-- -- Returns only if the remote party closes the socket or if an error occurs. -- This runs in a thread that will never be killed. -handleIncomingMessages :: EndPointPair -> IO () +handleIncomingMessages :: EndPointPair -> IO () handleIncomingMessages (ourEndPoint, theirEndPoint) = do mSock <- withMVar theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) - "handleIncomingMessages (invalid)" + relyViolation (ourEndPoint, theirEndPoint) + "handleIncomingMessages (invalid)" RemoteEndPointInit _ _ _ -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages (init)" RemoteEndPointValid ep -> return . Just $ remoteSocket ep @@ -800,13 +800,13 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do return . Just $ remoteSocket ep RemoteEndPointClosed -> return Nothing - RemoteEndPointFailed _ -> + RemoteEndPointFailed _ -> return Nothing - - forM_ mSock $ \sock -> + + forM_ mSock $ \sock -> tryIO (go sock) >>= either (prematureExit sock) return where - -- Dispatch + -- Dispatch -- -- If a recv throws an exception this will be caught top-level and -- 'prematureExit' will be invoked. The same will happen if the remote @@ -817,38 +817,38 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do go :: N.Socket -> IO () go sock = do lcid <- recvInt32 sock :: IO LightweightConnectionId - if lcid >= firstNonReservedLightweightConnectionId + if lcid >= firstNonReservedLightweightConnectionId then do - readMessage sock lcid + readMessage sock lcid go sock - else + else case tryToEnum (fromIntegral lcid) of Just CreatedNewConnection -> do - recvInt32 sock >>= createdNewConnection - go sock + recvInt32 sock >>= createdNewConnection + go sock Just CloseConnection -> do - recvInt32 sock >>= closeConnection + recvInt32 sock >>= closeConnection go sock - Just CloseSocket -> do - didClose <- recvInt32 sock >>= closeSocket sock + Just CloseSocket -> do + didClose <- recvInt32 sock >>= closeSocket sock unless didClose $ go sock Nothing -> throwIO $ userError "Invalid control request" - + -- Create a new connection - createdNewConnection :: LightweightConnectionId -> IO () + createdNewConnection :: LightweightConnectionId -> IO () createdNewConnection lcid = do modifyMVar_ theirState $ \st -> do vst <- case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection (invalid)" RemoteEndPointInit _ _ _ -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:createNewConnection (init)" RemoteEndPointValid vst -> return ( (remoteIncoming ^: Set.insert lcid) - $ (remoteMaxIncoming ^= lcid) + $ (remoteMaxIncoming ^= lcid) vst ) RemoteEndPointClosing resolved vst -> do @@ -863,36 +863,36 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do . (remoteMaxIncoming ^= lcid) $ vst ) - RemoteEndPointFailed err -> + RemoteEndPointFailed err -> throwIO err RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "createNewConnection (closed)" return (RemoteEndPointValid vst) - writeChan ourChannel (ConnectionOpened (connId lcid) ReliableOrdered theirAddr) + writeChan ourChannel (ConnectionOpened (connId lcid) ReliableOrdered theirAddr) - -- Close a connection + -- Close a connection -- It is important that we verify that the connection is in fact open, -- because otherwise we should not decrement the reference count - closeConnection :: LightweightConnectionId -> IO () + closeConnection :: LightweightConnectionId -> IO () closeConnection lcid = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (invalid)" RemoteEndPointInit _ _ _ -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (init)" - RemoteEndPointValid vst -> do - unless (Set.member lcid (vst ^. remoteIncoming)) $ + RemoteEndPointValid vst -> do + unless (Set.member lcid (vst ^. remoteIncoming)) $ throwIO $ userError "Invalid CloseConnection" - return ( RemoteEndPointValid - . (remoteIncoming ^: Set.delete lcid) + return ( RemoteEndPointValid + . (remoteIncoming ^: Set.delete lcid) $ vst ) RemoteEndPointClosing _ _ -> -- If the remote endpoint is in Closing state, that means that are as -- far as we are concerned there are no incoming connections. This -- means that a CloseConnection request at this point is invalid. - throwIO $ userError "Invalid CloseConnection request" + throwIO $ userError "Invalid CloseConnection request" RemoteEndPointFailed err -> throwIO err RemoteEndPointClosed -> @@ -900,7 +900,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do writeChan ourChannel (ConnectionClosed (connId lcid)) -- Close the socket (if we don't have any outgoing connections) - closeSocket :: N.Socket -> LightweightConnectionId -> IO Bool + closeSocket :: N.Socket -> LightweightConnectionId -> IO Bool closeSocket sock lastReceivedId = do mAct <- modifyMVar theirState $ \st -> do case st of @@ -908,38 +908,38 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (invalid)" RemoteEndPointInit _ _ _ -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (init)" RemoteEndPointValid vst -> do -- We regard a CloseSocket message as an (optimized) way for the -- remote endpoint to indicate that all its connections to us are -- now properly closed - forM_ (Set.elems $ vst ^. remoteIncoming) $ + forM_ (Set.elems $ vst ^. remoteIncoming) $ writeChan ourChannel . ConnectionClosed . connId - let vst' = remoteIncoming ^= Set.empty $ vst + let vst' = remoteIncoming ^= Set.empty $ vst -- If we still have outgoing connections then we ignore the -- CloseSocket request (we sent a ConnectionCreated message to the -- remote endpoint, but it did not receive it before sending the -- CloseSocket request). Similarly, if lastReceivedId < lastSentId -- then we sent a ConnectionCreated *AND* a ConnectionClosed -- message to the remote endpoint, *both of which* it did not yet - -- receive before sending the CloseSocket request. - if vst' ^. remoteOutgoing > 0 || lastReceivedId < lastSentId vst + -- receive before sending the CloseSocket request. + if vst' ^. remoteOutgoing > 0 || lastReceivedId < lastSentId vst then return (RemoteEndPointValid vst', Nothing) - else do + else do removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- Attempt to reply (but don't insist) act <- schedule theirEndPoint $ do tryIO $ sendOn vst' [ encodeInt32 CloseSocket , encodeInt32 (vst ^. remoteMaxIncoming) ] - tryCloseSocket sock + tryCloseSocket sock return (RemoteEndPointClosed, Just act) RemoteEndPointClosing resolved vst -> do -- Like above, we need to check if there is a ConnectionCreated - -- message that we sent but that the remote endpoint has not yet - -- received. However, since we are in 'closing' state, the only + -- message that we sent but that the remote endpoint has not yet + -- received. However, since we are in 'closing' state, the only -- way this may happen is when we sent a ConnectionCreated, -- ConnectionClosed, and CloseSocket message, none of which have -- yet been received. We leave the endpoint in closing state in @@ -949,29 +949,29 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do return (RemoteEndPointClosing resolved vst, Nothing) else do removeRemoteEndPoint (ourEndPoint, theirEndPoint) - act <- schedule theirEndPoint $ tryCloseSocket sock + act <- schedule theirEndPoint $ tryCloseSocket sock putMVar resolved () return (RemoteEndPointClosed, Just act) RemoteEndPointFailed err -> throwIO err RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:closeSocket (closed)" case mAct of Nothing -> return False Just act -> do runScheduledAction (ourEndPoint, theirEndPoint) act return True - + -- Read a message and output it on the endPoint's channel. By rights we -- should verify that the connection ID is valid, but this is unnecessary -- overhead - readMessage :: N.Socket -> LightweightConnectionId -> IO () - readMessage sock lcid = - recvWithLength sock >>= writeChan ourChannel . Received (connId lcid) + readMessage :: N.Socket -> LightweightConnectionId -> IO () + readMessage sock lcid = + recvWithLength sock >>= writeChan ourChannel . Received (connId lcid) -- Arguments - ourChannel = localChannel ourEndPoint + ourChannel = localChannel ourEndPoint theirState = remoteState theirEndPoint theirAddr = remoteAddress theirEndPoint @@ -982,20 +982,20 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointInit _ _ _ -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointValid _ -> do - let code = EventConnectionLost (remoteAddress theirEndPoint) + let code = EventConnectionLost (remoteAddress theirEndPoint) writeChan ourChannel . ErrorEvent $ TransportError code (show err) return (RemoteEndPointFailed err) RemoteEndPointClosing resolved _ -> do putMVar resolved () return (RemoteEndPointFailed err) RemoteEndPointClosed -> - relyViolation (ourEndPoint, theirEndPoint) + relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit" RemoteEndPointFailed err' -> return (RemoteEndPointFailed err') @@ -1006,7 +1006,7 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- The ID of the last connection _we_ created (or 0 for none) lastSentId :: ValidRemoteEndPointState -> LightweightConnectionId - lastSentId vst = + lastSentId vst = if vst ^. remoteNextConnOutId == firstNonReservedLightweightConnectionId then 0 else (vst ^. remoteNextConnOutId) - 1 @@ -1021,12 +1021,12 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do -- | Create a connection to a remote endpoint -- -- If the remote endpoint is in 'RemoteEndPointClosing' state then we will --- block until that is resolved. +-- block until that is resolved. -- -- May throw a TransportError ConnectErrorCode exception. -createConnectionTo :: TCPParameters - -> LocalEndPoint - -> EndPointAddress +createConnectionTo :: TCPParameters + -> LocalEndPoint + -> EndPointAddress -> ConnectHints -> IO (RemoteEndPoint, LightweightConnectionId) createConnectionTo params ourEndPoint theirAddress hints = go @@ -1035,27 +1035,27 @@ createConnectionTo params ourEndPoint theirAddress hints = go (theirEndPoint, isNew) <- mapIOException connectFailed $ findRemoteEndPoint ourEndPoint theirAddress RequestedByUs - if isNew + if isNew then do - forkIO . handle absorbAllExceptions $ - setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints + forkIO . handle absorbAllExceptions $ + setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints go else do -- 'findRemoteEndPoint' will have increased 'remoteOutgoing' mapIOException connectFailed $ do act <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointValid vst -> do - let connId = vst ^. remoteNextConnOutId + RemoteEndPointValid vst -> do + let connId = vst ^. remoteNextConnOutId act <- schedule theirEndPoint $ do sendOn vst [encodeInt32 CreatedNewConnection, encodeInt32 connId] return connId - return ( RemoteEndPointValid - $ remoteNextConnOutId ^= connId + 1 + return ( RemoteEndPointValid + $ remoteNextConnOutId ^= connId + 1 $ vst - , act + , act ) -- Error cases - RemoteEndPointInvalid err -> + RemoteEndPointInvalid err -> throwIO err RemoteEndPointFailed err -> throwIO err @@ -1065,32 +1065,32 @@ createConnectionTo params ourEndPoint theirAddress hints = go -- TODO: deal with exception case? connId <- runScheduledAction (ourEndPoint, theirEndPoint) act return (theirEndPoint, connId) - + connectFailed :: IOException -> TransportError ConnectErrorCode connectFailed = TransportError ConnectFailed . show absorbAllExceptions :: SomeException -> IO () - absorbAllExceptions _ex = + absorbAllExceptions _ex = return () -- | Set up a remote endpoint -setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () +setupRemoteEndPoint :: TCPParameters -> EndPointPair -> ConnectHints -> IO () setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do - result <- socketToEndPoint ourAddress - theirAddress + result <- socketToEndPoint ourAddress + theirAddress (tcpReuseClientAddr params) (connectTimeout hints) didAccept <- case result of - Right (sock, ConnectionRequestAccepted) -> do - sendLock <- newMVar () - let vst = ValidRemoteEndPointState + Right (sock, ConnectionRequestAccepted) -> do + sendLock <- newMVar () + let vst = ValidRemoteEndPointState { remoteSocket = sock , remoteSendLock = sendLock - , _remoteOutgoing = 0 + , _remoteOutgoing = 0 , _remoteIncoming = Set.empty , _remoteMaxIncoming = 0 - , _remoteNextConnOutId = firstNonReservedLightweightConnectionId + , _remoteNextConnOutId = firstNonReservedLightweightConnectionId } resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst) return True @@ -1101,7 +1101,7 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do return False Right (sock, ConnectionRequestCrossed) -> do withMVar (remoteState theirEndPoint) $ \st -> case st of - RemoteEndPointInit _ crossed _ -> + RemoteEndPointInit _ crossed _ -> putMVar crossed () RemoteEndPointFailed ex -> throwIO ex @@ -1109,11 +1109,11 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do relyViolation (ourEndPoint, theirEndPoint) "setupRemoteEndPoint: Crossed" tryCloseSocket sock return False - Left err -> do + Left err -> do resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err) return False - when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) + when didAccept $ handleIncomingMessages (ourEndPoint, theirEndPoint) where ourAddress = localAddress ourEndPoint theirAddress = remoteAddress theirEndPoint @@ -1121,23 +1121,23 @@ setupRemoteEndPoint params (ourEndPoint, theirEndPoint) hints = do -- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO () -closeIfUnused (ourEndPoint, theirEndPoint) = do +closeIfUnused (ourEndPoint, theirEndPoint) = do mAct <- modifyMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointValid vst -> - if vst ^. remoteOutgoing == 0 && Set.null (vst ^. remoteIncoming) - then do + if vst ^. remoteOutgoing == 0 && Set.null (vst ^. remoteIncoming) + then do resolved <- newEmptyMVar - act <- schedule theirEndPoint $ + act <- schedule theirEndPoint $ sendOn vst [ encodeInt32 CloseSocket , encodeInt32 (vst ^. remoteMaxIncoming) ] return (RemoteEndPointClosing resolved vst, Just act) - else + else return (RemoteEndPointValid vst, Nothing) _ -> return (st, Nothing) - forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) - + forM_ mAct $ runScheduledAction (ourEndPoint, theirEndPoint) + -- | Reset a remote endpoint if it is in Invalid mode -- -- If the remote endpoint is currently in broken state, and @@ -1156,7 +1156,7 @@ resetIfBroken ourEndPoint theirAddress = do return (vst ^. localConnectionTo theirAddress) LocalEndPointClosed -> throwIO $ TransportError ConnectFailed "Endpoint closed" - forM_ mTheirEndPoint $ \theirEndPoint -> + forM_ mTheirEndPoint $ \theirEndPoint -> withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInvalid _ -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) @@ -1166,26 +1166,26 @@ resetIfBroken ourEndPoint theirAddress = do return () -- | Special case of 'apiConnect': connect an endpoint to itself --- +-- -- May throw a TransportError ConnectErrorCode (if the local endpoint is closed) -connectToSelf :: LocalEndPoint +connectToSelf :: LocalEndPoint -> IO Connection -connectToSelf ourEndPoint = do +connectToSelf ourEndPoint = do connAlive <- newIORef True -- Protected by the local endpoint lock - lconnId <- mapIOException connectFailed $ getLocalNextConnOutId ourEndPoint + lconnId <- mapIOException connectFailed $ getLocalNextConnOutId ourEndPoint let connId = createConnectionId heavyweightSelfConnectionId lconnId writeChan ourChan $ ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) - return Connection - { send = selfSend connAlive connId + return Connection + { send = selfSend connAlive connId , close = selfClose connAlive connId } where - selfSend :: IORef Bool - -> ConnectionId - -> [ByteString] + selfSend :: IORef Bool + -> ConnectionId + -> [ByteString] -> IO (Either (TransportError SendErrorCode) ()) - selfSend connAlive connId msg = + selfSend connAlive connId msg = try . withMVar ourState $ \st -> case st of LocalEndPointValid _ -> do alive <- readIORef connAlive @@ -1196,19 +1196,19 @@ connectToSelf ourEndPoint = do throwIO $ TransportError SendFailed "Endpoint closed" selfClose :: IORef Bool -> ConnectionId -> IO () - selfClose connAlive connId = + selfClose connAlive connId = withMVar ourState $ \st -> case st of LocalEndPointValid _ -> do alive <- readIORef connAlive when alive $ do - writeChan ourChan (ConnectionClosed connId) + writeChan ourChan (ConnectionClosed connId) writeIORef connAlive False LocalEndPointClosed -> - return () + return () ourChan = localChannel ourEndPoint ourState = localState ourEndPoint - connectFailed = TransportError ConnectFailed . show + connectFailed = TransportError ConnectFailed . show -- | Resolve an endpoint currently in 'Init' state resolveInit :: EndPointPair -> RemoteState -> IO () @@ -1216,57 +1216,57 @@ resolveInit (ourEndPoint, theirEndPoint) newState = modifyMVar_ (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInit resolved _ _ -> do putMVar resolved () - case newState of - RemoteEndPointClosed -> + case newState of + RemoteEndPointClosed -> removeRemoteEndPoint (ourEndPoint, theirEndPoint) _ -> return () return newState - RemoteEndPointFailed ex -> + RemoteEndPointFailed ex -> throwIO ex _ -> relyViolation (ourEndPoint, theirEndPoint) "resolveInit" -- | Get the next outgoing self-connection ID --- +-- -- Throws an IO exception when the endpoint is closed. getLocalNextConnOutId :: LocalEndPoint -> IO LightweightConnectionId -getLocalNextConnOutId ourEndpoint = +getLocalNextConnOutId ourEndpoint = modifyMVar (localState ourEndpoint) $ \st -> case st of LocalEndPointValid vst -> do - let connId = vst ^. localNextConnOutId - return ( LocalEndPointValid - . (localNextConnOutId ^= connId + 1) + let connId = vst ^. localNextConnOutId + return ( LocalEndPointValid + . (localNextConnOutId ^= connId + 1) $ vst , connId) LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" -- | Create a new local endpoint --- +-- -- May throw a TransportError NewEndPointErrorCode exception if the transport -- is closed. createLocalEndPoint :: TCPTransport -> IO LocalEndPoint -createLocalEndPoint transport = do +createLocalEndPoint transport = do chan <- newChan - state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState + state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState { _localNextConnOutId = firstNonReservedLightweightConnectionId , _localConnections = Map.empty - , _nextConnInId = firstNonReservedHeavyweightConnectionId + , _nextConnInId = firstNonReservedHeavyweightConnectionId } modifyMVar (transportState transport) $ \st -> case st of TransportValid vst -> do let ix = vst ^. nextEndPointId - let addr = encodeEndPointAddress (transportHost transport) + let addr = encodeEndPointAddress (transportHost transport) (transportPort transport) - ix + ix let localEndPoint = LocalEndPoint { localAddress = addr , localChannel = chan , localState = state } - return ( TransportValid - . (localEndPointAt addr ^= Just localEndPoint) - . (nextEndPointId ^= ix + 1) + return ( TransportValid + . (localEndPointAt addr ^= Just localEndPoint) + . (nextEndPointId ^= ix + 1) $ vst , localEndPoint ) @@ -1282,13 +1282,13 @@ removeRemoteEndPoint (ourEndPoint, theirEndPoint) = modifyMVar_ ourState $ \st -> case st of LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of - Nothing -> + Nothing -> return st Just remoteEndPoint' -> - if remoteId remoteEndPoint' == remoteId theirEndPoint - then return - ( LocalEndPointValid - . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) + if remoteId remoteEndPoint' == remoteId theirEndPoint + then return + ( LocalEndPointValid + . (localConnectionTo (remoteAddress theirEndPoint) ^= Nothing) $ vst ) else return st @@ -1302,23 +1302,23 @@ removeRemoteEndPoint (ourEndPoint, theirEndPoint) = -- -- Does nothing if the transport is closed removeLocalEndPoint :: TCPTransport -> LocalEndPoint -> IO () -removeLocalEndPoint transport ourEndPoint = - modifyMVar_ (transportState transport) $ \st -> case st of +removeLocalEndPoint transport ourEndPoint = + modifyMVar_ (transportState transport) $ \st -> case st of TransportValid vst -> - return ( TransportValid - . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) + return ( TransportValid + . (localEndPointAt (localAddress ourEndPoint) ^= Nothing) $ vst ) TransportClosed -> return TransportClosed -- | Find a remote endpoint. If the remote endpoint does not yet exist we --- create it in Init state. Returns if the endpoint was new. -findRemoteEndPoint +-- create it in Init state. Returns if the endpoint was new. +findRemoteEndPoint :: LocalEndPoint -> EndPointAddress - -> RequestedBy - -> IO (RemoteEndPoint, Bool) + -> RequestedBy + -> IO (RemoteEndPoint, Bool) findRemoteEndPoint ourEndPoint theirAddress findOrigin = go where go = do @@ -1335,55 +1335,55 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go { remoteAddress = theirAddress , remoteState = theirState , remoteId = vst ^. nextConnInId - , remoteScheduled = scheduled + , remoteScheduled = scheduled } - return ( LocalEndPointValid - . (localConnectionTo theirAddress ^= Just theirEndPoint) - . (nextConnInId ^: (+ 1)) + return ( LocalEndPointValid + . (localConnectionTo theirAddress ^= Just theirEndPoint) + . (nextConnInId ^: (+ 1)) $ vst - , (theirEndPoint, True) + , (theirEndPoint, True) ) LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" - - if isNew + + if isNew then return (theirEndPoint, True) else do let theirState = remoteState theirEndPoint snapshot <- modifyMVar theirState $ \st -> case st of - RemoteEndPointValid vst -> + RemoteEndPointValid vst -> case findOrigin of RequestedByUs -> do - let st' = RemoteEndPointValid - . (remoteOutgoing ^: (+ 1)) - $ vst + let st' = RemoteEndPointValid + . (remoteOutgoing ^: (+ 1)) + $ vst return (st', st') RequestedByThem -> - return (st, st) + return (st, st) _ -> return (st, st) -- The snapshot may no longer be up to date at this point, but if we - -- increased the refcount then it can only either be Valid or Failed - -- (after an explicit call to 'closeEndPoint' or 'closeTransport') + -- increased the refcount then it can only either be Valid or Failed + -- (after an explicit call to 'closeEndPoint' or 'closeTransport') case snapshot of RemoteEndPointInvalid err -> throwIO err RemoteEndPointInit resolved crossed initOrigin -> case (findOrigin, initOrigin) of - (RequestedByUs, RequestedByUs) -> - readMVar resolved >> go - (RequestedByUs, RequestedByThem) -> + (RequestedByUs, RequestedByUs) -> readMVar resolved >> go - (RequestedByThem, RequestedByUs) -> - if ourAddress > theirAddress + (RequestedByUs, RequestedByThem) -> + readMVar resolved >> go + (RequestedByThem, RequestedByUs) -> + if ourAddress > theirAddress then do -- Wait for the Crossed message - readMVar crossed + readMVar crossed return (theirEndPoint, True) else return (theirEndPoint, False) - (RequestedByThem, RequestedByThem) -> + (RequestedByThem, RequestedByThem) -> throwIO $ userError "Already connected" RemoteEndPointValid _ -> -- We assume that the request crossed if we find the endpoint in @@ -1395,15 +1395,15 @@ findRemoteEndPoint ourEndPoint theirAddress findOrigin = go readMVar resolved >> go RemoteEndPointClosed -> go - RemoteEndPointFailed err -> + RemoteEndPointFailed err -> throwIO err - - ourState = localState ourEndPoint + + ourState = localState ourEndPoint ourAddress = localAddress ourEndPoint -- | Send a payload over a heavyweight connection (thread safe) -sendOn :: ValidRemoteEndPointState -> [ByteString] -> IO () -sendOn vst bs = withMVar (remoteSendLock vst) $ \() -> +sendOn :: ValidRemoteEndPointState -> [ByteString] -> IO () +sendOn vst bs = withMVar (remoteSendLock vst) $ \() -> sendMany (remoteSocket vst) bs -------------------------------------------------------------------------------- @@ -1417,7 +1417,7 @@ type Action a = MVar (Either SomeException a) schedule :: RemoteEndPoint -> IO a -> IO (Action a) schedule theirEndPoint act = do mvar <- newEmptyMVar - writeChan (remoteScheduled theirEndPoint) $ + writeChan (remoteScheduled theirEndPoint) $ catch (act >>= putMVar mvar . Right) (putMVar mvar . Left) return mvar @@ -1429,7 +1429,7 @@ schedule theirEndPoint act = do -- > runScheduledAction -- -- 'runScheduledAction' will run @p@ (it might run some other scheduled action). --- However, it will then wait until @p@ is executed (by this call to +-- However, it will then wait until @p@ is executed (by this call to -- 'runScheduledAction' or by another). runScheduledAction :: EndPointPair -> Action a -> IO a runScheduledAction (ourEndPoint, theirEndPoint) mvar = do @@ -1438,21 +1438,21 @@ runScheduledAction (ourEndPoint, theirEndPoint) mvar = do case ma of Right a -> return a Left e -> do - forM_ (fromException e) $ \ioe -> + forM_ (fromException e) $ \ioe -> modifyMVar_ (remoteState theirEndPoint) $ \st -> - case st of + case st of RemoteEndPointValid vst -> handleIOException ioe vst _ -> return (RemoteEndPointFailed ioe) throwIO e where - handleIOException :: IOException - -> ValidRemoteEndPointState - -> IO RemoteState + handleIOException :: IOException + -> ValidRemoteEndPointState + -> IO RemoteState handleIOException ex vst = do tryCloseSocket (remoteSocket vst) - let code = EventConnectionLost (remoteAddress theirEndPoint) + let code = EventConnectionLost (remoteAddress theirEndPoint) err = TransportError code (show ex) - writeChan (localChannel ourEndPoint) $ ErrorEvent err + writeChan (localChannel ourEndPoint) $ ErrorEvent err return (RemoteEndPointFailed ex) -------------------------------------------------------------------------------- @@ -1462,25 +1462,25 @@ runScheduledAction (ourEndPoint, theirEndPoint) mvar = do -- | Establish a connection to a remote endpoint -- -- Maybe throw a TransportError -socketToEndPoint :: EndPointAddress -- ^ Our address +socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address -> Bool -- ^ Use SO_REUSEADDR? - -> Maybe Int -- ^ Timeout for connect - -> IO (Either (TransportError ConnectErrorCode) - (N.Socket, ConnectionRequestResponse)) -socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = - try $ do - (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of + -> Maybe Int -- ^ Timeout for connect + -> IO (Either (TransportError ConnectErrorCode) + (N.Socket, ConnectionRequestResponse)) +socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = + try $ do + (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of Nothing -> throwIO (failed . userError $ "Could not parse") Just dec -> return dec - addr:_ <- mapIOException invalidAddress $ + addr:_ <- mapIOException invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port) bracketOnError (createSocket addr) tryCloseSocket $ \sock -> do - when reuseAddr $ + when reuseAddr $ mapIOException failed $ N.setSocketOption sock N.ReuseAddr 1 - mapIOException invalidAddress $ - timeoutMaybe timeout timeoutError $ - N.connect sock (N.addrAddress addr) + mapIOException invalidAddress $ + timeoutMaybe timeout timeoutError $ + N.connect sock (N.addrAddress addr) response <- mapIOException failed $ do sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) recvInt32 sock @@ -1489,50 +1489,50 @@ socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr timeout = Just r -> return (sock, r) where createSocket :: N.AddrInfo -> IO N.Socket - createSocket addr = mapIOException insufficientResources $ + createSocket addr = mapIOException insufficientResources $ N.socket (N.addrFamily addr) N.Stream N.defaultProtocol - invalidAddress = TransportError ConnectNotFound . show - insufficientResources = TransportError ConnectInsufficientResources . show + invalidAddress = TransportError ConnectNotFound . show + insufficientResources = TransportError ConnectInsufficientResources . show failed = TransportError ConnectFailed . show timeoutError = TransportError ConnectTimeout "Timed out" -- | Encode end point address -encodeEndPointAddress :: N.HostName - -> N.ServiceName - -> EndPointId +encodeEndPointAddress :: N.HostName + -> N.ServiceName + -> EndPointId -> EndPointAddress encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $ - host ++ ":" ++ port ++ ":" ++ show ix + host ++ ":" ++ port ++ ":" ++ show ix -- | Decode end point address -decodeEndPointAddress :: EndPointAddress +decodeEndPointAddress :: EndPointAddress -> Maybe (N.HostName, N.ServiceName, EndPointId) -decodeEndPointAddress (EndPointAddress bs) = +decodeEndPointAddress (EndPointAddress bs) = case splitMaxFromEnd (== ':') 2 $ BSC.unpack bs of - [host, port, endPointIdStr] -> - case reads endPointIdStr of + [host, port, endPointIdStr] -> + case reads endPointIdStr of [(endPointId, "")] -> Just (host, port, endPointId) _ -> Nothing _ -> Nothing -- | Construct a ConnectionId -createConnectionId :: HeavyweightConnectionId - -> LightweightConnectionId +createConnectionId :: HeavyweightConnectionId + -> LightweightConnectionId -> ConnectionId -createConnectionId hcid lcid = +createConnectionId hcid lcid = (fromIntegral hcid `shiftL` 32) .|. fromIntegral lcid -- | @spltiMaxFromEnd p n xs@ splits list @xs@ at elements matching @p@, -- returning at most @p@ segments -- counting from the /end/ --- +-- -- > splitMaxFromEnd (== ':') 2 "ab:cd:ef:gh" == ["ab:cd", "ef", "gh"] splitMaxFromEnd :: (a -> Bool) -> Int -> [a] -> [[a]] -splitMaxFromEnd p = \n -> go [[]] n . reverse +splitMaxFromEnd p = \n -> go [[]] n . reverse where -- go :: [[a]] -> Int -> [a] -> [[a]] - go accs _ [] = accs + go accs _ [] = accs go ([] : accs) 0 xs = reverse xs : accs go (acc : accs) n (x:xs) = if p x then go ([] : acc : accs) (n - 1) xs @@ -1544,40 +1544,40 @@ splitMaxFromEnd p = \n -> go [[]] n . reverse -------------------------------------------------------------------------------- -- Find a socket between two endpoints --- +-- -- Throws an IO exception if the socket could not be found. -internalSocketBetween :: TCPTransport -- ^ Transport +internalSocketBetween :: TCPTransport -- ^ Transport -> EndPointAddress -- ^ Local endpoint -> EndPointAddress -- ^ Remote endpoint - -> IO N.Socket + -> IO N.Socket internalSocketBetween transport ourAddress theirAddress = do ourEndPoint <- withMVar (transportState transport) $ \st -> case st of - TransportClosed -> - throwIO $ userError "Transport closed" - TransportValid vst -> + TransportClosed -> + throwIO $ userError "Transport closed" + TransportValid vst -> case vst ^. localEndPointAt ourAddress of Nothing -> throwIO $ userError "Local endpoint not found" Just ep -> return ep theirEndPoint <- withMVar (localState ourEndPoint) $ \st -> case st of LocalEndPointClosed -> throwIO $ userError "Local endpoint closed" - LocalEndPointValid vst -> + LocalEndPointValid vst -> case vst ^. localConnectionTo theirAddress of Nothing -> throwIO $ userError "Remote endpoint not found" Just ep -> return ep withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInit _ _ _ -> throwIO $ userError "Remote endpoint not yet initialized" - RemoteEndPointValid vst -> + RemoteEndPointValid vst -> return $ remoteSocket vst RemoteEndPointClosing _ vst -> - return $ remoteSocket vst + return $ remoteSocket vst RemoteEndPointClosed -> throwIO $ userError "Remote endpoint closed" RemoteEndPointInvalid err -> - throwIO err + throwIO err RemoteEndPointFailed err -> - throwIO err + throwIO err -------------------------------------------------------------------------------- -- Constants -- @@ -1611,7 +1611,7 @@ localNextConnOutId = accessor _localNextConnOutId (\cix st -> st { _localNextCon localConnections :: Accessor ValidLocalEndPointState (Map EndPointAddress RemoteEndPoint) localConnections = accessor _localConnections (\es st -> st { _localConnections = es }) -nextConnInId :: Accessor ValidLocalEndPointState HeavyweightConnectionId +nextConnInId :: Accessor ValidLocalEndPointState HeavyweightConnectionId nextConnInId = accessor _nextConnInId (\rid st -> st { _nextConnInId = rid }) remoteOutgoing :: Accessor ValidRemoteEndPointState Int @@ -1627,10 +1627,10 @@ remoteNextConnOutId :: Accessor ValidRemoteEndPointState LightweightConnectionId remoteNextConnOutId = accessor _remoteNextConnOutId (\cix st -> st { _remoteNextConnOutId = cix }) localEndPointAt :: EndPointAddress -> Accessor ValidTransportState (Maybe LocalEndPoint) -localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr +localEndPointAt addr = localEndPoints >>> DAC.mapMaybe addr localConnectionTo :: EndPointAddress -> Accessor ValidLocalEndPointState (Maybe RemoteEndPoint) -localConnectionTo addr = localConnections >>> DAC.mapMaybe addr +localConnectionTo addr = localConnections >>> DAC.mapMaybe addr ------------------------------------------------------------------------------- -- Debugging -- @@ -1638,14 +1638,14 @@ localConnectionTo addr = localConnections >>> DAC.mapMaybe addr relyViolation :: EndPointPair -> String -> IO a relyViolation (ourEndPoint, theirEndPoint) str = do - elog (ourEndPoint, theirEndPoint) (str ++ " RELY violation") + elog (ourEndPoint, theirEndPoint) (str ++ " RELY violation") fail (str ++ " RELY violation") elog :: EndPointPair -> String -> IO () elog (ourEndPoint, theirEndPoint) msg = do tid <- myThreadId - putStrLn $ show (localAddress ourEndPoint) - ++ "/" ++ show (remoteAddress theirEndPoint) + putStrLn $ show (localAddress ourEndPoint) + ++ "/" ++ show (remoteAddress theirEndPoint) ++ "(" ++ show (remoteId theirEndPoint) ++ ")" - ++ "/" ++ show tid + ++ "/" ++ show tid ++ ": " ++ msg diff --git a/src/Network/Transport/TCP/Internal.hs b/src/Network/Transport/TCP/Internal.hs index 2c24eefe..4991b090 100644 --- a/src/Network/Transport/TCP/Internal.hs +++ b/src/Network/Transport/TCP/Internal.hs @@ -1,8 +1,8 @@ --- | Utility functions for TCP sockets -module Network.Transport.TCP.Internal +-- | Utility functions for TCP sockets +module Network.Transport.TCP.Internal ( forkServer , recvWithLength - , recvExact + , recvExact , recvInt32 , tryCloseSocket ) where @@ -16,7 +16,7 @@ import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask) #ifdef USE_MOCK_NETWORK import qualified Network.Transport.TCP.Mock.Socket as N #else -import qualified Network.Socket as N +import qualified Network.Socket as N #endif ( HostName , ServiceName @@ -49,10 +49,10 @@ import Control.Applicative ((<$>)) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length, concat, null) import Data.Int (Int32) -import Data.ByteString.Lazy.Internal (smallChunkSize) +import Data.ByteString.Lazy.Internal (smallChunkSize) -- | Start a server at the specified address. --- +-- -- This sets up a server socket for the specified host and port. Exceptions -- thrown during setup are not caught. -- @@ -67,15 +67,15 @@ import Data.ByteString.Lazy.Internal (smallChunkSize) -- The request handler should spawn threads to handle each individual request -- or the server will block. Once a thread has been spawned it will be the -- responsibility of the new thread to close the socket when an exception --- occurs. +-- occurs. forkServer :: N.HostName -- ^ Host - -> N.ServiceName -- ^ Port + -> N.ServiceName -- ^ Port -> Int -- ^ Backlog (maximum number of queued connections) -> Bool -- ^ Set ReuseAddr option? -> (SomeException -> IO ()) -- ^ Termination handler - -> (N.Socket -> IO ()) -- ^ Request handler + -> (N.Socket -> IO ()) -- ^ Request handler -> IO ThreadId -forkServer host port backlog reuseAddr terminationHandler requestHandler = do +forkServer host port backlog reuseAddr terminationHandler requestHandler = do -- Resolve the specified address. By specification, getAddrInfo will never -- return an empty list (but will throw an exception instead) and will return -- the "best" address first, whatever that means @@ -84,14 +84,14 @@ forkServer host port backlog reuseAddr terminationHandler requestHandler = do tryCloseSocket $ \sock -> do when reuseAddr $ N.setSocketOption sock N.ReuseAddr 1 N.bindSocket sock (N.addrAddress addr) - N.listen sock backlog + N.listen sock backlog -- We start listening for incoming requests in a separate thread. When -- that thread is killed, we close the server socket and the termination -- handler. We have to make sure that the exception handler is installed -- /before/ any asynchronous exception occurs. So we mask_, then fork -- (the child thread inherits the masked state from the parent), then -- unmask only inside the catch. - mask_ $ forkIOWithUnmask $ \unmask -> + mask_ $ forkIOWithUnmask $ \unmask -> catch (unmask (forever $ acceptRequest sock)) $ \ex -> do tryCloseSocket sock terminationHandler ex @@ -100,34 +100,34 @@ forkServer host port backlog reuseAddr terminationHandler requestHandler = do acceptRequest sock = bracketOnError (N.accept sock) (tryCloseSocket . fst) (requestHandler . fst) - + -- | Read a length and then a payload of that length recvWithLength :: N.Socket -> IO [ByteString] recvWithLength sock = recvInt32 sock >>= recvExact sock -- | Receive a 32-bit integer -recvInt32 :: Num a => N.Socket -> IO a -recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4 +recvInt32 :: Num a => N.Socket -> IO a +recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4 -- | Close a socket, ignoring I/O exceptions tryCloseSocket :: N.Socket -> IO () -tryCloseSocket sock = void . tryIO $ +tryCloseSocket sock = void . tryIO $ N.sClose sock -- | Read an exact number of bytes from a socket --- +-- -- Throws an I/O exception if the socket closes before the specified -- number of bytes could be read -recvExact :: N.Socket -- ^ Socket to read from +recvExact :: N.Socket -- ^ Socket to read from -> Int32 -- ^ Number of bytes to read -> IO [ByteString] -recvExact _ len | len < 0 = throwIO (userError "recvExact: Negative length") +recvExact _ len | len < 0 = throwIO (userError "recvExact: Negative length") recvExact sock len = go [] len where - go :: [ByteString] -> Int32 -> IO [ByteString] - go acc 0 = return (reverse acc) + go :: [ByteString] -> Int32 -> IO [ByteString] + go acc 0 = return (reverse acc) go acc l = do bs <- NBS.recv sock (fromIntegral l `min` smallChunkSize) - if BS.null bs + if BS.null bs then throwIO (userError "recvExact: Socket closed") else go (bs : acc) (l - fromIntegral (BS.length bs)) diff --git a/src/Network/Transport/TCP/Mock/Socket.hs b/src/Network/Transport/TCP/Mock/Socket.hs index 489cb643..85ed3c8c 100644 --- a/src/Network/Transport/TCP/Mock/Socket.hs +++ b/src/Network/Transport/TCP/Mock/Socket.hs @@ -38,7 +38,7 @@ import Data.Map (Map) import qualified Data.Map as Map import Control.Exception (throwIO) import Control.Category ((>>>)) -import Control.Concurrent.MVar +import Control.Concurrent.MVar import Control.Concurrent.Chan import System.IO.Unsafe (unsafePerformIO) import Data.Accessor (Accessor, accessor, (^=), (^.), (^:)) @@ -70,7 +70,7 @@ get :: Accessor MockState a -> IO a get acc = timeoutThrow mvarThreshold $ withMVar mockState $ return . (^. acc) set :: Accessor MockState a -> a -> IO () -set acc val = timeoutThrow mvarThreshold $ modifyMVar_ mockState $ return . (acc ^= val) +set acc val = timeoutThrow mvarThreshold $ modifyMVar_ mockState $ return . (acc ^= val) boundSockets :: Accessor MockState (Map SockAddr Socket) boundSockets = accessor _boundSockets (\bs st -> st { _boundSockets = bs }) @@ -90,34 +90,34 @@ validHostnames = accessor _validHostnames (\ns st -> st { _validHostnames = ns } type HostName = String type ServiceName = String -type PortNumber = String -type HostAddress = String +type PortNumber = String +type HostAddress = String -data SocketType = Stream +data SocketType = Stream data SocketOption = ReuseAddr data ShutdownCmd = ShutdownSend data Family data ProtocolNumber -data Socket = Socket { +data Socket = Socket { socketState :: MVar SocketState , socketDescription :: String } -data SocketState = +data SocketState = Uninit - | BoundSocket { - socketBacklog :: Chan (Socket, SockAddr, MVar Socket) + | BoundSocket { + socketBacklog :: Chan (Socket, SockAddr, MVar Socket) } - | Connected { - socketBuff :: Chan Message + | Connected { + socketBuff :: Chan Message , _socketPeer :: Maybe Socket , _scheduledReadActions :: [(Int, IO ())] } | Closed -data Message = +data Message = Payload Word8 | CloseSocket @@ -145,43 +145,43 @@ getAddrInfo :: Maybe AddrInfo -> Maybe HostName -> Maybe ServiceName -> IO [Addr getAddrInfo _ (Just host) (Just port) = do validHosts <- get validHostnames if host `elem` validHosts - then return . return $ AddrInfo { - addrFamily = error "Family unused" - , addrAddress = SockAddrInet port host + then return . return $ AddrInfo { + addrFamily = error "Family unused" + , addrAddress = SockAddrInet port host } else throwSocketError $ "getAddrInfo: invalid hostname '" ++ host ++ "'" getAddrInfo _ _ _ = error "getAddrInfo: unsupported arguments" defaultHints :: AddrInfo -defaultHints = error "defaultHints not implemented" +defaultHints = error "defaultHints not implemented" socket :: Family -> SocketType -> ProtocolNumber -> IO Socket socket _ Stream _ = do state <- newMVar Uninit sid <- get nextSocketId set nextSocketId (sid + 1) - return Socket { + return Socket { socketState = state , socketDescription = show sid } - + bindSocket :: Socket -> SockAddr -> IO () bindSocket sock addr = do timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> case st of Uninit -> do backlog <- newChan - return BoundSocket { - socketBacklog = backlog + return BoundSocket { + socketBacklog = backlog } _ -> throwSocketError "bind: socket already initialized" set (boundSocketAt addr) (Just sock) - + listen :: Socket -> Int -> IO () -listen _ _ = return () +listen _ _ = return () defaultProtocol :: ProtocolNumber -defaultProtocol = error "defaultProtocol not implemented" +defaultProtocol = error "defaultProtocol not implemented" setSocketOption :: Socket -> SocketOption -> Int -> IO () setSocketOption _ ReuseAddr 1 = return () @@ -190,13 +190,13 @@ setSocketOption _ _ _ = error "setSocketOption: unsupported arguments" accept :: Socket -> IO (Socket, SockAddr) accept serverSock = do backlog <- timeoutThrow mvarThreshold $ withMVar (socketState serverSock) $ \st -> case st of - BoundSocket {} -> + BoundSocket {} -> return (socketBacklog st) _ -> throwSocketError "accept: socket not bound" - (theirSocket, theirAddress, reply) <- readChan backlog + (theirSocket, theirAddress, reply) <- readChan backlog ourBuff <- newChan - ourState <- newMVar Connected { + ourState <- newMVar Connected { socketBuff = ourBuff , _socketPeer = Just theirSocket , _scheduledReadActions = [] @@ -205,13 +205,13 @@ accept serverSock = do socketState = ourState , socketDescription = "" } - timeoutThrow mvarThreshold $ putMVar reply ourSocket + timeoutThrow mvarThreshold $ putMVar reply ourSocket return (ourSocket, theirAddress) sClose :: Socket -> IO () sClose sock = do -- Close the peer socket - writeSocket sock CloseSocket + writeSocket sock CloseSocket -- Close our socket timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> @@ -220,7 +220,7 @@ sClose sock = do -- In case there is a parallel read stuck on a readChan writeChan (socketBuff st) CloseSocket return Closed - _ -> + _ -> return Closed connect :: Socket -> SockAddr -> IO () @@ -235,13 +235,13 @@ connect us serverAddr = do throwSocketError "connect: server socket not bound" reply <- newEmptyMVar writeChan serverBacklog (us, SockAddrInet "" "", reply) - them <- timeoutThrow mvarThreshold $ readMVar reply + them <- timeoutThrow mvarThreshold $ readMVar reply timeoutThrow mvarThreshold $ modifyMVar_ (socketState us) $ \st -> case st of - Uninit -> do + Uninit -> do buff <- newChan - return Connected { + return Connected { socketBuff = buff - , _socketPeer = Just them + , _socketPeer = Just them , _scheduledReadActions = [] } _ -> @@ -249,7 +249,7 @@ connect us serverAddr = do Nothing -> throwSocketError "connect: unknown address" sOMAXCONN :: Int -sOMAXCONN = error "sOMAXCONN not implemented" +sOMAXCONN = error "sOMAXCONN not implemented" shutdown :: Socket -> ShutdownCmd -> IO () shutdown sock ShutdownSend = do @@ -267,7 +267,7 @@ shutdown sock ShutdownSend = do peerBuffer :: Socket -> IO (Either String (Chan Message)) peerBuffer sock = do mPeer <- timeoutThrow mvarThreshold $ withMVar (socketState sock) $ \st -> case st of - Connected {} -> + Connected {} -> return (st ^. socketPeer) _ -> return Nothing @@ -276,9 +276,9 @@ peerBuffer sock = do Connected {} -> return (Right (socketBuff st)) _ -> - return (Left "Peer socket closed") - Nothing -> - return (Left "Socket closed") + return (Left "Peer socket closed") + Nothing -> + return (Left "Socket closed") throwSocketError :: String -> IO a throwSocketError = throwIO . userError @@ -287,8 +287,8 @@ writeSocket :: Socket -> Message -> IO () writeSocket sock msg = do theirBuff <- peerBuffer sock case theirBuff of - Right buff -> writeChan buff msg - Left err -> case msg of Payload _ -> throwSocketError $ "writeSocket: " ++ err + Right buff -> writeChan buff msg + Left err -> case msg of Payload _ -> throwSocketError $ "writeSocket: " ++ err CloseSocket -> return () readSocket :: Socket -> IO (Maybe Word8) @@ -304,7 +304,7 @@ readSocket sock = do case mBuff of Just (buff, actions) -> do sequence actions - msg <- timeoutThrow readSocketThreshold $ readChan buff + msg <- timeoutThrow readSocketThreshold $ readChan buff case msg of Payload w -> return (Just w) CloseSocket -> timeoutThrow mvarThreshold $ modifyMVar (socketState sock) $ \st -> case st of @@ -312,7 +312,7 @@ readSocket sock = do return (Closed, Nothing) _ -> throwSocketError "readSocket: socket in unexpected state" - Nothing -> + Nothing -> return Nothing -- | Given a list of scheduled actions, reduce all delays by 1, and return the @@ -321,7 +321,7 @@ tick :: [(Int, IO ())] -> ([(Int, IO ())], [IO ()]) tick = go [] [] where go later now [] = (reverse later, reverse now) - go later now ((n, action) : actions) + go later now ((n, action) : actions) | n == 0 = go later (action : now) actions | otherwise = go ((n - 1, action) : later) now actions @@ -330,16 +330,16 @@ tick = go [] [] -------------------------------------------------------------------------------- -- | Schedule an action to be executed after /n/ reads on this socket --- +-- -- If /n/ is zero we execute the action immediately. scheduleReadAction :: Socket -> Int -> IO () -> IO () scheduleReadAction _ 0 action = action -scheduleReadAction sock n action = - modifyMVar_ (socketState sock) $ \st -> case st of +scheduleReadAction sock n action = + modifyMVar_ (socketState sock) $ \st -> case st of Connected {} -> return (scheduledReadActions ^: ((n, action) :) $ st) _ -> - throwSocketError "scheduleReadAction: socket not connected" + throwSocketError "scheduleReadAction: socket not connected" -------------------------------------------------------------------------------- -- Util -- diff --git a/tests/TestQC.hs b/tests/TestQC.hs index 4dfdcddd..c80ffa00 100644 --- a/tests/TestQC.hs +++ b/tests/TestQC.hs @@ -1,10 +1,10 @@ -- Test the TCP transport using QuickCheck generated scripts -- --- TODO: This is not quite working yet. The main problem, I think, is the +-- TODO: This is not quite working yet. The main problem, I think, is the -- allocation of "bundle ID"s to connections. The problem is exposed by the -- aptly-named regression test script_Foo (to be renamed once I figure out what -- bug that test is actually exposing :) -module Main +module Main ( main -- Shush the compiler about unused definitions , log @@ -18,7 +18,7 @@ import Prelude hiding (log) import Test.Framework (Test, TestName, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.Framework.Providers.HUnit (testCase) -import Test.QuickCheck +import Test.QuickCheck ( Gen , choose , suchThatMaybe @@ -55,7 +55,7 @@ import qualified Data.Set as Set import GHC.Stack (currentCallStack, renderStack) import Network.Transport -import Network.Transport.TCP +import Network.Transport.TCP ( createTransportExposeInternals , defaultTCPParameters , TransportInternals(socketBetween) @@ -74,26 +74,26 @@ type ConnectionIx = Int -- | We randomly generate /scripts/ which are essentially a deep embedding of -- the Transport API. These scripts are then executed and the results compared -- against an abstract interpreter. -data ScriptCmd = +data ScriptCmd = -- | Create a new endpoint NewEndPoint -- | @Connect i j@ creates a connection from endpoint @i@ to endpoint @j@, -- where @i@ and @j@ are indices and refer to the @i@th and @j@th endpoint -- created by NewEndPoint - | Connect SourceEndPointIx TargetEndPointIx + | Connect SourceEndPointIx TargetEndPointIx -- | @Close i@ closes the @i@ connection created using 'Connect'. Note that -- closing a connection does not shift other indices; in other words, in -- @[Connect 0 0, Close 0, Connect 0 0, Close 0]@ the second 'Close' -- refers to the first (already closed) connection - | Close ConnectionIx - -- | @Send i bs@ sends payload @bs@ on the @i@ connection created + | Close ConnectionIx + -- | @Send i bs@ sends payload @bs@ on the @i@ connection created | Send ConnectionIx [ByteString] -- | @BreakAfterReads n i j@ force-closes the socket between endpoints @i@ - -- and @j@ after @n@ reads by @i@ - -- + -- and @j@ after @n@ reads by @i@ + -- -- We should have @i /= j@ because the TCP transport does not use sockets -- for connections from an endpoint to itself - | BreakAfterReads Int SourceEndPointIx TargetEndPointIx + | BreakAfterReads Int SourceEndPointIx TargetEndPointIx deriving Show type Script = [ScriptCmd] @@ -111,7 +111,7 @@ instance Show a => Show (Variable a) where -- | In the implementation "bundles" are purely a conceptual idea, but in the -- verifier we need to concretize this notion -type BundleId = Int +type BundleId = Int data ConnectionInfo = ConnectionInfo { source :: EndPointAddress @@ -120,10 +120,10 @@ data ConnectionInfo = ConnectionInfo { , connectionBundle :: BundleId } deriving Show - + data ExpEvent = ExpConnectionOpened ConnectionInfo - | ExpConnectionClosed ConnectionInfo + | ExpConnectionClosed ConnectionInfo | ExpReceived ConnectionInfo [ByteString] | ExpConnectionLost BundleId EndPointAddress deriving Show @@ -149,18 +149,18 @@ data RunState = RunState { -- | Current bundle ID between two endpoints -- -- Invariant: For all keys (A, B), A <= B - , _currentBundle :: Map (EndPointAddress, EndPointAddress) BundleId + , _currentBundle :: Map (EndPointAddress, EndPointAddress) BundleId } -initialRunState :: RunState +initialRunState :: RunState initialRunState = RunState { _endPoints = [] , _connections = [] , _expectedEvents = Map.empty , _forwardingThreads = [] - , _mayBreak = Set.empty + , _mayBreak = Set.empty , _broken = Set.empty - , _currentBundle = Map.empty + , _currentBundle = Map.empty } verify :: (Transport, TransportInternals) -> Script -> IO (Either String ()) @@ -168,17 +168,17 @@ verify (transport, transportInternals) script = do allEvents <- newQ let runScript :: Script -> StateT RunState IO () - runScript = mapM_ runCmd + runScript = mapM_ runCmd - runCmd :: ScriptCmd -> StateT RunState IO () + runCmd :: ScriptCmd -> StateT RunState IO () runCmd NewEndPoint = do mEndPoint <- liftIO $ newEndPoint transport case mEndPoint of Right endPoint -> do - tid <- liftIO $ forkIO (forward endPoint) - append endPoints endPoint - append forwardingThreads tid - set (expectedEventsAt (address endPoint)) [] + tid <- liftIO $ forkIO (forward endPoint) + append endPoints endPoint + append forwardingThreads tid + set (expectedEventsAt (address endPoint)) [] Left err -> liftIO $ throwIO err runCmd (Connect i j) = do @@ -190,11 +190,11 @@ verify (transport, transportInternals) script = do connMayBreak = mayBreak (address endPointA) endPointB case mConn of Right conn -> do - bundleBroken <- get bundleId >>= get . connBroken - currentBundleId <- if bundleBroken + bundleBroken <- get bundleId >>= get . connBroken + currentBundleId <- if bundleBroken then modify bundleId (+ 1) >> get bundleId - else get bundleId - connId <- Variable <$> liftIO newUnique + else get bundleId + connId <- Variable <$> liftIO newUnique let connInfo = ConnectionInfo { source = address endPointA , target = endPointB @@ -204,17 +204,17 @@ verify (transport, transportInternals) script = do append connections (conn, connInfo) append (expectedEventsAt endPointB) (ExpConnectionOpened connInfo) Left err -> do - currentBundleId <- get bundleId + currentBundleId <- get bundleId expectingBreak <- get $ connMayBreak currentBundleId - if expectingBreak + if expectingBreak then do set (connMayBreak currentBundleId) False set (connBroken currentBundleId) True - else + else liftIO $ throwIO err runCmd (Close i) = do (conn, connInfo) <- get (connectionAt i) - liftIO $ close conn + liftIO $ close conn append (expectedEventsAt (target connInfo)) (ExpConnectionClosed connInfo) runCmd (Send i payload) = do (conn, connInfo) <- get (connectionAt i) @@ -224,16 +224,16 @@ verify (transport, transportInternals) script = do case mResult of Right () -> return () Left err -> do - expectingBreak <- get connMayBreak + expectingBreak <- get connMayBreak isBroken <- get connBroken if expectingBreak || isBroken then do set connMayBreak False set connBroken True - else + else liftIO $ throwIO err append (expectedEventsAt (target connInfo)) (ExpReceived connInfo payload) - -- TODO: This will only work if a connection between 'i' and 'j' has + -- TODO: This will only work if a connection between 'i' and 'j' has -- already been established. We would need to modify the mock network -- layer to support breaking "future" connections runCmd (BreakAfterReads n i j) = do @@ -247,13 +247,13 @@ verify (transport, transportInternals) script = do set (mayBreak endPointB endPointA currentBundleId) True append (expectedEventsAt endPointA) (ExpConnectionLost currentBundleId endPointB) append (expectedEventsAt endPointB) (ExpConnectionLost currentBundleId endPointA) - + forward :: EndPoint -> IO () forward endPoint = forever $ do ev <- receive endPoint pushL allEvents (address endPoint, ev) - collectEvents :: RunState -> IO (Map EndPointAddress [Event]) + collectEvents :: RunState -> IO (Map EndPointAddress [Event]) collectEvents st = do threadDelay 10000 mapM_ killThread (st ^. forwardingThreads) @@ -264,17 +264,17 @@ verify (transport, transportInternals) script = do mEv <- tryPopR allEvents case mEv of Just ev -> go (ev : acc) - Nothing -> return (reverse acc) - - st <- execStateT (runScript script) initialRunState + Nothing -> return (reverse acc) + + st <- execStateT (runScript script) initialRunState actualEvents <- collectEvents st - - let eventsMatch = all (uncurry match) $ + + let eventsMatch = all (uncurry match) $ zip (Map.elems (st ^. expectedEvents)) (Map.elems actualEvents) - return $ if eventsMatch - then Right () + return $ if eventsMatch + then Right () else Left ("Could not match " ++ show (st ^. expectedEvents) ++ " and " ++ show actualEvents) @@ -296,7 +296,7 @@ possibleTraces :: [ExpEvent] -> [[ExpEvent]] possibleTraces = go where go [] = [[]] - go (ev@(ExpConnectionLost _ _) : evs) = + go (ev@(ExpConnectionLost _ _) : evs) = [ trace | evs' <- possibleTraces evs, trace <- insertConnectionLost ev evs' ] go (ev : evs) = [ trace | evs' <- possibleTraces evs, trace <- insertEvent ev evs' ] @@ -304,38 +304,38 @@ possibleTraces = go -- We don't know when exactly the error will occur (indeed, it may never -- happen at all), but it must occur before any future connection lost -- event to the same destination. - -- If it occurs now, then all other events on this bundle will not happen. + -- If it occurs now, then all other events on this bundle will not happen. insertConnectionLost :: ExpEvent -> [ExpEvent] -> [[ExpEvent]] insertConnectionLost ev [] = [[ev], []] insertConnectionLost ev@(ExpConnectionLost bid addr) (ev' : evs) = (ev : removeBundle bid (ev' : evs)) : case ev' of - ExpConnectionLost _ addr' | addr == addr' -> [] + ExpConnectionLost _ addr' | addr == addr' -> [] _ -> [ev' : evs' | evs' <- insertConnectionLost ev evs] insertConnectionLost _ _ = error "The impossible happened" - + -- All other events can be arbitrarily reordered /across/ connections, but -- never /within/ connections insertEvent :: ExpEvent -> [ExpEvent] -> [[ExpEvent]] insertEvent ev [] = [[ev]] insertEvent ev (ev' : evs) = - (ev : ev' : evs) : - if eventConnId ev == eventConnId ev' + (ev : ev' : evs) : + if eventConnId ev == eventConnId ev' then [] else [ev' : evs' | evs' <- insertEvent ev evs] removeBundle :: BundleId -> [ExpEvent] -> [ExpEvent] - removeBundle bid = filter ((/= bid) . eventBundleId) + removeBundle bid = filter ((/= bid) . eventBundleId) eventBundleId :: ExpEvent -> BundleId eventBundleId (ExpConnectionOpened connInfo) = connectionBundle connInfo eventBundleId (ExpConnectionClosed connInfo) = connectionBundle connInfo eventBundleId (ExpReceived connInfo _) = connectionBundle connInfo eventBundleId (ExpConnectionLost bid _) = bid - - eventConnId :: ExpEvent -> Maybe (Variable ConnectionId) - eventConnId (ExpConnectionOpened connInfo) = Just $ connectionId connInfo - eventConnId (ExpConnectionClosed connInfo) = Just $ connectionId connInfo + + eventConnId :: ExpEvent -> Maybe (Variable ConnectionId) + eventConnId (ExpConnectionOpened connInfo) = Just $ connectionId connInfo + eventConnId (ExpConnectionClosed connInfo) = Just $ connectionId connInfo eventConnId (ExpReceived connInfo _) = Just $ connectionId connInfo eventConnId (ExpConnectionLost _ _) = Nothing @@ -343,56 +343,56 @@ possibleTraces = go -- Unification -- -------------------------------------------------------------------------------- -type Substitution = Map Unique ConnectionId +type Substitution = Map Unique ConnectionId -newtype Unifier a = Unifier { - runUnifier :: Substitution -> Maybe (a, Substitution) - } +newtype Unifier a = Unifier { + runUnifier :: Substitution -> Maybe (a, Substitution) + } instance Monad Unifier where return x = Unifier $ \subst -> Just (x, subst) x >>= f = Unifier $ \subst -> case runUnifier x subst of Nothing -> Nothing Just (a, subst') -> runUnifier (f a) subst' - fail _str = mzero + fail _str = mzero -instance MonadPlus Unifier where +instance MonadPlus Unifier where mzero = Unifier $ const Nothing f `mplus` g = Unifier $ \subst -> case runUnifier f subst of Nothing -> runUnifier g subst Just (a, subst') -> Just (a, subst') class Unify a b where - unify :: a -> b -> Unifier () + unify :: a -> b -> Unifier () canUnify :: Unify a b => a -> b -> Bool canUnify a b = isJust $ runUnifier (unify a b) Map.empty instance Unify Unique ConnectionId where - unify x cid = Unifier $ \subst -> + unify x cid = Unifier $ \subst -> case Map.lookup x subst of Just cid' -> if cid == cid' then Just ((), subst) else Nothing Nothing -> Just ((), Map.insert x cid subst) instance Unify (Variable ConnectionId) ConnectionId where - unify (Variable x) connId = unify x connId - unify (Value connId') connId = guard $ connId' == connId + unify (Variable x) connId = unify x connId + unify (Value connId') connId = guard $ connId' == connId instance Unify ExpEvent Event where - unify (ExpConnectionOpened connInfo) (ConnectionOpened connId _ _) = - unify (connectionId connInfo) connId - unify (ExpConnectionClosed connInfo) (ConnectionClosed connId) = + unify (ExpConnectionOpened connInfo) (ConnectionOpened connId _ _) = + unify (connectionId connInfo) connId + unify (ExpConnectionClosed connInfo) (ConnectionClosed connId) = unify (connectionId connInfo) connId unify (ExpReceived connInfo payload) (Received connId payload') = do guard $ BSS.concat payload == BSS.concat payload' unify (connectionId connInfo) connId - unify (ExpConnectionLost _ addr) (ErrorEvent (TransportError (EventConnectionLost addr') _)) = + unify (ExpConnectionLost _ addr) (ErrorEvent (TransportError (EventConnectionLost addr') _)) = guard $ addr == addr' - unify _ _ = fail "Cannot unify" + unify _ _ = fail "Cannot unify" instance Unify a b => Unify [a] [b] where - unify [] [] = return () + unify [] [] = return () unify (x:xs) (y:ys) = unify x y >> unify xs ys unify _ _ = fail "Cannot unify" @@ -422,7 +422,7 @@ script_Connect numEndPoints = do script_ConnectClose :: Int -> Gen Script script_ConnectClose numEndPoints = do - script <- go Map.empty + script <- go Map.empty return (replicate numEndPoints NewEndPoint ++ script) where go :: Map Int Bool -> Gen Script @@ -432,15 +432,15 @@ script_ConnectClose numEndPoints = do 0 -> do fr <- choose (0, numEndPoints - 1) to <- choose (0, numEndPoints - 1) - cmds <- go (Map.insert (Map.size conns) True conns) + cmds <- go (Map.insert (Map.size conns) True conns) return (Connect fr to : cmds) 1 -> do - mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns - case mConn of + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns + case mConn of Nothing -> go conns Just conn -> do cmds <- go (Map.insert conn False conns) - return (Close conn : cmds) + return (Close conn : cmds) _ -> return [] @@ -449,7 +449,7 @@ script_ConnectClose numEndPoints = do script_ConnectSendClose :: Int -> Gen Script script_ConnectSendClose numEndPoints = do - script <- go Map.empty + script <- go Map.empty return (replicate numEndPoints NewEndPoint ++ script) where go :: Map Int Bool -> Gen Script @@ -459,24 +459,24 @@ script_ConnectSendClose numEndPoints = do 0 -> do fr <- choose (0, numEndPoints - 1) to <- choose (0, numEndPoints - 1) - cmds <- go (Map.insert (Map.size conns) True conns) + cmds <- go (Map.insert (Map.size conns) True conns) return (Connect fr to : cmds) 1 -> do - mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns - case mConn of + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns + case mConn of Nothing -> go conns Just conn -> do numSegments <- choose (0, 2) - payload <- replicateM numSegments arbitrary - cmds <- go conns - return (Send conn payload : cmds) + payload <- replicateM numSegments arbitrary + cmds <- go conns + return (Send conn payload : cmds) 2 -> do - mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns - case mConn of + mConn <- choose (0, Map.size conns - 1) `suchThatMaybe` isOpen conns + case mConn of Nothing -> go conns Just conn -> do cmds <- go (Map.insert conn False conns) - return (Close conn : cmds) + return (Close conn : cmds) _ -> return [] @@ -505,7 +505,7 @@ withErrors numErrors gen = gen >>= insertError numErrors return $ cmd : cmds' -------------------------------------------------------------------------------- --- Individual scripts to test specific bugs -- +-- Individual scripts to test specific bugs -- -------------------------------------------------------------------------------- -- | Bug #1 @@ -513,15 +513,15 @@ withErrors numErrors gen = gen >>= insertError numErrors -- When process A wants to close the heavyweight connection to process B it -- sends a CloseSocket request together with the ID of the last connection from -- B. When B receives the CloseSocket request it can compare this ID to the last --- connection it created; if they don't match, B knows that there are some --- messages still on the way from B to A (in particular, a CreatedConnection --- message) which will cancel the CloseSocket request from A. Hence, it will +-- connection it created; if they don't match, B knows that there are some +-- messages still on the way from B to A (in particular, a CreatedConnection +-- message) which will cancel the CloseSocket request from A. Hence, it will -- know to ignore the CloseSocket request from A. -- -- The bug was that we recorded the last _created_ outgoing connection on the -- local endpoint, but the last _received_ incoming connection on the state of -- the heavyweight connection. So, in the script below, the following happened: --- +-- -- A connects to B, records "last connection ID is 1024" -- A closes the lightweight connection, sends [CloseConnection 1024] -- A closes the heivyweight connection, sends [CloseSocket 0] @@ -541,7 +541,7 @@ withErrors numErrors gen = gen >>= insertError numErrors -- A receives the [CloseSocket 0] request, compares it to the last recorded -- outgoing ID (1024), sees that they are not equal, and concludes that this -- must mean that there is still a CreatedConnection message on the way from A --- to B. +-- to B. -- -- This of course is not the case, so B will wait forever for A to confirm -- the CloseSocket request, and deadlock arises. (This deadlock doesn't become @@ -572,7 +572,7 @@ script_MultipleSends = [ , Send 0 ["E"] ] --- | Simulate broken network connection during send +-- | Simulate broken network connection during send script_BreakSend :: Script script_BreakSend = [ NewEndPoint @@ -650,12 +650,12 @@ tests transport = [ , testOne "Foo" transport script_Foo ] , testGroup "Without errors" [ - testGroup "One endpoint, with delays" (basicTests transport 1 id) - , testGroup "Two endpoints, with delays" (basicTests transport 2 id) + testGroup "One endpoint, with delays" (basicTests transport 1 id) + , testGroup "Two endpoints, with delays" (basicTests transport 2 id) , testGroup "Three endpoints, with delays" (basicTests transport 3 id) ] , testGroup "Single error" [ - testGroup "Two endpoints, with delays" (basicTests transport 2 (withErrors 1)) + testGroup "Two endpoints, with delays" (basicTests transport 2 (withErrors 1)) , testGroup "Three endpoints, with delays" (basicTests transport 3 (withErrors 1)) ] ] @@ -665,7 +665,7 @@ testOne :: TestName -> (Transport, TransportInternals) -> Script -> Test testOne label transport script = testCase label (testScript transport script) testGen :: TestName -> (Transport, TransportInternals) -> Gen Script -> Test -testGen label transport script = testProperty label (testScriptGen transport script) +testGen label transport script = testProperty label (testScriptGen transport script) main :: IO () main = do @@ -677,10 +677,10 @@ main = do -------------------------------------------------------------------------------- testScriptGen :: (Transport, TransportInternals) -> Gen Script -> Property -testScriptGen transport scriptGen = - forAll scriptGen $ \script -> - morallyDubiousIOProperty $ do - logShow script +testScriptGen transport scriptGen = + forAll scriptGen $ \script -> + morallyDubiousIOProperty $ do + logShow script mErr <- try $ verify transport script return $ case mErr of Left (ExpectedFailure str) -> @@ -696,12 +696,12 @@ testScriptGen transport scriptGen = testScript :: (Transport, TransportInternals) -> Script -> Assertion testScript transport script = do - logShow script + logShow script mErr <- try $ verify transport script case mErr of - Left (ExpectedFailure _str) -> + Left (ExpectedFailure _str) -> return () - Right (Left err) -> + Right (Left err) -> assertFailure $ "Failed with script " ++ show script ++ ": " ++ err ++ "\n" Right (Right ()) -> return () @@ -756,7 +756,7 @@ verticalList :: Show a => [a] -> PP.Doc verticalList = PP.brackets . PP.vcat . map (PP.text . show) instance Show Script where - show = ("\n" ++) . show . verticalList + show = ("\n" ++) . show . verticalList instance Show [Event] where show = ("\n" ++) . show . verticalList @@ -765,13 +765,13 @@ instance Show [ExpEvent] where show = ("\n" ++) . show . verticalList instance Show (Map EndPointAddress [ExpEvent]) where - show = ("\n" ++) . show . PP.brackets . PP.vcat - . map (\(addr, evs) -> PP.hcat . PP.punctuate PP.comma $ [PP.text (show addr), verticalList evs]) + show = ("\n" ++) . show . PP.brackets . PP.vcat + . map (\(addr, evs) -> PP.hcat . PP.punctuate PP.comma $ [PP.text (show addr), verticalList evs]) . Map.toList instance Show (Map EndPointAddress [Event]) where - show = ("\n" ++) . show . PP.brackets . PP.vcat - . map (\(addr, evs) -> PP.hcat . PP.punctuate PP.comma $ [PP.text (show addr), verticalList evs]) + show = ("\n" ++) . show . PP.brackets . PP.vcat + . map (\(addr, evs) -> PP.hcat . PP.punctuate PP.comma $ [PP.text (show addr), verticalList evs]) . Map.toList -------------------------------------------------------------------------------- @@ -784,25 +784,25 @@ class Distribution d where probabilityOf :: d -> Double -> Double instance Distribution NormalD where - probabilityOf d x = a * exp (-0.5 * b * b) + probabilityOf d x = a * exp (-0.5 * b * b) where a = 1 / (stdDev d * sqrt (2 * pi)) b = (x - mean d) / stdDev d --- | Choose from a distribution +-- | Choose from a distribution chooseFrom :: Distribution d => d -> (Double, Double) -> Gen Double -chooseFrom d (lo, hi) = findCandidate +chooseFrom d (lo, hi) = findCandidate where - findCandidate :: Gen Double + findCandidate :: Gen Double findCandidate = do candidate <- choose (lo, hi) uniformSample <- choose (0, 1) if uniformSample < probabilityOf d candidate then return candidate - else findCandidate + else findCandidate chooseFrom' :: Distribution d => d -> (Int, Int) -> Gen Int -chooseFrom' d (lo, hi) = +chooseFrom' d (lo, hi) = round <$> chooseFrom d (fromIntegral lo, fromIntegral hi) -------------------------------------------------------------------------------- @@ -817,15 +817,15 @@ logShow = log . show instance Arbitrary ByteString where arbitrary = do - len <- chooseFrom' NormalD { mean = 5, stdDev = 10 } (0, 100) + len <- chooseFrom' NormalD { mean = 5, stdDev = 10 } (0, 100) xs <- replicateM len arbitrary return (pack xs) listAccessor :: Int -> Accessor [a] a -listAccessor i = accessor (!! i) (error "listAccessor.set not defined") +listAccessor i = accessor (!! i) (error "listAccessor.set not defined") append :: Monad m => Accessor st [a] -> a -> StateT st m () -append acc x = modify acc (snoc x) +append acc x = modify acc (snoc x) snoc :: a -> [a] -> [a] snoc x xs = xs ++ [x] @@ -833,8 +833,8 @@ snoc x xs = xs ++ [x] groupByKey :: Ord a => [a] -> [(a, b)] -> Map a [b] groupByKey keys = go (Map.fromList [(key, []) | key <- keys]) where - go acc [] = Map.map reverse acc - go acc ((key, val) : rest) = go (Map.adjust (val :) key acc) rest + go acc [] = Map.map reverse acc + go acc ((key, val) : rest) = go (Map.adjust (val :) key acc) rest -------------------------------------------------------------------------------- -- Expected failures (can't find explicit support for this in test-framework) -- diff --git a/tests/TestTCP.hs b/tests/TestTCP.hs index 71f45973..47c90071 100644 --- a/tests/TestTCP.hs +++ b/tests/TestTCP.hs @@ -2,7 +2,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Prelude hiding +import Prelude hiding ( (>>=) , return , fail @@ -49,7 +49,7 @@ import Network.Transport.TCP.Internal (recvInt32, forkServer, recvWithLength) #ifdef USE_MOCK_NETWORK import qualified Network.Transport.TCP.Mock.Socket as N #else -import qualified Network.Socket as N +import qualified Network.Socket as N #endif ( sClose , ServiceName @@ -69,9 +69,9 @@ import Data.String (fromString) import GHC.IO.Exception (ioe_errno) import Foreign.C.Error (Errno(..), eADDRNOTAVAIL) import System.Timeout (timeout) -import Network.Transport.Tests (testTransport) +import Network.Transport.Tests (testTransport) import Network.Transport.Tests.Auxiliary (forkTry, runTests) -import Network.Transport.Tests.Traced +import Network.Transport.Tests.Traced instance Traceable ControlHeader where trace = traceShow @@ -80,7 +80,7 @@ instance Traceable ConnectionRequestResponse where trace = traceShow instance Traceable N.Socket where - trace = traceShow + trace = traceShow instance Traceable N.AddrInfo where trace = traceShow @@ -112,17 +112,17 @@ testEarlyDisconnect nextPort = do -- TEST 1: they connect to us, then drop the connection do - ConnectionOpened _ _ addr <- receive endpoint + ConnectionOpened _ _ addr <- receive endpoint True <- return $ addr == theirAddr - - ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint - True <- return $ addr' == theirAddr + + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint + True <- return $ addr' == theirAddr return () -- TEST 2: after they dropped their connection to us, we now try to -- establish a connection to them. This should re-establish the broken - -- TCP connection. + -- TCP connection. tlog "Trying to connect to client" Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -131,39 +131,39 @@ testEarlyDisconnect nextPort = do -- closes the socket do Right () <- send conn ["ping"] - - ConnectionOpened cid _ addr <- receive endpoint + + ConnectionOpened cid _ addr <- receive endpoint True <- return $ addr == theirAddr - + Received cid' ["pong"] <- receive endpoint True <- return $ cid == cid' - + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint - True <- return $ addr' == theirAddr + True <- return $ addr' == theirAddr return () -- TEST 4: A subsequent send on an already-open connection will now break Left (TransportError SendFailed _) <- send conn ["ping2"] - -- *Pfew* + -- *Pfew* putMVar serverDone () client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () client serverAddr clientAddr = do tlog "Client" clientPort <- nextPort - let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 - putMVar clientAddr ourAddress - + let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 + putMVar clientAddr ourAddress + -- Listen for incoming messages forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do - -- Initial setup + -- Initial setup 0 <- recvInt32 sock :: IO Int - _ <- recvWithLength sock + _ <- recvWithLength sock sendMany sock [encodeInt32 ConnectionRequestAccepted] - -- Server opens a logical connection + -- Server opens a logical connection CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) 1024 <- recvInt32 sock :: IO LightweightConnectionId @@ -171,21 +171,21 @@ testEarlyDisconnect nextPort = do 1024 <- recvInt32 sock :: IO Int ["ping"] <- recvWithLength sock - -- Reply + -- Reply sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10002 :: Int)] - sendMany sock (encodeInt32 10002 : prependLength ["pong"]) + sendMany sock (encodeInt32 10002 : prependLength ["pong"]) -- Close the socket N.sClose sock - + -- Connect to the server Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - + -- Open a new connection sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10003 :: Int)] - + -- Close the socket without closing the connection explicitly - -- The server should receive an error event + -- The server should receive an error event N.sClose sock -- | Test the behaviour of a premature CloseSocket request @@ -211,19 +211,19 @@ testEarlyCloseSocket nextPort = do -- TEST 1: they connect to us, then send a CloseSocket. Since we don't -- have any outgoing connections, this means we will agree to close the - -- socket + -- socket do - ConnectionOpened cid _ addr <- receive endpoint + ConnectionOpened cid _ addr <- receive endpoint True <- return $ addr == theirAddr - - ConnectionClosed cid' <- receive endpoint + + ConnectionClosed cid' <- receive endpoint True <- return $ cid' == cid return () -- TEST 2: after they dropped their connection to us, we now try to -- establish a connection to them. This should re-establish the broken - -- TCP connection. + -- TCP connection. tlog "Trying to connect to client" Right conn <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -234,65 +234,65 @@ testEarlyCloseSocket nextPort = do -- the socket gets closed do Right () <- send conn ["ping"] - + ConnectionOpened cid _ addr <- receive endpoint True <- return $ addr == theirAddr - + Received cid' ["pong"] <- receive endpoint True <- return $ cid' == cid - + ConnectionClosed cid'' <- receive endpoint True <- return $ cid'' == cid - + ErrorEvent (TransportError (EventConnectionLost addr') _) <- receive endpoint - True <- return $ addr' == theirAddr - + True <- return $ addr' == theirAddr + return () -- TEST 4: A subsequent send on an already-open connection will now break Left (TransportError SendFailed _) <- send conn ["ping2"] - -- *Pfew* + -- *Pfew* putMVar serverDone () client :: MVar EndPointAddress -> MVar EndPointAddress -> IO () client serverAddr clientAddr = do tlog "Client" clientPort <- nextPort - let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 - putMVar clientAddr ourAddress - + let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 + putMVar clientAddr ourAddress + -- Listen for incoming messages forkServer "127.0.0.1" clientPort 5 True throwIO $ \sock -> do - -- Initial setup + -- Initial setup 0 <- recvInt32 sock :: IO Int - _ <- recvWithLength sock + _ <- recvWithLength sock sendMany sock [encodeInt32 ConnectionRequestAccepted] - -- Server opens a logical connection + -- Server opens a logical connection CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) - 1024 <- recvInt32 sock :: IO LightweightConnectionId + 1024 <- recvInt32 sock :: IO LightweightConnectionId -- Server sends a message 1024 <- recvInt32 sock :: IO Int ["ping"] <- recvWithLength sock - -- Reply + -- Reply sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10002 :: Int)] - sendMany sock (encodeInt32 (10002 :: Int) : prependLength ["pong"]) + sendMany sock (encodeInt32 (10002 :: Int) : prependLength ["pong"]) -- Send a CloseSocket even though there are still connections *in both -- directions* sendMany sock [encodeInt32 CloseSocket, encodeInt32 (1024 :: Int)] N.sClose sock - + -- Connect to the server Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing - + -- Open a new connection sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10003 :: Int)] - - -- Send a CloseSocket without sending a closeconnecton + + -- Send a CloseSocket without sending a closeconnecton -- The server should still receive a ConnectionClosed message sendMany sock [encodeInt32 CloseSocket, encodeInt32 (0 :: Int)] N.sClose sock @@ -311,26 +311,26 @@ testInvalidConnect nextPort = do Right endpoint <- newEndPoint transport -- Syntax error in the endpoint address - Left (TransportError ConnectFailed _) <- + Left (TransportError ConnectFailed _) <- connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered defaultConnectHints - + -- Syntax connect, but invalid hostname (TCP address lookup failure) - Left (TransportError ConnectNotFound _) <- + Left (TransportError ConnectNotFound _) <- connect endpoint (encodeEndPointAddress "invalidHost" "port" 0) ReliableOrdered defaultConnectHints - + -- TCP address correct, but nobody home at that address - Left (TransportError ConnectNotFound _) <- + Left (TransportError ConnectNotFound _) <- connect endpoint (encodeEndPointAddress "127.0.0.1" "9000" 0) ReliableOrdered defaultConnectHints - + -- Valid TCP address but invalid endpoint number - Left (TransportError ConnectNotFound _) <- + Left (TransportError ConnectNotFound _) <- connect endpoint (encodeEndPointAddress "127.0.0.1" port 1) ReliableOrdered defaultConnectHints return () -- | Test that an endpoint can ignore CloseSocket requests (in "reality" this -- would happen when the endpoint sends a new connection request before --- receiving an (already underway) CloseSocket request) +-- receiving an (already underway) CloseSocket request) testIgnoreCloseSocket :: IO N.ServiceName -> IO () testIgnoreCloseSocket nextPort = do serverAddr <- newEmptyMVar @@ -397,7 +397,7 @@ testIgnoreCloseSocket nextPort = do -- Close it again tlog "Closing connection" - sendMany sock [encodeInt32 CloseConnection, encodeInt32 (1024 :: Int)] + sendMany sock [encodeInt32 CloseConnection, encodeInt32 (1024 :: Int)] -- And close the connection completely tlog "Closing socket" @@ -471,7 +471,7 @@ testBlockAfterCloseSocket nextPort = do unblocked <- newMVar False -- We should not hear from the server until we unblock him by - -- responding to the CloseSocket request (in this case, we + -- responding to the CloseSocket request (in this case, we -- respond by sending a ConnectionRequest) forkTry $ do recvInt32 sock :: IO Int32 @@ -490,7 +490,7 @@ testBlockAfterCloseSocket nextPort = do -- | Test what happens when a remote endpoint sends a connection request to our -- transport for an endpoint it already has a connection to -testUnnecessaryConnect :: IO N.ServiceName -> Int -> IO () +testUnnecessaryConnect :: IO N.ServiceName -> Int -> IO () testUnnecessaryConnect nextPort numThreads = do clientDone <- newEmptyMVar serverAddr <- newEmptyMVar @@ -501,22 +501,22 @@ testUnnecessaryConnect nextPort numThreads = do putMVar serverAddr (address endpoint) forkTry $ do - -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check + -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check let ourAddress = EndPointAddress "126.0.0.1" -- We should only get a single 'Accepted' reply gotAccepted <- newEmptyMVar dones <- replicateM numThreads $ do - done <- newEmptyMVar + done <- newEmptyMVar forkTry $ do -- It is possible that the remote endpoint just rejects the request by closing the socket -- immediately (depending on far the remote endpoint got with the initialization) - response <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing + response <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True Nothing case response of - Right (_, ConnectionRequestAccepted) -> + Right (_, ConnectionRequestAccepted) -> -- We don't close this socket because we want to keep this connection open putMVar gotAccepted () - -- We might get either Invalid or Crossed (the transport does not + -- We might get either Invalid or Crossed (the transport does not -- maintain enough history to be able to tell) Right (sock, ConnectionRequestInvalid) -> N.sClose sock @@ -536,7 +536,7 @@ testUnnecessaryConnect nextPort numThreads = do testMany :: IO N.ServiceName -> IO () testMany nextPort = do Right masterTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters - Right masterEndPoint <- newEndPoint masterTransport + Right masterEndPoint <- newEndPoint masterTransport replicateM_ 10 $ do mTransport <- nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters @@ -544,7 +544,7 @@ testMany nextPort = do Left ex -> do putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex) case (ioe_errno ex) of - Just no | Errno no == eADDRNOTAVAIL -> putStrLn "(ADDRNOTAVAIL)" + Just no | Errno no == eADDRNOTAVAIL -> putStrLn "(ADDRNOTAVAIL)" _ -> return () throwIO ex Right transport -> @@ -561,7 +561,7 @@ testBreakTransport nextPort = do killThread (transportThread internals) -- Uh oh - ErrorEvent (TransportError EventTransportFailed _) <- receive endpoint + ErrorEvent (TransportError EventTransportFailed _) <- receive endpoint return () @@ -580,16 +580,16 @@ testReconnect nextPort = do endpointCreated <- newEmptyMVar -- Server - forkTry $ do + forkTry $ do -- Wait for the client to do its first attempt - readMVar firstAttempt + readMVar firstAttempt - counter <- newMVar (0 :: Int) + counter <- newMVar (0 :: Int) forkServer "127.0.0.1" serverPort 5 True throwIO $ \sock -> do - -- Accept the connection + -- Accept the connection Right 0 <- tryIO $ (recvInt32 sock :: IO Int) - Right _ <- tryIO $ recvWithLength sock + Right _ <- tryIO $ recvWithLength sock Right () <- tryIO $ sendMany sock [encodeInt32 ConnectionRequestAccepted] -- The first time we close the socket before accepting the logical connection @@ -604,10 +604,10 @@ testReconnect nextPort = do when (count > 1) $ do -- Client sends a message Right connId' <- tryIO $ (recvInt32 sock :: IO LightweightConnectionId) - True <- return $ connId == connId' + True <- return $ connId == connId' Right ["ping"] <- tryIO $ recvWithLength sock putMVar serverDone () - + Right () <- tryIO $ N.sClose sock return () @@ -626,17 +626,17 @@ testReconnect nextPort = do -- The second attempt will fail because the server closes the socket before we can request a connection takeMVar endpointCreated -- This might time out or not, depending on whether the server closes the - -- socket before or after we can send the RequestConnectionId request - resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints + -- socket before or after we can send the RequestConnectionId request + resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints case resultConnect of Nothing -> return () Just (Left (TransportError ConnectFailed _)) -> return () Just (Left err) -> throwIO err - Just (Right _) -> throwIO $ userError "testConnect: unexpected connect success" + Just (Right _) -> throwIO $ userError "testConnect: unexpected connect success" -- The third attempt succeeds Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints - + -- But a send will fail because the server has closed the connection again threadDelay 100000 Left (TransportError SendFailed _) <- send conn1 ["ping"] @@ -675,12 +675,12 @@ testUnidirectionalError nextPort = do CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int) connId <- recvInt32 sock :: IO LightweightConnectionId - + connId' <- recvInt32 sock :: IO LightweightConnectionId True <- return $ connId == connId' ["ping"] <- recvWithLength sock putMVar serverGotPing () - + -- Client forkTry $ do Right (transport, internals) <- nextPort >>= \port -> createTransportExposeInternals "127.0.0.1" port defaultTCPParameters @@ -693,12 +693,12 @@ testUnidirectionalError nextPort = do takeMVar serverGotPing -- Close the *outgoing* part of the socket only - sock <- socketBetween internals (address endpoint) theirAddr + sock <- socketBetween internals (address endpoint) theirAddr N.shutdown sock N.ShutdownSend -- At this point we cannot notice the problem yet so we shouldn't receive an event yet Nothing <- timeout 500000 $ receive endpoint - + -- But when we send we find the error Left (TransportError SendFailed _) <- send conn1 ["ping"] ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint @@ -709,7 +709,7 @@ testUnidirectionalError nextPort = do takeMVar serverGotPing -- Again, close the outgoing part of the socket - sock' <- socketBetween internals (address endpoint) theirAddr + sock' <- socketBetween internals (address endpoint) theirAddr N.shutdown sock' N.ShutdownSend -- We now find the error when we attempt to close the connection @@ -720,10 +720,10 @@ testUnidirectionalError nextPort = do send conn3 ["ping"] takeMVar serverGotPing - -- We repeat once more. - sock'' <- socketBetween internals (address endpoint) theirAddr + -- We repeat once more. + sock'' <- socketBetween internals (address endpoint) theirAddr N.shutdown sock'' N.ShutdownSend - + -- Now we notice the problem when we try to connect Nothing <- timeout 500000 $ receive endpoint Left (TransportError ConnectFailed _) <- connect endpoint theirAddr ReliableOrdered defaultConnectHints @@ -754,7 +754,7 @@ testInvalidCloseConnection nextPort = do -- connection ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint - putMVar serverDone () + putMVar serverDone () -- Client forkTry $ do @@ -766,7 +766,7 @@ testInvalidCloseConnection nextPort = do Right _ <- connect endpoint theirAddr ReliableOrdered defaultConnectHints -- Get a handle on the TCP connection and manually send an invalid CloseConnection request - sock <- socketBetween internals ourAddr theirAddr + sock <- socketBetween internals ourAddr theirAddr sendMany sock [encodeInt32 CloseConnection, encodeInt32 (12345 :: Int)] putMVar clientDone () @@ -776,23 +776,23 @@ testInvalidCloseConnection nextPort = do main :: IO () main = do portMVar <- newEmptyMVar - forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show - let nextPort = takeMVar portMVar - tcpResult <- tryIO $ runTests + forkTry $ forM_ ([10080 ..] :: [Int]) $ putMVar portMVar . show + let nextPort = takeMVar portMVar + tcpResult <- tryIO $ runTests [ ("EarlyDisconnect", testEarlyDisconnect nextPort) , ("EarlyCloseSocket", testEarlyCloseSocket nextPort) , ("IgnoreCloseSocket", testIgnoreCloseSocket nextPort) , ("BlockAfterCloseSocket", testBlockAfterCloseSocket nextPort) , ("UnnecessaryConnect", testUnnecessaryConnect nextPort 10) , ("InvalidAddress", testInvalidAddress nextPort) - , ("InvalidConnect", testInvalidConnect nextPort) + , ("InvalidConnect", testInvalidConnect nextPort) , ("Many", testMany nextPort) , ("BreakTransport", testBreakTransport nextPort) , ("Reconnect", testReconnect nextPort) , ("UnidirectionalError", testUnidirectionalError nextPort) , ("InvalidCloseConnection", testInvalidCloseConnection nextPort) ] - -- Run the generic tests even if the TCP specific tests failed.. + -- Run the generic tests even if the TCP specific tests failed.. testTransport (either (Left . show) (Right) <$> nextPort >>= \port -> createTransport "127.0.0.1" port defaultTCPParameters) -- ..but if the generic tests pass, still fail if the specific tests did not case tcpResult of From ed47024276ac12a37238b54aadcd644813b5bfe3 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0312/2357] Remove trailing whitespace --- src/Control/Distributed/Static.hs | 128 +++++++++++++++--------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/src/Control/Distributed/Static.hs b/src/Control/Distributed/Static.hs index 53cae039..696a0d91 100644 --- a/src/Control/Distributed/Static.hs +++ b/src/Control/Distributed/Static.hs @@ -1,9 +1,9 @@ --- | /Towards Haskell in the Cloud/ (Epstein et al, Haskell Symposium 2011) +-- | /Towards Haskell in the Cloud/ (Epstein et al, Haskell Symposium 2011) -- introduces the concept of /static/ values: values that are known at compile --- time. In a distributed setting where all nodes are running the same +-- time. In a distributed setting where all nodes are running the same -- executable, static values can be serialized simply by transmitting a code -- pointer to the value. This however requires special compiler support, which --- is not yet available in ghc. We can mimick the behaviour by keeping an +-- is not yet available in ghc. We can mimick the behaviour by keeping an -- explicit mapping ('RemoteTable') from labels to values (and making sure that -- all distributed nodes are using the same 'RemoteTable'). In this module -- we implement this mimickry and various extensions. @@ -13,13 +13,13 @@ -- The paper stipulates that 'Static' values should have a free 'Binary' -- instance: -- --- > instance Binary (Static a) +-- > instance Binary (Static a) -- -- This however is not (runtime) type safe: for instance, what would be the -- behaviour of -- --- > f :: Static Int -> Static Bool --- > f = decode . encode +-- > f :: Static Int -> Static Bool +-- > f = decode . encode -- -- For this reason we work only with 'Typeable' terms in this module, and -- implement runtime checks @@ -34,7 +34,7 @@ -- -- Since the runtime mapping ('RemoteTable') contains values of different types, -- it maps labels ('String's) to 'Data.Rank1Dynamic.Dynamic' values. Again, we --- use the implementation from "Data.Rank1Dynamic" so that we can store +-- use the implementation from "Data.Rank1Dynamic" so that we can store -- polymorphic dynamic values. -- -- [Compositionality] @@ -43,18 +43,18 @@ -- way to combine two static values and get a static value out of it. This -- makes sense when interpreting static strictly as /known at compile time/, -- but it severely limits expressiveness. However, the main motivation for --- 'static' is not that they are known at compile time but rather that +-- 'static' is not that they are known at compile time but rather that -- /they provide a free/ 'Binary' /instance/. We therefore provide two basic -- constructors for 'Static' values: -- -- > staticLabel :: String -> Static a -- > staticApply :: Static (a -> b) -> Static a -> Static b -- --- The first constructor refers to a label in a 'RemoteTable'. The second +-- The first constructor refers to a label in a 'RemoteTable'. The second -- allows to apply a static function to a static argument, and makes 'Static' -- compositional: once we have 'staticApply' we can implement numerous derived -- combinators on 'Static' values (we define a few in this module; see --- 'staticCompose', 'staticSplit', and 'staticConst'). +-- 'staticCompose', 'staticSplit', and 'staticConst'). -- -- [Closures] -- @@ -67,7 +67,7 @@ -- -- See /Towards Haskell in the Cloud/ for the rationale behind representing -- the function closure environment in serialized ('ByteString') form. Any --- static value can trivially be turned into a 'Closure' ('staticClosure'). +-- static value can trivially be turned into a 'Closure' ('staticClosure'). -- Moreover, since 'Static' is now compositional, we can also define derived -- operators on 'Closure' values ('closureApplyStatic', 'closureApply', -- 'closureCompose', 'closureSplit'). @@ -76,9 +76,9 @@ -- -- Suppose we are working in the context of some distributed environment, with -- a monadic type 'Process' representing processes, 'NodeId' representing node --- addresses and 'ProcessId' representing process addresses. Suppose further --- that we have a primitive --- +-- addresses and 'ProcessId' representing process addresses. Suppose further +-- that we have a primitive +-- -- > sendInt :: ProcessId -> Int -> Process () -- -- We might want to define @@ -106,7 +106,7 @@ -- > sendIntClosure :: ProcessId -> Closure (Int -> Process ()) -- > sendIntClosure pid = closure decoder (encode pid) -- > where --- > decoder :: Static (ByteString -> Int -> Process ()) +-- > decoder :: Static (ByteString -> Int -> Process ()) -- > decoder = sendIntStatic `staticCompose` decodeProcessIdStatic -- -- [Polymorphic example] @@ -118,8 +118,8 @@ -- which turns a process that computes an integer into a process that computes -- the integer and then sends it someplace else. -- --- We can define --- +-- We can define +-- -- > bindStatic :: (Typeable a, Typeable b) => Static (Process a -> (a -> Process b) -> Process b) -- > bindStatic = staticLabel "$bind" -- @@ -132,7 +132,7 @@ -- -- (Note that we are using the special 'Data.Rank1Typeable.ANY1' and -- 'Data.Rank1Typeable.ANY2' types from "Data.Rank1Typeable" to represent this --- polymorphic value.) Once we have a static bind we can define +-- polymorphic value.) Once we have a static bind we can define -- -- > sendIntResult :: ProcessId -> Closure (Process Int) -> Closure (Process ()) -- > sendIntResult pid cl = bindStatic `closureApplyStatic` cl `closureApply` sendIntClosure pid @@ -157,7 +157,7 @@ -- > sendDict BinaryDict = send -- -- Now 'sendDict' is a normal polymorphic value: --- +-- -- > sendDictStatic :: Static (BinaryDict a -> ProcessId -> a -> Process ()) -- > sendDictStatic = staticLabel "$sendDict" -- > @@ -165,20 +165,20 @@ -- > rtable = ... -- > . registerStatic "$sendDict" (sendDict :: BinaryDict ANY -> ProcessId -> ANY -> Process ()) -- > $ initRemoteTable --- +-- -- so that we can define -- -- > sendClosure :: Static (BinaryDict a) -> Process a -> Closure (a -> Process ()) -- > sendClosure dict pid = closure decoder (encode pid) -- > where -- > decoder :: Static (ByteString -> a -> Process ()) --- > decoder = (sendDictStatic `staticApply` dict) `staticCompose` decodeProcessIdStatic +-- > decoder = (sendDictStatic `staticApply` dict) `staticCompose` decodeProcessIdStatic -- -- [Word of Caution] -- -- You should not /define/ functions on 'ANY' and co. For example, the following -- definition of 'rtable' is incorrect: --- +-- -- > rtable :: RemoteTable -- > rtable = registerStatic "$sdictSendPort" sdictSendPort -- > $ initRemoteTable @@ -186,7 +186,7 @@ -- > sdictSendPort :: SerializableDict ANY -> SerializableDict (SendPort ANY) -- > sdictSendPort SerializableDict = SerializableDict -- --- This definition of 'sdictSendPort' ignores its argument completely, and +-- This definition of 'sdictSendPort' ignores its argument completely, and -- constructs a 'SerializableDict' for the /monomorphic/ type @SendPort ANY@, -- which isn't what you want. Instead, you should do -- @@ -196,8 +196,8 @@ -- > where -- > sdictSendPort :: forall a. SerializableDict a -> SerializableDict (SendPort a) -- > sdictSendPort SerializableDict = SerializableDict -module Control.Distributed.Static - ( -- * Static values +module Control.Distributed.Static + ( -- * Static values Static , staticLabel , staticApply @@ -215,15 +215,15 @@ module Control.Distributed.Static , closureApply , closureCompose , closureSplit - -- * Resolution + -- * Resolution , RemoteTable , initRemoteTable , registerStatic - , unstatic - , unclosure + , unstatic + , unclosure ) where -import Data.Binary +import Data.Binary ( Binary(get, put) , Put , Get @@ -238,7 +238,7 @@ import qualified Data.Map as Map (lookup, empty, insert) import Control.Applicative ((<$>), (<*>)) import Control.Arrow as Arrow ((***), app) import Data.Rank1Dynamic (Dynamic, toDynamic, fromDynamic, dynApply) -import Data.Rank1Typeable +import Data.Rank1Typeable ( Typeable , typeOf , ANY1 @@ -258,7 +258,7 @@ data StaticLabel = deriving (Typeable, Show) -- | A static value. Static is opaque; see 'staticLabel' and 'staticApply'. -newtype Static a = Static StaticLabel +newtype Static a = Static StaticLabel deriving (Typeable, Show) instance Typeable a => Binary (Static a) where @@ -272,9 +272,9 @@ instance Typeable a => Binary (Static a) where -- We don't want StaticLabel to be its own Binary instance putStaticLabel :: StaticLabel -> Put -putStaticLabel (StaticLabel string) = - putWord8 0 >> put string -putStaticLabel (StaticApply label1 label2) = +putStaticLabel (StaticLabel string) = + putWord8 0 >> put string +putStaticLabel (StaticApply label1 label2) = putWord8 1 >> putStaticLabel label1 >> putStaticLabel label2 getStaticLabel :: Get StaticLabel @@ -283,14 +283,14 @@ getStaticLabel = do case header of 0 -> StaticLabel <$> get 1 -> StaticApply <$> getStaticLabel <*> getStaticLabel - _ -> fail "StaticLabel.get: invalid" - + _ -> fail "StaticLabel.get: invalid" + -- | Create a primitive static value. --- +-- -- It is the responsibility of the client code to make sure the corresponding -- entry in the 'RemoteTable' has the appropriate type. staticLabel :: String -> Static a -staticLabel = Static . StaticLabel +staticLabel = Static . StaticLabel -- | Apply two static values staticApply :: Static (a -> b) -> Static a -> Static b @@ -300,12 +300,12 @@ staticApply (Static f) (Static x) = Static (StaticApply f x) -- Eliminating static values -- -------------------------------------------------------------------------------- --- | Runtime dictionary for 'unstatic' lookups +-- | Runtime dictionary for 'unstatic' lookups newtype RemoteTable = RemoteTable (Map String Dynamic) -- | Initial remote table initRemoteTable :: RemoteTable -initRemoteTable = +initRemoteTable = registerStatic "$compose" (toDynamic ((.) :: (ANY2 -> ANY3) -> (ANY1 -> ANY2) -> ANY1 -> ANY3)) . registerStatic "$const" (toDynamic (const :: ANY1 -> ANY2 -> ANY1)) . registerStatic "$split" (toDynamic ((***) :: (ANY1 -> ANY3) -> (ANY2 -> ANY4) -> (ANY1, ANY2) -> (ANY3, ANY4))) @@ -321,23 +321,23 @@ registerStatic label dyn (RemoteTable rtable) -- Pseudo-type: RemoteTable -> Static a -> a resolveStaticLabel :: RemoteTable -> StaticLabel -> Either String Dynamic -resolveStaticLabel (RemoteTable rtable) (StaticLabel label) = +resolveStaticLabel (RemoteTable rtable) (StaticLabel label) = case Map.lookup label rtable of Nothing -> Left $ "Invalid static label '" ++ label ++ "'" Just d -> Right d resolveStaticLabel rtable (StaticApply label1 label2) = do - f <- resolveStaticLabel rtable label1 + f <- resolveStaticLabel rtable label1 x <- resolveStaticLabel rtable label2 f `dynApply` x -- | Resolve a static value unstatic :: Typeable a => RemoteTable -> Static a -> Either String a -unstatic rtable (Static static) = do - dyn <- resolveStaticLabel rtable static +unstatic rtable (Static static) = do + dyn <- resolveStaticLabel rtable static fromDynamic dyn -------------------------------------------------------------------------------- --- Closures -- +-- Closures -- -------------------------------------------------------------------------------- -- | A closure is a static value and an encoded environment @@ -346,16 +346,16 @@ data Closure a = Closure (Static (ByteString -> a)) ByteString instance Typeable a => Binary (Closure a) where put (Closure static env) = put static >> put env - get = Closure <$> get <*> get + get = Closure <$> get <*> get closure :: Static (ByteString -> a) -- ^ Decoder -> ByteString -- ^ Encoded closure environment -> Closure a -closure = Closure +closure = Closure -- | Resolve a closure unclosure :: Typeable a => RemoteTable -> Closure a -> Either String a -unclosure rtable (Closure static env) = do +unclosure rtable (Closure static env) = do f <- unstatic rtable static return (f env) @@ -368,18 +368,18 @@ staticClosure static = closure (staticConst static) empty -------------------------------------------------------------------------------- -- | Static version of ('Prelude..') -composeStatic :: (Typeable a, Typeable b, Typeable c) +composeStatic :: (Typeable a, Typeable b, Typeable c) => Static ((b -> c) -> (a -> b) -> a -> c) -composeStatic = staticLabel "$compose" +composeStatic = staticLabel "$compose" -- | Static version of 'const' constStatic :: (Typeable a, Typeable b) => Static (a -> b -> a) -constStatic = staticLabel "$const" +constStatic = staticLabel "$const" -- | Static version of ('Arrow.***') -splitStatic :: (Typeable a, Typeable a', Typeable b, Typeable b') - => Static ((a -> b) -> (a' -> b') -> (a, a') -> (b, b')) +splitStatic :: (Typeable a, Typeable a', Typeable b, Typeable b') + => Static ((a -> b) -> (a' -> b') -> (a, a') -> (b, b')) splitStatic = staticLabel "$split" -- | Static version of 'Arrow.app' @@ -390,31 +390,31 @@ appStatic = staticLabel "$app" -- | Static version of 'flip' flipStatic :: (Typeable a, Typeable b, Typeable c) => Static ((a -> b -> c) -> b -> a -> c) -flipStatic = staticLabel "$flip" +flipStatic = staticLabel "$flip" -------------------------------------------------------------------------------- -- Combinators on static values -- -------------------------------------------------------------------------------- --- | Static version of ('Prelude..') +-- | Static version of ('Prelude..') staticCompose :: (Typeable a, Typeable b, Typeable c) => Static (b -> c) -> Static (a -> b) -> Static (a -> c) staticCompose g f = composeStatic `staticApply` g `staticApply` f -- | Static version of ('Control.Arrow.***') -staticSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') +staticSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') => Static (a -> b) -> Static (a' -> b') -> Static ((a, a') -> (b, b')) staticSplit f g = splitStatic `staticApply` f `staticApply` g -- | Static version of 'Prelude.const' staticConst :: (Typeable a, Typeable b) => Static a -> Static (b -> a) -staticConst x = constStatic `staticApply` x +staticConst x = constStatic `staticApply` x -- | Static version of 'Prelude.flip' staticFlip :: (Typeable a, Typeable b, Typeable c) => Static (a -> b -> c) -> Static (b -> a -> c) -staticFlip f = flipStatic `staticApply` f +staticFlip f = flipStatic `staticApply` f -------------------------------------------------------------------------------- -- Combinators on Closures -- @@ -423,7 +423,7 @@ staticFlip f = flipStatic `staticApply` f -- | Apply a static function to a closure closureApplyStatic :: (Typeable a, Typeable b) => Static (a -> b) -> Closure a -> Closure b -closureApplyStatic f (Closure decoder env) = +closureApplyStatic f (Closure decoder env) = closure (f `staticCompose` decoder) env decodeEnvPairStatic :: Static (ByteString -> (ByteString, ByteString)) @@ -432,15 +432,15 @@ decodeEnvPairStatic = staticLabel "$decodeEnvPair" -- | Closure application closureApply :: forall a b. (Typeable a, Typeable b) => Closure (a -> b) -> Closure a -> Closure b -closureApply (Closure fdec fenv) (Closure xdec xenv) = +closureApply (Closure fdec fenv) (Closure xdec xenv) = closure decoder (encode (fenv, xenv)) where decoder :: Static (ByteString -> b) - decoder = appStatic + decoder = appStatic `staticCompose` (fdec `staticSplit` xdec) `staticCompose` - decodeEnvPairStatic + decodeEnvPairStatic -- | Closure composition closureCompose :: (Typeable a, Typeable b, Typeable c) @@ -448,6 +448,6 @@ closureCompose :: (Typeable a, Typeable b, Typeable c) closureCompose g f = composeStatic `closureApplyStatic` g `closureApply` f -- | Closure version of ('Arrow.***') -closureSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') +closureSplit :: (Typeable a, Typeable a', Typeable b, Typeable b') => Closure (a -> b) -> Closure (a' -> b') -> Closure ((a, a') -> (b, b')) -closureSplit f g = splitStatic `closureApplyStatic` f `closureApply` g +closureSplit f g = splitStatic `closureApplyStatic` f `closureApply` g From 20d791738943a7264d298d5fd41ae911776fce54 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0313/2357] Remove trailing whitespace --- src/Network/Transport.hs | 72 +++++++++++++++---------------- src/Network/Transport/Internal.hs | 52 +++++++++++----------- src/Network/Transport/Util.hs | 10 ++--- tests/chat/ChatClient.hs | 26 +++++------ tests/chat/ChatServer.hs | 2 +- tests/sumeuler/SumEulerMaster.hs | 8 ++-- tests/sumeuler/SumEulerWorker.hs | 8 ++-- 7 files changed, 89 insertions(+), 89 deletions(-) diff --git a/src/Network/Transport.hs b/src/Network/Transport.hs index ef39fd9e..a79063e6 100644 --- a/src/Network/Transport.hs +++ b/src/Network/Transport.hs @@ -1,5 +1,5 @@ --- | Network Transport -module Network.Transport +-- | Network Transport +module Network.Transport ( -- * Types Transport(..) , EndPoint(..) @@ -41,8 +41,8 @@ import Data.Word (Word64) data Transport = Transport { -- | Create a new end point (heavyweight operation) newEndPoint :: IO (Either (TransportError NewEndPointErrorCode) EndPoint) - -- | Shutdown the transport completely - , closeTransport :: IO () + -- | Shutdown the transport completely + , closeTransport :: IO () } -- | Network endpoint. @@ -50,8 +50,8 @@ data EndPoint = EndPoint { -- | Endpoints have a single shared receive queue. receive :: IO Event -- | EndPointAddress of the endpoint. - , address :: EndPointAddress - -- | Create a new lightweight connection. + , address :: EndPointAddress + -- | Create a new lightweight connection. -- -- 'connect' should be as asynchronous as possible; for instance, in -- Transport implementations based on some heavy-weight underlying network @@ -64,12 +64,12 @@ data EndPoint = EndPoint { , resolveMulticastGroup :: MulticastAddress -> IO (Either (TransportError ResolveMulticastGroupErrorCode) MulticastGroup) -- | Close the endpoint , closeEndPoint :: IO () - } + } -- | Lightweight connection to an endpoint. data Connection = Connection { -- | Send a message on this connection. - -- + -- -- 'send' provides vectored I/O, and allows multiple data segments to be -- sent using a single call (cf. 'Network.Socket.ByteString.sendMany'). -- Note that this segment structure is entirely unrelated to the segment @@ -80,7 +80,7 @@ data Connection = Connection { } -- | Event on an endpoint. -data Event = +data Event = -- | Received a message Received {-# UNPACK #-} !ConnectionId [ByteString] -- | Connection closed @@ -88,33 +88,33 @@ data Event = -- | Connection opened -- -- 'ConnectionId's need not be allocated contiguously. - | ConnectionOpened {-# UNPACK #-} !ConnectionId Reliability EndPointAddress + | ConnectionOpened {-# UNPACK #-} !ConnectionId Reliability EndPointAddress -- | Received multicast | ReceivedMulticast MulticastAddress [ByteString] -- | The endpoint got closed (manually, by a call to closeEndPoint or closeTransport) | EndPointClosed - -- | An error occurred - | ErrorEvent (TransportError EventErrorCode) + -- | An error occurred + | ErrorEvent (TransportError EventErrorCode) deriving (Show, Eq) -- | Connection data ConnectHintsIDs enable receivers to distinguish one connection from another. -type ConnectionId = Word64 +type ConnectionId = Word64 -- | Reliability guarantees of a connection. -data Reliability = - ReliableOrdered - | ReliableUnordered +data Reliability = + ReliableOrdered + | ReliableUnordered | Unreliable deriving (Show, Eq) -- | Multicast group. data MulticastGroup = MulticastGroup { - -- | EndPointAddress of the multicast group. + -- | EndPointAddress of the multicast group. multicastAddress :: MulticastAddress -- | Delete the multicast group completely. , deleteMulticastGroup :: IO () -- | Maximum message size that we can send to this group. - , maxMsgSize :: Maybe Int + , maxMsgSize :: Maybe Int -- | Send a message to the group. , multicastSend :: [ByteString] -> IO () -- | Subscribe to the given multicast group (to start receiving messages from the group). @@ -172,13 +172,13 @@ defaultConnectHints = ConnectHints { -------------------------------------------------------------------------------- -- | Errors returned by Network.Transport API functions consist of an error --- code and a human readable description of the problem +-- code and a human readable description of the problem data TransportError error = TransportError error String deriving (Show, Typeable) -- | Although the functions in the transport API never throw TransportErrors -- (but return them explicitly), application code may want to turn these into --- exceptions. +-- exceptions. instance (Typeable err, Show err) => Exception (TransportError err) -- | When comparing errors we ignore the human-readable strings @@ -190,19 +190,19 @@ data NewEndPointErrorCode = -- | Not enough resources NewEndPointInsufficientResources -- | Failed for some other reason - | NewEndPointFailed + | NewEndPointFailed deriving (Show, Typeable, Eq) --- | Connection failure -data ConnectErrorCode = - -- | Could not resolve the address +-- | Connection failure +data ConnectErrorCode = + -- | Could not resolve the address ConnectNotFound -- | Insufficient resources (for instance, no more sockets available) - | ConnectInsufficientResources + | ConnectInsufficientResources -- | Timeout | ConnectTimeout -- | Failed for other reasons (including syntax error) - | ConnectFailed + | ConnectFailed deriving (Show, Typeable, Eq) -- | Failure during the creation of a new multicast group @@ -221,7 +221,7 @@ data ResolveMulticastGroupErrorCode = ResolveMulticastGroupNotFound -- | Failed for some other reason (including syntax error) | ResolveMulticastGroupFailed - -- | Not all transport implementations support multicast + -- | Not all transport implementations support multicast | ResolveMulticastGroupUnsupported deriving (Show, Typeable, Eq) @@ -230,12 +230,12 @@ data SendErrorCode = -- | Connection was closed SendClosed -- | Send failed for some other reason - | SendFailed + | SendFailed deriving (Show, Typeable, Eq) -- | Error codes used when reporting errors to endpoints (through receive) -data EventErrorCode = - -- | Failure of the entire endpoint +data EventErrorCode = + -- | Failure of the entire endpoint EventEndPointFailed -- | Transport-wide fatal error | EventTransportFailed @@ -247,27 +247,27 @@ data EventErrorCode = -- both directions, must now be considered to have failed; they fail as a -- "bundle" of connections, with only a single "bundle" of connections per -- endpoint at any point in time. - -- + -- -- That is, suppose there are multiple connections in either direction -- between endpoints A and B, and A receives a notification that it has -- lost contact with B. Then A must not be able to send any further - -- messages to B on existing connections. + -- messages to B on existing connections. -- -- Although B may not realize /immediately/ that its connection to A has -- been broken, messages sent by B on existing connections should not be -- delivered, and B must eventually get an EventConnectionLost message, - -- too. + -- too. -- -- Moreover, this event must be posted before A has successfully -- reconnected (in other words, if B notices a reconnection attempt from A, -- it must post the EventConnectionLost before acknowledging the connection -- from A) so that B will not receive events about new connections or - -- incoming messages from A without realizing that it got disconnected. - -- + -- incoming messages from A without realizing that it got disconnected. + -- -- If B attempts to establish another connection to A before it realized -- that it got disconnected from A then it's okay for this connection -- attempt to fail, and the EventConnectionLost to be posted at that point, -- or for the EventConnectionLost to be posted and for the new connection -- to be considered the first connection of the "new bundle". - | EventConnectionLost EndPointAddress + | EventConnectionLost EndPointAddress deriving (Show, Typeable, Eq) diff --git a/src/Network/Transport/Internal.hs b/src/Network/Transport/Internal.hs index 076460ec..0da87e75 100644 --- a/src/Network/Transport/Internal.hs +++ b/src/Network/Transport/Internal.hs @@ -1,5 +1,5 @@ -- | Internal functions -module Network.Transport.Internal +module Network.Transport.Internal ( -- * Encoders/decoders encodeInt32 , decodeInt32 @@ -28,13 +28,13 @@ import Foreign.C (CInt(..), CShort(..)) import Foreign.ForeignPtr (withForeignPtr) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length) -import qualified Data.ByteString.Internal as BSI +import qualified Data.ByteString.Internal as BSI ( unsafeCreate , toForeignPtr , inlinePerformIO ) import Control.Monad.IO.Class (MonadIO, liftIO) -import Control.Exception +import Control.Exception ( IOException , SomeException , AsyncException @@ -56,36 +56,36 @@ foreign import ccall unsafe "ntohl" ntohl :: CInt -> CInt foreign import ccall unsafe "htons" htons :: CShort -> CShort foreign import ccall unsafe "ntohs" ntohs :: CShort -> CShort --- | Serialize 32-bit to network byte order +-- | Serialize 32-bit to network byte order encodeInt32 :: Enum a => a -> ByteString -encodeInt32 i32 = +encodeInt32 i32 = BSI.unsafeCreate 4 $ \p -> pokeByteOff p 0 (htonl . fromIntegral . fromEnum $ i32) --- | Deserialize 32-bit from network byte order +-- | Deserialize 32-bit from network byte order -- Throws an IO exception if this is not a valid integer. -decodeInt32 :: Num a => ByteString -> a -decodeInt32 bs - | BS.length bs /= 4 = throw $ userError "decodeInt32: Invalid length" - | otherwise = BSI.inlinePerformIO $ do - let (fp, offset, _) = BSI.toForeignPtr bs +decodeInt32 :: Num a => ByteString -> a +decodeInt32 bs + | BS.length bs /= 4 = throw $ userError "decodeInt32: Invalid length" + | otherwise = BSI.inlinePerformIO $ do + let (fp, offset, _) = BSI.toForeignPtr bs withForeignPtr fp $ \p -> do - w32 <- peekByteOff p offset + w32 <- peekByteOff p offset return (fromIntegral . ntohl $ w32) --- | Serialize 16-bit to network byte order -encodeInt16 :: Enum a => a -> ByteString -encodeInt16 i16 = +-- | Serialize 16-bit to network byte order +encodeInt16 :: Enum a => a -> ByteString +encodeInt16 i16 = BSI.unsafeCreate 2 $ \p -> pokeByteOff p 0 (htons . fromIntegral . fromEnum $ i16) --- | Deserialize 16-bit from network byte order +-- | Deserialize 16-bit from network byte order -- Throws an IO exception if this is not a valid integer decodeInt16 :: Num a => ByteString -> a -decodeInt16 bs - | BS.length bs /= 2 = throw $ userError "decodeInt16: Invalid length" +decodeInt16 bs + | BS.length bs /= 2 = throw $ userError "decodeInt16: Invalid length" | otherwise = BSI.inlinePerformIO $ do - let (fp, offset, _) = BSI.toForeignPtr bs + let (fp, offset, _) = BSI.toForeignPtr bs withForeignPtr fp $ \p -> do w16 <- peekByteOff p offset return (fromIntegral . ntohs $ w16) @@ -120,17 +120,17 @@ forkIOWithUnmask :: ((forall a . IO a -> IO a) -> IO ()) -> IO ThreadId forkIOWithUnmask io = forkIO (io unsafeUnmask) -- | Safe version of 'toEnum' -tryToEnum :: (Enum a, Bounded a) => Int -> Maybe a +tryToEnum :: (Enum a, Bounded a) => Int -> Maybe a tryToEnum = go minBound maxBound where go :: Enum b => b -> b -> Int -> Maybe b - go lo hi n = if fromEnum lo <= n && n <= fromEnum hi then Just (toEnum n) else Nothing + go lo hi n = if fromEnum lo <= n && n <= fromEnum hi then Just (toEnum n) else Nothing -- | If the timeout value is not Nothing, wrap the given computation with a -- timeout and it if times out throw the specified exception. Identity -- otherwise. timeoutMaybe :: Exception e => Maybe Int -> e -> IO a -> IO a -timeoutMaybe Nothing _ f = f +timeoutMaybe Nothing _ f = f timeoutMaybe (Just n) e f = do ma <- timeout n f case ma of @@ -138,19 +138,19 @@ timeoutMaybe (Just n) e f = do Just a -> return a -- | @asyncWhenCancelled g f@ runs f in a separate thread and waits for it --- to complete. If f throws an exception we catch it and rethrow it in the +-- to complete. If f throws an exception we catch it and rethrow it in the -- current thread. If the current thread is interrupted before f completes, -- we run the specified clean up handler (if f throws an exception we assume -- that no cleanup is necessary). asyncWhenCancelled :: forall a. (a -> IO ()) -> IO a -> IO a asyncWhenCancelled g f = mask_ $ do mvar <- newEmptyMVar - forkIO $ try f >>= putMVar mvar + forkIO $ try f >>= putMVar mvar -- takeMVar is interruptible (even inside a mask_) catch (takeMVar mvar) (exceptionHandler mvar) >>= either throwIO return where - exceptionHandler :: MVar (Either SomeException a) - -> AsyncException + exceptionHandler :: MVar (Either SomeException a) + -> AsyncException -> IO (Either SomeException a) exceptionHandler mvar ex = do forkIO $ takeMVar mvar >>= either (const $ return ()) g diff --git a/src/Network/Transport/Util.hs b/src/Network/Transport/Util.hs index e247c2b2..243c6e2d 100644 --- a/src/Network/Transport/Util.hs +++ b/src/Network/Transport/Util.hs @@ -1,9 +1,9 @@ --- | Utility functions --- +-- | Utility functions +-- -- Note: this module is bound to change even more than the rest of the API :) module Network.Transport.Util (spawn) where -import Network.Transport +import Network.Transport ( Transport , EndPoint(..) , EndPointAddress @@ -14,9 +14,9 @@ import Control.Concurrent (forkIO) import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) -- | Fork a new thread, create a new end point on that thread, and run the specified IO operation on that thread. --- +-- -- Returns the address of the new end point. -spawn :: Transport -> (EndPoint -> IO ()) -> IO EndPointAddress +spawn :: Transport -> (EndPoint -> IO ()) -> IO EndPointAddress spawn transport proc = do addrMVar <- newEmptyMVar forkIO $ do diff --git a/tests/chat/ChatClient.hs b/tests/chat/ChatClient.hs index 527af993..69a97468 100644 --- a/tests/chat/ChatClient.hs +++ b/tests/chat/ChatClient.hs @@ -12,7 +12,7 @@ import qualified Data.Map as Map (fromList, elems, insert, member, empty, size, chatClient :: MVar () -> EndPoint -> EndPointAddress -> IO () chatClient done endpoint serverAddr = do connect endpoint serverAddr ReliableOrdered - cOut <- getPeers >>= connectToPeers + cOut <- getPeers >>= connectToPeers cIn <- newMVar Map.empty -- Listen for incoming messages @@ -20,11 +20,11 @@ chatClient done endpoint serverAddr = do event <- receive endpoint case event of Received _ msg -> - putStrLn . BSC.unpack . BS.concat $ msg + putStrLn . BSC.unpack . BS.concat $ msg ConnectionOpened cid _ addr -> do modifyMVar_ cIn $ return . Map.insert cid addr didAdd <- modifyMVar cOut $ \conns -> - if not (Map.member addr conns) + if not (Map.member addr conns) then do Right conn <- connect endpoint addr ReliableOrdered return (Map.insert addr conn conns, True) @@ -43,24 +43,24 @@ chatClient done endpoint serverAddr = do {- chatState <- newMVar (Map.fromList peerConns) - + -- Thread to listen to incoming messages forkIO . forever $ do - event <- receive endpoint + event <- receive endpoint case event of ConnectionOpened _ _ (EndPointAddress addr) -> do - modifyMVar_ chatState $ \peers -> + modifyMVar_ chatState $ \peers -> if not (Map.member addr peers) then do - Right conn <- connect endpoint (EndPointAddress addr) ReliableOrdered + Right conn <- connect endpoint (EndPointAddress addr) ReliableOrdered return (Map.insert addr conn peers) else return peers Received _ msg -> - putStrLn . BSC.unpack . BS.concat $ msg + putStrLn . BSC.unpack . BS.concat $ msg ConnectionClosed _ -> return () - + -} -- Thread to interact with the user showNumPeers cOut @@ -74,7 +74,7 @@ chatClient done endpoint serverAddr = do where getPeers :: IO [EndPointAddress] - getPeers = do + getPeers = do ConnectionOpened _ _ _ <- receive endpoint Received _ msg <- receive endpoint ConnectionClosed _ <- receive endpoint @@ -86,11 +86,11 @@ chatClient done endpoint serverAddr = do Right conn <- connect endpoint addr ReliableOrdered return (addr, conn) newMVar (Map.fromList conns) - + showNumPeers :: MVar (Map EndPointAddress Connection) -> IO () - showNumPeers cOut = + showNumPeers cOut = readMVar cOut >>= \conns -> putStrLn $ "# " ++ show (Map.size conns) ++ " peers" - + diff --git a/tests/chat/ChatServer.hs b/tests/chat/ChatServer.hs index 6c63d8b8..7c36df00 100644 --- a/tests/chat/ChatServer.hs +++ b/tests/chat/ChatServer.hs @@ -23,6 +23,6 @@ main = do Right conn <- connect endpoint addr ReliableOrdered send conn [BSC.pack . show . IntMap.elems $ clients] close conn - modify $ IntMap.insert cid (endPointAddressToByteString addr) + modify $ IntMap.insert cid (endPointAddressToByteString addr) ConnectionClosed cid -> modify $ IntMap.delete cid diff --git a/tests/sumeuler/SumEulerMaster.hs b/tests/sumeuler/SumEulerMaster.hs index 8159a47b..45e921c6 100644 --- a/tests/sumeuler/SumEulerMaster.hs +++ b/tests/sumeuler/SumEulerMaster.hs @@ -11,7 +11,7 @@ import Control.Monad.IO.Class (liftIO) master :: MVar () -> EndPoint -> [String] -> IO () master done endpoint workers = do - conns <- forM workers $ \worker -> do + conns <- forM workers $ \worker -> do Right conn <- connect endpoint (EndPointAddress $ BSC.pack worker) ReliableOrdered return conn -- Send out requests @@ -24,10 +24,10 @@ master done endpoint workers = do case event of Received _ msg -> tell [read . BSC.unpack . BS.concat $ msg] - _ -> + _ -> return () putStrLn $ "Replies: " ++ show (replies :: [Int]) - putMVar done () + putMVar done () main :: IO () main = do @@ -41,4 +41,4 @@ main = do forkIO $ master masterDone endpoint workers takeMVar masterDone - + diff --git a/tests/sumeuler/SumEulerWorker.hs b/tests/sumeuler/SumEulerWorker.hs index 071eb0ab..d65b8a60 100644 --- a/tests/sumeuler/SumEulerWorker.hs +++ b/tests/sumeuler/SumEulerWorker.hs @@ -20,15 +20,15 @@ sumEuler :: Int -> Int sumEuler = sum . (map euler) . mkList worker :: String -> MVar () -> EndPoint -> IO () -worker id done endpoint = do - ConnectionOpened _ _ theirAddr <- receive endpoint +worker id done endpoint = do + ConnectionOpened _ _ theirAddr <- receive endpoint Right replyChan <- connect endpoint theirAddr ReliableOrdered go replyChan where go replyChan = do event <- receive endpoint case event of - ConnectionClosed _ -> do + ConnectionClosed _ -> do close replyChan putMVar done () Received _ msg -> do @@ -39,7 +39,7 @@ worker id done endpoint = do main :: IO () main = do - (id:host:port:_) <- getArgs + (id:host:port:_) <- getArgs Right transport <- createTransport host port Right endpoint <- newEndPoint transport workerDone <- newEmptyMVar From 7da7afeb525e97b83eaf044ec8af0d57fad190e8 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Mon, 3 Dec 2012 14:38:21 +0000 Subject: [PATCH 0314/2357] Remove trailing whitespace --- demos/Echo.hs | 16 +- demos/Fib.hs | 16 +- demos/Ping.hs | 42 +-- .../Distributed/Process/Backend/Azure.hs | 348 +++++++++--------- 4 files changed, 211 insertions(+), 211 deletions(-) diff --git a/demos/Echo.hs b/demos/Echo.hs index 0790fe71..6aded942 100644 --- a/demos/Echo.hs +++ b/demos/Echo.hs @@ -5,12 +5,12 @@ import System.Environment (getArgs) import Control.Monad (unless, forever) import Control.Monad.IO.Class (liftIO) import Control.Distributed.Process (Process, expect) -import Control.Distributed.Process.Closure (remotable, mkClosure) -import Control.Distributed.Process.Backend.Azure +import Control.Distributed.Process.Closure (remotable, mkClosure) +import Control.Distributed.Process.Backend.Azure echoRemote :: () -> Backend -> Process () echoRemote () _backend = forever $ do - str <- expect + str <- expect remoteSend (str :: String) remotable ['echoRemote] @@ -29,21 +29,21 @@ main :: IO () main = do args <- getArgs case args of - "onvm":args' -> + "onvm":args' -> -- Pass execution to 'onVmMain' if we are running on the VM -- ('callOnVM' will provide the right arguments) onVmMain __remoteTable args' sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do -- Initialize the Azure backend - params <- defaultAzureParameters sid x509 pkey + params <- defaultAzureParameters sid x509 pkey let params' = params { azureSshUserName = user } backend <- initializeBackend params' cloudService -- Find the specified virtual machine Just vm <- findNamedVM backend virtualMachine - + -- Run the echo client proper callOnVM backend vm port $ - ProcessPair ($(mkClosure 'echoRemote) ()) - echoLocal + ProcessPair ($(mkClosure 'echoRemote) ()) + echoLocal diff --git a/demos/Fib.hs b/demos/Fib.hs index c03e748a..1e25ee04 100644 --- a/demos/Fib.hs +++ b/demos/Fib.hs @@ -13,8 +13,8 @@ import Control.Distributed.Process , receiveChan , spawnLocal ) -import Control.Distributed.Process.Backend.Azure -import Control.Distributed.Process.Closure +import Control.Distributed.Process.Backend.Azure +import Control.Distributed.Process.Closure ( remotable , remotableDecl , mkClosure @@ -25,7 +25,7 @@ randomElement xs = do ix <- randomIO return (xs !! (ix `mod` length xs)) -remotableDecl [ +remotableDecl [ [d| dfib :: ([NodeId], SendPort Integer, Integer) -> Process () ; dfib (_, reply, 0) = sendChan reply 0 dfib (_, reply, 1) = sendChan reply 1 @@ -54,7 +54,7 @@ remotable ['remoteFib] printResult :: LocalProcess () printResult = do result <- localExpect :: LocalProcess Integer - liftIO $ print result + liftIO $ print result main :: IO () main = do @@ -62,13 +62,13 @@ main = do case args of "onvm":args' -> onVmMain (__remoteTable . __remoteTableDecl) args' [sid, x509, pkey, user, cloudService, n] -> do - params <- defaultAzureParameters sid x509 pkey + params <- defaultAzureParameters sid x509 pkey let params' = params { azureSshUserName = user } backend <- initializeBackend params' cloudService vms <- findVMs backend nids <- forM vms $ \vm -> spawnNodeOnVM backend vm "8080" - callOnVM backend (head vms) "8081" $ - ProcessPair ($(mkClosure 'remoteFib) (nids, read n :: Integer)) - printResult + callOnVM backend (head vms) "8081" $ + ProcessPair ($(mkClosure 'remoteFib) (nids, read n :: Integer)) + printResult _ -> error "Invalid command line arguments" diff --git a/demos/Ping.hs b/demos/Ping.hs index 798c7e55..1e4060ea 100644 --- a/demos/Ping.hs +++ b/demos/Ping.hs @@ -5,7 +5,7 @@ import Data.Binary (encode, decode) import Control.Monad (forever) import Control.Monad.IO.Class (liftIO) import Control.Exception (try, IOException) -import Control.Distributed.Process +import Control.Distributed.Process ( Process , getSelfPid , expect @@ -15,30 +15,30 @@ import Control.Distributed.Process , match , ProcessMonitorNotification(..) ) -import Control.Distributed.Process.Closure (remotable, mkClosure) -import Control.Distributed.Process.Backend.Azure -import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) +import Control.Distributed.Process.Closure (remotable, mkClosure) +import Control.Distributed.Process.Backend.Azure +import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) pingServer :: () -> Backend -> Process () pingServer () _backend = do us <- getSelfPid liftIO $ BSL.writeFile "pingServer.pid" (encode us) - forever $ do + forever $ do them <- expect send them () -pingClientRemote :: () -> Backend -> Process () +pingClientRemote :: () -> Backend -> Process () pingClientRemote () _backend = do mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") case mPingServerEnc of - Left err -> + Left err -> remoteSend $ "Ping server not found: " ++ show (err :: IOException) - Right pingServerEnc -> do + Right pingServerEnc -> do let pingServerPid = decode pingServerEnc pid <- getSelfPid - _ref <- monitor pingServerPid + _ref <- monitor pingServerPid send pingServerPid pid - gotReply <- receiveWait + gotReply <- receiveWait [ match (\() -> return True) , match (\(ProcessMonitorNotification {}) -> return False) ] @@ -49,26 +49,26 @@ pingClientRemote () _backend = do remotable ['pingClientRemote, 'pingServer] pingClientLocal :: LocalProcess () -pingClientLocal = localExpect >>= liftIO . putStrLn +pingClientLocal = localExpect >>= liftIO . putStrLn main :: IO () main = do args <- getArgs case args of - "onvm":args' -> + "onvm":args' -> -- Pass execution to 'onVmMain' if we are running on the VM onVmMain __remoteTable args' "list":sid:x509:pkey:_ -> do -- List all available cloud services -- (useful, but not strictly necessary for the example) - params <- defaultAzureParameters sid x509 pkey + params <- defaultAzureParameters sid x509 pkey css <- cloudServices (azureSetup params) mapM_ print css cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do - -- Initialize the backend and find the right VM - params <- defaultAzureParameters sid x509 pkey + -- Initialize the backend and find the right VM + params <- defaultAzureParameters sid x509 pkey let params' = params { azureSshUserName = user } backend <- initializeBackend params' cloudService Just vm <- findNamedVM backend virtualMachine @@ -76,10 +76,10 @@ main = do -- The same binary can behave as the client or the server, -- depending on the command line arguments case cmd of - "server" -> do - pid <- spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) + "server" -> do + pid <- spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) putStrLn $ "Ping server started at " ++ show pid - "client" -> - callOnVM backend vm port $ - ProcessPair ($(mkClosure 'pingClientRemote) ()) - pingClientLocal + "client" -> + callOnVM backend vm port $ + ProcessPair ($(mkClosure 'pingClientRemote) ()) + pingClientLocal diff --git a/src/Control/Distributed/Process/Backend/Azure.hs b/src/Control/Distributed/Process/Backend/Azure.hs index 546a7d7c..d4bd54a2 100644 --- a/src/Control/Distributed/Process/Backend/Azure.hs +++ b/src/Control/Distributed/Process/Backend/Azure.hs @@ -3,17 +3,17 @@ -- Azure cloud service can talk to each other directly using standard Cloud -- Haskell primitives (using TCP/IP under the hood); to talk to the remote -- machines from your local machine you can use the primitives provided in this --- module (which use ssh under the hood). It looks something like +-- module (which use ssh under the hood). It looks something like -- -- > _ _ -- > ( ` )_ -- > ( ) `) Azure cloud service -- > (_ (_ . _) _) --- > +-- > -- > | -- > | ssh connection -- > | --- > +-- > -- > +---+ -- > | | Local machine -- > +---+ @@ -39,27 +39,27 @@ -- subsequent virtual machines to the first virtual machine, thereby implicitly -- setting up a Cloud Service. -- --- We have only tested Cloud Haskell with Linux based virtual machines; +-- We have only tested Cloud Haskell with Linux based virtual machines; -- Windows based virtual machines /might/ work, but you'll be entering -- uncharted territory. Cloud Haskell assumes that all nodes run the same --- binary code; hence, you must use the same OS on all virtual machines, +-- binary code; hence, you must use the same OS on all virtual machines, -- /as well as on your local machine/. We use Ubuntu Server 12.04 LTS for our --- tests (running on VirtualBox on our local machine). +-- tests (running on VirtualBox on our local machine). -- -- When you set up your virtual machine, you can pick an arbitrary virtual -- machine name; these names are for your own use only and do not need to be -- globally unique. Set a username and password; you should use the same -- username on all virtual machines. You should also upload an SSH key for --- authentication (see +-- authentication (see -- /Converting OpenSSH keys for use on Windows Azure Linux VM's/, -- , for -- information on how to convert a standard Linux @id_rsa.pub@ public key to -- X509 format suitable for Azure). For the first VM you create select -- /Standalone Virtual Machine/, and pick an appropriate DNS name. The DNS name -- /does/ have to be globally unique, and will also be the name of the Cloud --- Service. For subsequent virtual machines, select +-- Service. For subsequent virtual machines, select -- /Connect to Existing Virtual Machine/ instead and then select the first VM --- you created. +-- you created. -- -- Once your virtual machines have been set up, you have to make sure that the -- user you created when you created the VM can ssh from any virtual machine to @@ -91,24 +91,24 @@ -- -- And to extract the private key: -- --- > openssl pkcs12 -in credentials.pfx -nocerts -nodes | openssl rsa -out credentials.private +-- > openssl pkcs12 -in credentials.pfx -nocerts -nodes | openssl rsa -out credentials.private -- -- (@openssl pkcs12@ outputs the private key in PKCS#8 format (BEGIN PRIVATE -- KEY), but we need it in PKCS#1 format (BEGIN RSA PRIVATE KEY). --- +-- -- [Testing the Setup] -- -- Build and install the @distributed-process-azure@ package, making sure to -- pass the @build-demos@ flag to Cabal. --- +-- -- > cabal-dev install distributed-process-azure -f build-demos -- -- We can the @cloud-haskell-azure-ping@ demo to test our setup: --- +-- -- > cloud-haskell-azure-ping list \ -- > <> \ -- > /path/to/credentials.x509 \ --- > /path/to/credentials.private +-- > /path/to/credentials.private -- -- (you can find your subscription ID in the @.publishsettings@ file from the previous step). -- If everything went well, this will output something like @@ -157,7 +157,7 @@ -- @RemoteProcess ()@, starts the executable on the remote node, sets up a new -- Cloud Haskell node, and then runs the specified process. The Cloud Haskell -- node will be shut down when the given process terminates. 'RemoteProcess' is --- defined as +-- defined as -- -- > type RemoteProcess a = Closure (Backend -> Process a) -- @@ -173,7 +173,7 @@ -- because the remote process and the local process can communicate through a -- set of primitives provided in this module ('localSend', 'localExpect', and -- 'remoteSend' -- there is no 'remoteExpect'; instead the remote process can --- use the standard Cloud Haskell 'expect' primitive). +-- use the standard Cloud Haskell 'expect' primitive). -- -- [First Example: Echo] -- @@ -181,45 +181,45 @@ -- starts a new Cloud Haskell node on the specified remote virtual machine. It -- then repeatedly waits for input from the user on the local machine, sends -- this to the remote virtual machine which will echo it back, and wait for and --- show the echo. +-- show the echo. -- -- Before you can try it you will first need to copy the executable (for -- example, using scp, although the Azure backend also provides this natively -- in Haskell). Once that's done, you can run the demo as follows: -- --- > cloud-haskell-azure-echo \ --- > <> \ --- > /path/to/credentials.x509 \ --- > /path/to/credentials.private \ --- > <> \ --- > <> \ --- > <> \ --- > <> +-- > cloud-haskell-azure-echo \ +-- > <> \ +-- > /path/to/credentials.x509 \ +-- > /path/to/credentials.private \ +-- > <> \ +-- > <> \ +-- > <> \ +-- > <> -- > # Everything I type gets echoed back -- > Echo: Everything I type gets echoed back -- > # Until I enter a blank line -- > Echo: Until I enter a blank line --- > # +-- > # -- -- The full @echo@ demo is -- -- > {-# LANGUAGE TemplateHaskell #-} --- > +-- > -- > import System.IO (hFlush, stdout) -- > import System.Environment (getArgs) -- > import Control.Monad (unless, forever) -- > import Control.Monad.IO.Class (liftIO) -- > import Control.Distributed.Process (Process, expect) --- > import Control.Distributed.Process.Closure (remotable, mkClosure) --- > import Control.Distributed.Process.Backend.Azure --- > +-- > import Control.Distributed.Process.Closure (remotable, mkClosure) +-- > import Control.Distributed.Process.Backend.Azure +-- > -- > echoRemote :: () -> Backend -> Process () -- > echoRemote () _backend = forever $ do --- > str <- expect +-- > str <- expect -- > remoteSend (str :: String) --- > +-- > -- > remotable ['echoRemote] --- > +-- > -- > echoLocal :: LocalProcess () -- > echoLocal = do -- > str <- liftIO $ putStr "# " >> hFlush stdout >> getLine @@ -229,35 +229,35 @@ -- > echo <- localExpect -- > liftIO $ putStrLn echo -- > echoLocal --- > +-- > -- > main :: IO () -- > main = do -- > args <- getArgs -- > case args of --- > "onvm":args' -> +-- > "onvm":args' -> -- > -- Pass execution to 'onVmMain' if we are running on the VM -- > -- ('callOnVM' will provide the right arguments) -- > onVmMain __remoteTable args' -- > -- > sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do -- > -- Initialize the Azure backend --- > params <- defaultAzureParameters sid x509 pkey +-- > params <- defaultAzureParameters sid x509 pkey -- > let params' = params { azureSshUserName = user } -- > backend <- initializeBackend params' cloudService -- > -- > -- Find the specified virtual machine -- > Just vm <- findNamedVM backend virtualMachine -- > --- > -- Run the echo client proper +-- > -- Run the echo client proper -- > callOnVM backend vm port $ --- > ProcessPair ($(mkClosure 'echoRemote) ()) --- > echoLocal --- +-- > ProcessPair ($(mkClosure 'echoRemote) ()) +-- > echoLocal +-- -- The most important part of this code is the last three lines -- -- > callOnVM backend vm port $ --- > ProcessPair ($(mkClosure 'echoRemote) ()) --- > echoLocal +-- > ProcessPair ($(mkClosure 'echoRemote) ()) +-- > echoLocal -- -- 'callOnVM' creats a new Cloud Haskell node on the specified virtual machine, -- then runs @echoRemote@ on the remote machine and @echoLocal@ on the local @@ -269,19 +269,19 @@ -- 'callOnVM' and 'spawnOnVM'. It uses the latter to -- install a ping server which keeps running in the background; it uses the -- former to run a ping client which sends a request to the ping server and --- outputs the response. +-- outputs the response. -- -- As with the @echo@ demo, make sure to copy the executable to the remote server first. -- Once that is done, you can start a ping server on a virtual machine using -- -- > cloud-haskell-azure-ping server \ --- > <> \ --- > /path/to/credentials.x509 \ --- > /path/to/credentials.private \ +-- > <> \ +-- > /path/to/credentials.x509 \ +-- > /path/to/credentials.private \ -- > <> \ --- > <> \ --- > <> \ --- > <> +-- > <> \ +-- > <> \ +-- > <> -- -- As before, when we execute this on our local machine, it starts a new Cloud -- Haskell node on the specified remote virtual machine and then executes the @@ -294,11 +294,11 @@ -- > cloud-haskell-azure-ping client \ -- > <> \ -- > /path/to/credentials.x509 \ --- > /path/to/credentials.private \ +-- > /path/to/credentials.private \ -- > <> \ --- > <> \ --- > <> \ --- > <> +-- > <> \ +-- > <> \ +-- > <> -- > Ping server at pid://10.59.224.122:8080:0:2 ok -- -- Note that we must pass a different port number, because the client will run @@ -309,13 +309,13 @@ -- this case, through a PID file). -- -- > {-# LANGUAGE TemplateHaskell #-} --- > +-- > -- > import System.Environment (getArgs) -- > import Data.Binary (encode, decode) -- > import Control.Monad (void, forever) -- > import Control.Monad.IO.Class (liftIO) -- > import Control.Exception (try, IOException) --- > import Control.Distributed.Process +-- > import Control.Distributed.Process -- > ( Process -- > , getSelfPid -- > , expect @@ -325,72 +325,72 @@ -- > , match -- > , ProcessMonitorNotification(..) -- > ) --- > import Control.Distributed.Process.Closure (remotable, mkClosure) --- > import Control.Distributed.Process.Backend.Azure --- > import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) --- > +-- > import Control.Distributed.Process.Closure (remotable, mkClosure) +-- > import Control.Distributed.Process.Backend.Azure +-- > import qualified Data.ByteString.Lazy as BSL (readFile, writeFile) +-- > -- > pingServer :: () -> Backend -> Process () -- > pingServer () _backend = do -- > us <- getSelfPid -- > liftIO $ BSL.writeFile "pingServer.pid" (encode us) --- > forever $ do +-- > forever $ do -- > them <- expect -- > send them () --- > --- > pingClientRemote :: () -> Backend -> Process () +-- > +-- > pingClientRemote :: () -> Backend -> Process () -- > pingClientRemote () _backend = do -- > mPingServerEnc <- liftIO $ try (BSL.readFile "pingServer.pid") -- > case mPingServerEnc of --- > Left err -> +-- > Left err -> -- > remoteSend $ "Ping server not found: " ++ show (err :: IOException) --- > Right pingServerEnc -> do +-- > Right pingServerEnc -> do -- > let pingServerPid = decode pingServerEnc -- > pid <- getSelfPid --- > _ref <- monitor pingServerPid +-- > _ref <- monitor pingServerPid -- > send pingServerPid pid --- > gotReply <- receiveWait +-- > gotReply <- receiveWait -- > [ match (\() -> return True) -- > , match (\(ProcessMonitorNotification {}) -> return False) -- > ] -- > if gotReply -- > then remoteSend $ "Ping server at " ++ show pingServerPid ++ " ok" -- > else remoteSend $ "Ping server at " ++ show pingServerPid ++ " failure" --- > +-- > -- > remotable ['pingClientRemote, 'pingServer] --- > +-- > -- > pingClientLocal :: LocalProcess () --- > pingClientLocal = localExpect >>= liftIO . putStrLn --- > +-- > pingClientLocal = localExpect >>= liftIO . putStrLn +-- > -- > main :: IO () -- > main = do -- > args <- getArgs -- > case args of --- > "onvm":args' -> +-- > "onvm":args' -> -- > -- Pass execution to 'onVmMain' if we are running on the VM -- > onVmMain __remoteTable args' -- > -- > "list":sid:x509:pkey:_ -> do -- > -- List all available cloud services -- > -- (useful, but not strictly necessary for the example) --- > params <- defaultAzureParameters sid x509 pkey +-- > params <- defaultAzureParameters sid x509 pkey -- > css <- cloudServices (azureSetup params) -- > mapM_ print css -- > -- > cmd:sid:x509:pkey:user:cloudService:virtualMachine:port:_ -> do -- > -- Initialize the backend and find the right VM --- > params <- defaultAzureParameters sid x509 pkey +-- > params <- defaultAzureParameters sid x509 pkey -- > let params' = params { azureSshUserName = user } -- > backend <- initializeBackend params' cloudService -- > Just vm <- findNamedVM backend virtualMachine --- > --- > -- The same binary can behave as the client or the server, +-- > +-- > -- The same binary can behave as the client or the server, -- > -- depending on the command line arguments -- > case cmd of --- > "server" -> void $ spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) --- > "client" -> callOnVM backend vm port $ --- > ProcessPair ($(mkClosure 'pingClientRemote) ()) --- > pingClientLocal -module Control.Distributed.Process.Backend.Azure +-- > "server" -> void $ spawnOnVM backend vm port ($(mkClosure 'pingServer) ()) +-- > "client" -> callOnVM backend vm port $ +-- > ProcessPair ($(mkClosure 'pingClientRemote) ()) +-- > pingClientLocal +module Control.Distributed.Process.Backend.Azure ( -- * Initialization Backend(..) , AzureParameters(..) @@ -422,7 +422,7 @@ import Prelude hiding (catch) import System.Environment (getEnv) import System.FilePath ((), takeFileName) import System.Environment.Executable (getExecutablePath) -import System.IO +import System.IO ( stdout , hFlush , hSetBinaryMode @@ -436,7 +436,7 @@ import qualified System.Posix.Process as Posix (forkProcess, createSession) import Data.Maybe (listToMaybe) import Data.Binary (Binary(get, put), encode, decode, getWord8, putWord8) import Data.Digest.Pure.MD5 (md5, MD5Digest) -import qualified Data.ByteString as BSS +import qualified Data.ByteString as BSS ( ByteString , length , concat @@ -444,7 +444,7 @@ import qualified Data.ByteString as BSS , hGet ) import qualified Data.ByteString.Char8 as BSSC (pack) -import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy as BSL ( ByteString , readFile , length @@ -458,7 +458,7 @@ import Data.Foldable (forM_) import Control.Applicative ((<$>), (<*>)) import Control.Monad (void, when) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask) -import Control.Exception +import Control.Exception ( Exception , catches , Handler(Handler) @@ -469,17 +469,17 @@ import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Concurrent.MVar (MVar, newEmptyMVar, readMVar, putMVar) -- Azure -import Network.Azure.ServiceManagement +import Network.Azure.ServiceManagement ( CloudService(..) , VirtualMachine(..) , Endpoint(..) , AzureSetup ) import qualified Network.Azure.ServiceManagement as Azure - ( cloudServices + ( cloudServices , azureSetup , vmSshEndpoint - ) + ) -- SSH import qualified Network.SSH.Client.LibSSH2 as SSH @@ -505,7 +505,7 @@ import qualified Network.SSH.Client.LibSSH2.Errors as SSH ) -- CH -import Control.Distributed.Process +import Control.Distributed.Process ( Process , Closure , RemoteTable @@ -520,7 +520,7 @@ import Control.Distributed.Process , nsendRemote ) import Control.Distributed.Process.Serializable (Serializable) -import qualified Control.Distributed.Process.Internal.Types as CH +import qualified Control.Distributed.Process.Internal.Types as CH ( LocalNode , LocalProcess(processQueue) , Message @@ -528,7 +528,7 @@ import qualified Control.Distributed.Process.Internal.Types as CH , messageToPayload , createMessage ) -import Control.Distributed.Process.Node +import Control.Distributed.Process.Node ( runProcess , forkProcess , newLocalNode @@ -539,7 +539,7 @@ import Network.Transport.TCP (createTransport, defaultTCPParameters) import Network.Transport.Internal (encodeInt32, decodeInt32, prependLength) -- Static -import Control.Distributed.Static +import Control.Distributed.Static ( Static , registerStatic , staticClosure @@ -552,18 +552,18 @@ data Backend = Backend { -- | Find virtual machines findVMs :: IO [VirtualMachine] -- | Copy the executable to a virtual machine - , copyToVM :: VirtualMachine -> IO () + , copyToVM :: VirtualMachine -> IO () -- | Check the MD5 hash of the remote executable - , checkMD5 :: VirtualMachine -> IO Bool + , checkMD5 :: VirtualMachine -> IO Bool -- | @runOnVM vm port pp@ starts a new CH node on machine @vm@ and then -- runs the specified process pair. The CH node will shut down when the -- /local/ process exists. @callOnVM@ returns the returned by the local -- process on exit. - , callOnVM :: forall a. VirtualMachine -> String -> ProcessPair a -> IO a + , callOnVM :: forall a. VirtualMachine -> String -> ProcessPair a -> IO a -- | Create a new CH node and run the specified process. -- The CH node will shut down when the /remote/ process exists. @spawnOnVM@ -- returns as soon as the process has been spawned. - , spawnOnVM :: VirtualMachine -> String -> RemoteProcess () -> IO ProcessId + , spawnOnVM :: VirtualMachine -> String -> RemoteProcess () -> IO ProcessId } deriving (Typeable) -- | Azure connection parameters @@ -588,7 +588,7 @@ instance Binary AzureParameters where put (azureSshKnownHosts params) put (azureSshRemotePath params) put (azureSshLocalPath params) - get = + get = AzureParameters <$> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get -- | Create default azure parameters @@ -600,9 +600,9 @@ defaultAzureParameters sid x509 pkey = do home <- getEnv "HOME" user <- getEnv "USER" self <- getExecutablePath - setup <- Azure.azureSetup sid x509 pkey - return AzureParameters - { azureSetup = setup + setup <- Azure.azureSetup sid x509 pkey + return AzureParameters + { azureSetup = setup , azureSshUserName = user , azureSshPublicKey = home ".ssh" "id_rsa.pub" , azureSshPrivateKey = home ".ssh" "id_rsa" @@ -616,10 +616,10 @@ defaultAzureParameters sid x509 pkey = do initializeBackend :: AzureParameters -- ^ Connection parameters -> String -- ^ Cloud service name -> IO Backend -initializeBackend params cloudService = +initializeBackend params cloudService = return Backend { - findVMs = apiFindVMs params cloudService - , copyToVM = apiCopyToVM params + findVMs = apiFindVMs params cloudService + , copyToVM = apiCopyToVM params , checkMD5 = apiCheckMD5 params , callOnVM = apiCallOnVM params cloudService , spawnOnVM = apiSpawnOnVM params cloudService @@ -628,81 +628,81 @@ initializeBackend params cloudService = -- | Find virtual machines apiFindVMs :: AzureParameters -> String -> IO [VirtualMachine] apiFindVMs params cloudService = do - css <- Azure.cloudServices (azureSetup params) + css <- Azure.cloudServices (azureSetup params) case filter ((== cloudService) . cloudServiceName) css of [cs] -> return $ cloudServiceVMs cs _ -> return [] -- | Start a CH node on the given virtual machine apiCopyToVM :: AzureParameters -> VirtualMachine -> IO () -apiCopyToVM params vm = +apiCopyToVM params vm = void . withSSH2 params vm $ \s -> catchSshError s $ SSH.scpSendFile s 0o700 (azureSshLocalPath params) (azureSshRemotePath params) --- | Call a process on a VM -apiCallOnVM :: AzureParameters +-- | Call a process on a VM +apiCallOnVM :: AzureParameters + -> String + -> VirtualMachine -> String - -> VirtualMachine - -> String -> ProcessPair a -> IO a apiCallOnVM = runOnVM False -apiSpawnOnVM :: AzureParameters +apiSpawnOnVM :: AzureParameters + -> String + -> VirtualMachine -> String - -> VirtualMachine - -> String - -> Closure (Backend -> Process ()) - -> IO ProcessId -apiSpawnOnVM params cloudService vm port rproc = - runOnVM True params cloudService vm port $ + -> Closure (Backend -> Process ()) + -> IO ProcessId +apiSpawnOnVM params cloudService vm port rproc = + runOnVM True params cloudService vm port $ ProcessPair rproc localExpect - + -- | Internal generalization of 'spawnOnVM' and 'callOnVM' -runOnVM :: Bool +runOnVM :: Bool -> AzureParameters -> String -> VirtualMachine -> String -> ProcessPair a -> IO a -runOnVM bg params cloudService vm port ppair = +runOnVM bg params cloudService vm port ppair = withSSH2 params vm $ \s -> do -- TODO: reduce duplication with apiCallOnVM - let exe = "PATH=. " ++ azureSshRemotePath params + let exe = "PATH=. " ++ azureSshRemotePath params ++ " onvm" - ++ " " ++ vmIpAddress vm + ++ " " ++ vmIpAddress vm ++ " " ++ port ++ " " ++ cloudService - ++ " " ++ show bg + ++ " " ++ show bg ++ " 2>&1" let paramsEnc = encode params - let rprocEnc = encode (ppairRemote ppair) + let rprocEnc = encode (ppairRemote ppair) (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch exe SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc)) - SSH.writeAllChannel ch rprocEnc + SSH.writeAllChannel ch rprocEnc SSH.writeChannel ch (encodeInt32 (BSL.length paramsEnc)) - SSH.writeAllChannel ch paramsEnc + SSH.writeAllChannel ch paramsEnc runLocalProcess (ppairLocal ppair) ch - if status == 0 - then return r - else error "runOnVM: Non-zero exit status" -- This would a bug + if status == 0 + then return r + else error "runOnVM: Non-zero exit status" -- This would a bug -- | Check the MD5 hash of the executable on the remote machine -apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool +apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool apiCheckMD5 params vm = do hash <- localHash params withSSH2 params vm $ \s -> do (r, _) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do SSH.channelExecute ch "md5sum -c --status" - SSH.writeChannel ch . BSSC.pack $ show hash ++ " " ++ azureSshRemotePath params + SSH.writeChannel ch . BSSC.pack $ show hash ++ " " ++ azureSshRemotePath params SSH.channelSendEOF ch SSH.readAllChannel ch return (r == 0) -withSSH2 :: AzureParameters -> VirtualMachine -> (SSH.Session -> IO a) -> IO a -withSSH2 params (Azure.vmSshEndpoint -> Just ep) = +withSSH2 :: AzureParameters -> VirtualMachine -> (SSH.Session -> IO a) -> IO a +withSSH2 params (Azure.vmSshEndpoint -> Just ep) = SSH.withSSH2 (azureSshKnownHosts params) (azureSshPublicKey params) (azureSshPrivateKey params) @@ -710,11 +710,11 @@ withSSH2 params (Azure.vmSshEndpoint -> Just ep) = (azureSshUserName params) (endpointVip ep) (read $ endpointPort ep) -withSSH2 _ vm = +withSSH2 _ vm = error $ "withSSH2: No SSH endpoint for virtual machine " ++ vmName vm catchSshError :: SSH.Session -> IO a -> IO a -catchSshError s io = +catchSshError s io = catches io [ Handler handleErrorCode , Handler handleNullPointer ] @@ -725,12 +725,12 @@ catchSshError s io = error str handleNullPointer :: SSH.NULL_POINTER -> IO a - handleNullPointer _ = do + handleNullPointer _ = do (_, str) <- SSH.getLastError s error str - -localHash :: AzureParameters -> IO MD5Digest -localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) + +localHash :: AzureParameters -> IO MD5Digest +localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) -------------------------------------------------------------------------------- -- Utilities -- @@ -738,8 +738,8 @@ localHash params = md5 <$> BSL.readFile (azureSshLocalPath params) -- | Find a virtual machine with a particular name findNamedVM :: Backend -> String -> IO (Maybe VirtualMachine) -findNamedVM backend vm = - listToMaybe . filter ((== vm) . vmName) <$> findVMs backend +findNamedVM backend vm = + listToMaybe . filter ((== vm) . vmName) <$> findVMs backend -------------------------------------------------------------------------------- -- Local and remote processes -- @@ -750,10 +750,10 @@ findNamedVM backend vm = -- for messages from the remote process using 'localExpect'. The remote process -- can send messages to the local process using 'remoteSend', and wait for -- messages from the local process using the standard Cloud Haskell primitives. --- +-- -- See also 'callOnVM'. data ProcessPair a = ProcessPair { - ppairRemote :: RemoteProcess () + ppairRemote :: RemoteProcess () , ppairLocal :: LocalProcess a } @@ -761,22 +761,22 @@ data ProcessPair a = ProcessPair { type RemoteProcess a = Closure (Backend -> Process a) -- | The process to run on the local node (see 'ProcessPair' and 'callOnVM'). -newtype LocalProcess a = LocalProcess { unLocalProcess :: ReaderT SSH.Channel IO a } +newtype LocalProcess a = LocalProcess { unLocalProcess :: ReaderT SSH.Channel IO a } deriving (Functor, Monad, MonadIO, MonadReader SSH.Channel) runLocalProcess :: LocalProcess a -> SSH.Channel -> IO a runLocalProcess = runReaderT . unLocalProcess --- | Send a messages from the local process to the remote process +-- | Send a messages from the local process to the remote process -- (see 'ProcessPair') localSend :: Serializable a => a -> LocalProcess () localSend x = LocalProcess $ do ch <- ask - liftIO $ mapM_ (SSH.writeChannel ch) + liftIO $ mapM_ (SSH.writeChannel ch) . prependLength - . CH.messageToPayload - . CH.createMessage - $ x + . CH.messageToPayload + . CH.createMessage + $ x -- | Wait for a message from the remote process (see 'ProcessPair'). -- Note that unlike for the standard Cloud Haskell 'expect' it will result in a @@ -784,14 +784,14 @@ localSend x = LocalProcess $ do -- -- Since it is relatively easy for the remote process to mess up the -- communication protocol (for instance, by doing a putStr) we ask for the --- length twice, as some sort of sanity check. +-- length twice, as some sort of sanity check. localExpect :: Serializable a => LocalProcess a localExpect = LocalProcess $ do ch <- ask - liftIO $ do + liftIO $ do isE <- readIntChannel ch - len <- readIntChannel ch - lenAgain <- readIntChannel ch + len <- readIntChannel ch + lenAgain <- readIntChannel ch when (len /= lenAgain) $ throwIO (userError "Internal error: protocol violation (perhaps the remote binary is not installed correctly?)") msg <- readSizeChannel ch len if isE /= 0 @@ -802,7 +802,7 @@ localExpect = LocalProcess $ do -- 'ProcessPair'). Note that the remote process can use the standard Cloud -- Haskell primitives to /receive/ messages from the local process. remoteSend :: Serializable a => a -> Process () -remoteSend = liftIO . remoteSend' +remoteSend = liftIO . remoteSend' remoteSend' :: Serializable a => a -> IO () remoteSend' = remoteSendFlagged 0 @@ -841,51 +841,51 @@ onVmMain rtable [host, port, cloudService, bg] = do hSetBinaryMode stdout True Just rprocEnc <- getWithLength stdin Just paramsEnc <- getWithLength stdin - backend <- initializeBackend (decode paramsEnc) cloudService - let rproc = decode rprocEnc + backend <- initializeBackend (decode paramsEnc) cloudService + let rproc = decode rprocEnc lprocMVar <- newEmptyMVar :: IO (MVar CH.LocalProcess) - if read bg + if read bg then void . Posix.forkProcess $ do -- We inherit the file descriptors from the parent, so the SSH -- session will not be terminated until we close them void Posix.createSession - startCH rproc lprocMVar backend + startCH rproc lprocMVar backend (\node proc -> runProcess node $ do us <- getSelfPid liftIO $ do remoteSend' us mapM_ hClose [stdin, stdout, stderr] - proc) + proc) else do - startCH rproc lprocMVar backend forkProcess + startCH rproc lprocMVar backend forkProcess lproc <- readMVar lprocMVar queueFromHandle stdin (CH.processQueue lproc) where - startCH :: RemoteProcess () - -> MVar CH.LocalProcess + startCH :: RemoteProcess () + -> MVar CH.LocalProcess -> Backend - -> (CH.LocalNode -> Process () -> IO a) - -> IO () + -> (CH.LocalNode -> Process () -> IO a) + -> IO () startCH rproc lprocMVar backend go = do - mTransport <- createTransport host port defaultTCPParameters + mTransport <- createTransport host port defaultTCPParameters case mTransport of Left err -> remoteThrow err - Right transport -> do + Right transport -> do node <- newLocalNode transport (rtable . __remoteTable $ initRemoteTable) - void . go node $ do + void . go node $ do ask >>= liftIO . putMVar lprocMVar proc <- unClosure rproc :: Process (Backend -> Process ()) - catch (proc backend) + catch (proc backend) (liftIO . (remoteThrow :: SomeException -> IO ())) -onVmMain _ _ +onVmMain _ _ = error "Invalid arguments passed on onVmMain" -- | Read a 4-byte length @l@ and then an @l@-byte payload -- -- Returns Nothing on EOF getWithLength :: Handle -> IO (Maybe BSL.ByteString) -getWithLength h = do +getWithLength h = do lenEnc <- BSS.hGet h 4 if BSS.length lenEnc < 4 then return Nothing @@ -898,9 +898,9 @@ getWithLength h = do queueFromHandle :: Handle -> CQueue CH.Message -> IO () queueFromHandle h q = do - mPayload <- getWithLength stdin + mPayload <- getWithLength stdin forM_ mPayload $ \payload -> do - enqueue q $ CH.payloadToMessage (BSL.toChunks payload) + enqueue q $ CH.payloadToMessage (BSL.toChunks payload) queueFromHandle h q -------------------------------------------------------------------------------- @@ -917,7 +917,7 @@ readSizeChannel ch = go [] go (bs : acc) (size - BSS.length bs) readIntChannel :: SSH.Channel -> IO Int -readIntChannel ch = +readIntChannel ch = decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4 -------------------------------------------------------------------------------- @@ -945,7 +945,7 @@ serviceProcess _backend = do go = do msg <- expect case msg of - ServiceProcessTerminate -> + ServiceProcessTerminate -> return () serviceProcessStatic :: Static (Backend -> Process ()) @@ -953,11 +953,11 @@ serviceProcessStatic = staticLabel "serviceProcess" -- | Start a new Cloud Haskell node on the given virtual machine spawnNodeOnVM :: Backend -> VirtualMachine -> String -> IO NodeId -spawnNodeOnVM backend vm port = +spawnNodeOnVM backend vm port = processNodeId <$> spawnOnVM backend vm port (staticClosure serviceProcessStatic) -- | Terminate a node started with 'spawnNodeOnVM' -terminateNode :: NodeId -> Process () +terminateNode :: NodeId -> Process () terminateNode nid = nsendRemote nid "$azureBackendServiceProcess" ServiceProcessTerminate __remoteTable :: RemoteTable -> RemoteTable From d06bd1b92647eab3b4442fd26bceb7ac75892d73 Mon Sep 17 00:00:00 2001 From: Simon Marlow Date: Mon, 3 Dec 2012 16:27:54 +0000 Subject: [PATCH 0315/2357] Shut down the logger process before exiting. --- .../Process/Backend/SimpleLocalnet.hs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index 226e2398..9c0e03b8 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -127,6 +127,11 @@ import Control.Distributed.Process , monitorNode , unmonitor , NodeMonitorNotification(..) + , finally + , newChan + , receiveChan + , nsend + , SendPort ) import qualified Control.Distributed.Process.Node as Node ( LocalNode @@ -349,7 +354,19 @@ startMaster backend proc = do Node.runProcess node $ do slaves <- findSlaves backend redirectLogsHere backend - proc slaves + proc slaves `finally` shutdownLogger + +-- +-- | shut down the logger process. This ensures that any pending +-- messages are flushed before the process exits. +-- +shutdownLogger :: Process () +shutdownLogger = do + (sport,rport) <- newChan + nsend "logger" (sport :: SendPort ()) + receiveChan rport + -- TODO: we should monitor the logger process so we don't deadlock if + -- it has already died. -------------------------------------------------------------------------------- -- Accessors -- From c8478a6d8d09a5174d00ba691dde0412904b7d2b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 5 Dec 2012 12:46:31 +0000 Subject: [PATCH 0316/2357] oops - we had already defined MessageMatcher --- src/Control/Distributed/Platform/GenServer.hs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 2d342e38..3a41e7ae 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -130,10 +130,6 @@ data ManageServer = TerminateServer TerminateReason deriving (Show, Typeable) $(derive makeBinary ''ManageServer) --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: d -> Match () - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s From 1cc780ee88f811a5f8a0d2febe3ef9a09d994a10 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 5 Dec 2012 12:46:31 +0000 Subject: [PATCH 0317/2357] oops - we had already defined MessageMatcher --- src/Control/Distributed/Platform/GenServer.hs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 2d342e38..3a41e7ae 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -130,10 +130,6 @@ data ManageServer = TerminateServer TerminateReason deriving (Show, Typeable) $(derive makeBinary ''ManageServer) --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: d -> Match () - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s From 7e04fe3dacfae97e2584a3cd8970335196c03e21 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 5 Dec 2012 07:55:57 -0500 Subject: [PATCH 0318/2357] additional test related changes - wip --- distributed-process-platform.cabal | 18 ++++-- tests/TestGenServer.hs | 90 ++++++++++++++++++++++++++++ tests/{Properties.hs => TestMain.hs} | 30 ++++++++-- 3 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 tests/TestGenServer.hs rename tests/{Properties.hs => TestMain.hs} (62%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0cbe46e7..d1a1c1be 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,7 +28,7 @@ executable dtp -- , stm , derive , distributed-static - , distributed-process + , distributed-process , distributed-process-simplelocalnet , network-transport , network-transport-tcp @@ -39,21 +39,31 @@ executable dtp hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts if flag(developer) - ghc-options: -auto-all + ghc-options: -auto-all -caf-all -fforce-recomp test-suite unit-tests default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: tests - main-is: Properties.hs - ghc-options: -Wall -threaded -rtsopts + main-is: TestMain.hs + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + default-extensions: ScopedTypeVariables, + DeriveDataTypeable, + GeneralizedNewtypeDeriving build-depends: QuickCheck, base, + binary >= 0.5 && < 0.7, containers, bytestring, template-haskell, + HUnit >= 1.2 && < 1.3, test-framework, test-framework-quickcheck2, + test-framework-hunit >= 0.2 && < 0.3, text, time + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs new file mode 100644 index 00000000..e166fa8e --- /dev/null +++ b/tests/TestGenServer.hs @@ -0,0 +1,90 @@ +module TestGenServer where + +import Data.Binary (Binary(..)) +import Data.Typeable (Typeable) +import Data.Foldable (forM_) +import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + , readMVar + ) +import Control.Monad (replicateM_, replicateM, forever) +import Control.Exception (SomeException, throwIO) +import qualified Control.Exception as Ex (catch) +import Control.Applicative ((<$>), (<*>), pure, (<|>)) +import qualified Network.Transport as NT (Transport, closeEndPoint) +import Network.Transport.TCP + ( createTransportExposeInternals + , TransportInternals(socketBetween) + , defaultTCPParameters + ) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) + , LocalNode(localEndPoint) + ) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable (Serializable) + +import Test.HUnit (Assertion) +import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework.Providers.HUnit (testCase) + +-------------------------------------------------------------------------------- +-- The tests proper -- +-------------------------------------------------------------------------------- + +newtype Ping = Ping ProcessId + deriving (Typeable, Binary, Show) + +newtype Pong = Pong ProcessId + deriving (Typeable, Binary, Show) + +-- | The ping server from the paper +ping :: Process () +ping = do + Pong partner <- expect + self <- getSelfPid + send partner (Ping self) + ping + +-- | Basic ping test +testPing :: NT.Transport -> Assertion +testPing transport = do + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Server + forkIO $ do + localNode <- newLocalNode transport initRemoteTable + addr <- forkProcess localNode ping + putMVar serverAddr addr + + -- Client + forkIO $ do + localNode <- newLocalNode transport initRemoteTable + pingServer <- readMVar serverAddr + + let numPings = 10000 + + runProcess localNode $ do + pid <- getSelfPid + replicateM_ numPings $ do + send pingServer (Pong pid) + Ping _ <- expect + return () + + putMVar clientDone () + + takeMVar clientDone + + +genServerTests :: NT.Transport -> [Test] +genServerTests transport = [ + testGroup "Basic features" [ + testCase "Ping" (testPing transport) + ] + ] diff --git a/tests/Properties.hs b/tests/TestMain.hs similarity index 62% rename from tests/Properties.hs rename to tests/TestMain.hs index ee3560f8..b3e4cef9 100644 --- a/tests/Properties.hs +++ b/tests/TestMain.hs @@ -4,6 +4,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +module Main where import Control.Applicative import Control.Monad @@ -21,8 +22,29 @@ import Test.Framework (Test, defaultMain, import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck (Arbitrary (..), Gen, choose) -main :: IO () -main = defaultMain tests -tests :: [Test] -tests = [] +import qualified Network.Transport as NT (Transport, closeEndPoint) +import Network.Transport.TCP + ( createTransportExposeInternals + , TransportInternals(socketBetween) + , defaultTCPParameters + ) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) + , LocalNode(localEndPoint) + ) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable (Serializable) + +import TestGenServer + +tests :: (NT.Transport, TransportInternals) -> [Test] +tests (transport, transportInternals) = [ + testGroup "GenServer" (genServerTests transport) + ] + +main :: IO () +main = do + Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters + defaultMain (tests transport) From 2df4ca5ca7a34e51c7ef2a907ceaa07e0afadd96 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 5 Dec 2012 07:55:57 -0500 Subject: [PATCH 0319/2357] additional test related changes - wip --- distributed-process-platform.cabal | 18 ++++-- tests/TestGenServer.hs | 90 ++++++++++++++++++++++++++++ tests/{Properties.hs => TestMain.hs} | 30 ++++++++-- 3 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 tests/TestGenServer.hs rename tests/{Properties.hs => TestMain.hs} (62%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0cbe46e7..d1a1c1be 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,7 +28,7 @@ executable dtp -- , stm , derive , distributed-static - , distributed-process + , distributed-process , distributed-process-simplelocalnet , network-transport , network-transport-tcp @@ -39,21 +39,31 @@ executable dtp hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts if flag(developer) - ghc-options: -auto-all + ghc-options: -auto-all -caf-all -fforce-recomp test-suite unit-tests default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: tests - main-is: Properties.hs - ghc-options: -Wall -threaded -rtsopts + main-is: TestMain.hs + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + default-extensions: ScopedTypeVariables, + DeriveDataTypeable, + GeneralizedNewtypeDeriving build-depends: QuickCheck, base, + binary >= 0.5 && < 0.7, containers, bytestring, template-haskell, + HUnit >= 1.2 && < 1.3, test-framework, test-framework-quickcheck2, + test-framework-hunit >= 0.2 && < 0.3, text, time + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs new file mode 100644 index 00000000..e166fa8e --- /dev/null +++ b/tests/TestGenServer.hs @@ -0,0 +1,90 @@ +module TestGenServer where + +import Data.Binary (Binary(..)) +import Data.Typeable (Typeable) +import Data.Foldable (forM_) +import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + , readMVar + ) +import Control.Monad (replicateM_, replicateM, forever) +import Control.Exception (SomeException, throwIO) +import qualified Control.Exception as Ex (catch) +import Control.Applicative ((<$>), (<*>), pure, (<|>)) +import qualified Network.Transport as NT (Transport, closeEndPoint) +import Network.Transport.TCP + ( createTransportExposeInternals + , TransportInternals(socketBetween) + , defaultTCPParameters + ) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) + , LocalNode(localEndPoint) + ) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable (Serializable) + +import Test.HUnit (Assertion) +import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework.Providers.HUnit (testCase) + +-------------------------------------------------------------------------------- +-- The tests proper -- +-------------------------------------------------------------------------------- + +newtype Ping = Ping ProcessId + deriving (Typeable, Binary, Show) + +newtype Pong = Pong ProcessId + deriving (Typeable, Binary, Show) + +-- | The ping server from the paper +ping :: Process () +ping = do + Pong partner <- expect + self <- getSelfPid + send partner (Ping self) + ping + +-- | Basic ping test +testPing :: NT.Transport -> Assertion +testPing transport = do + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Server + forkIO $ do + localNode <- newLocalNode transport initRemoteTable + addr <- forkProcess localNode ping + putMVar serverAddr addr + + -- Client + forkIO $ do + localNode <- newLocalNode transport initRemoteTable + pingServer <- readMVar serverAddr + + let numPings = 10000 + + runProcess localNode $ do + pid <- getSelfPid + replicateM_ numPings $ do + send pingServer (Pong pid) + Ping _ <- expect + return () + + putMVar clientDone () + + takeMVar clientDone + + +genServerTests :: NT.Transport -> [Test] +genServerTests transport = [ + testGroup "Basic features" [ + testCase "Ping" (testPing transport) + ] + ] diff --git a/tests/Properties.hs b/tests/TestMain.hs similarity index 62% rename from tests/Properties.hs rename to tests/TestMain.hs index ee3560f8..b3e4cef9 100644 --- a/tests/Properties.hs +++ b/tests/TestMain.hs @@ -4,6 +4,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +module Main where import Control.Applicative import Control.Monad @@ -21,8 +22,29 @@ import Test.Framework (Test, defaultMain, import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck (Arbitrary (..), Gen, choose) -main :: IO () -main = defaultMain tests -tests :: [Test] -tests = [] +import qualified Network.Transport as NT (Transport, closeEndPoint) +import Network.Transport.TCP + ( createTransportExposeInternals + , TransportInternals(socketBetween) + , defaultTCPParameters + ) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) + , LocalNode(localEndPoint) + ) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable (Serializable) + +import TestGenServer + +tests :: (NT.Transport, TransportInternals) -> [Test] +tests (transport, transportInternals) = [ + testGroup "GenServer" (genServerTests transport) + ] + +main :: IO () +main = do + Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters + defaultMain (tests transport) From 54a3c80666f4bdb382da91facdf6d25730802bae Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 5 Dec 2012 07:55:57 -0500 Subject: [PATCH 0320/2357] additional test related changes - wip --- distributed-process-platform.cabal | 18 ++++-- tests/TestGenServer.hs | 90 ++++++++++++++++++++++++++++ tests/{Properties.hs => TestMain.hs} | 30 ++++++++-- 3 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 tests/TestGenServer.hs rename tests/{Properties.hs => TestMain.hs} (62%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0cbe46e7..d1a1c1be 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,7 +28,7 @@ executable dtp -- , stm , derive , distributed-static - , distributed-process + , distributed-process , distributed-process-simplelocalnet , network-transport , network-transport-tcp @@ -39,21 +39,31 @@ executable dtp hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts if flag(developer) - ghc-options: -auto-all + ghc-options: -auto-all -caf-all -fforce-recomp test-suite unit-tests default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: tests - main-is: Properties.hs - ghc-options: -Wall -threaded -rtsopts + main-is: TestMain.hs + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + default-extensions: ScopedTypeVariables, + DeriveDataTypeable, + GeneralizedNewtypeDeriving build-depends: QuickCheck, base, + binary >= 0.5 && < 0.7, containers, bytestring, template-haskell, + HUnit >= 1.2 && < 1.3, test-framework, test-framework-quickcheck2, + test-framework-hunit >= 0.2 && < 0.3, text, time + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs new file mode 100644 index 00000000..e166fa8e --- /dev/null +++ b/tests/TestGenServer.hs @@ -0,0 +1,90 @@ +module TestGenServer where + +import Data.Binary (Binary(..)) +import Data.Typeable (Typeable) +import Data.Foldable (forM_) +import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + , readMVar + ) +import Control.Monad (replicateM_, replicateM, forever) +import Control.Exception (SomeException, throwIO) +import qualified Control.Exception as Ex (catch) +import Control.Applicative ((<$>), (<*>), pure, (<|>)) +import qualified Network.Transport as NT (Transport, closeEndPoint) +import Network.Transport.TCP + ( createTransportExposeInternals + , TransportInternals(socketBetween) + , defaultTCPParameters + ) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) + , LocalNode(localEndPoint) + ) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable (Serializable) + +import Test.HUnit (Assertion) +import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework.Providers.HUnit (testCase) + +-------------------------------------------------------------------------------- +-- The tests proper -- +-------------------------------------------------------------------------------- + +newtype Ping = Ping ProcessId + deriving (Typeable, Binary, Show) + +newtype Pong = Pong ProcessId + deriving (Typeable, Binary, Show) + +-- | The ping server from the paper +ping :: Process () +ping = do + Pong partner <- expect + self <- getSelfPid + send partner (Ping self) + ping + +-- | Basic ping test +testPing :: NT.Transport -> Assertion +testPing transport = do + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + + -- Server + forkIO $ do + localNode <- newLocalNode transport initRemoteTable + addr <- forkProcess localNode ping + putMVar serverAddr addr + + -- Client + forkIO $ do + localNode <- newLocalNode transport initRemoteTable + pingServer <- readMVar serverAddr + + let numPings = 10000 + + runProcess localNode $ do + pid <- getSelfPid + replicateM_ numPings $ do + send pingServer (Pong pid) + Ping _ <- expect + return () + + putMVar clientDone () + + takeMVar clientDone + + +genServerTests :: NT.Transport -> [Test] +genServerTests transport = [ + testGroup "Basic features" [ + testCase "Ping" (testPing transport) + ] + ] diff --git a/tests/Properties.hs b/tests/TestMain.hs similarity index 62% rename from tests/Properties.hs rename to tests/TestMain.hs index ee3560f8..b3e4cef9 100644 --- a/tests/Properties.hs +++ b/tests/TestMain.hs @@ -4,6 +4,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +module Main where import Control.Applicative import Control.Monad @@ -21,8 +22,29 @@ import Test.Framework (Test, defaultMain, import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck (Arbitrary (..), Gen, choose) -main :: IO () -main = defaultMain tests -tests :: [Test] -tests = [] +import qualified Network.Transport as NT (Transport, closeEndPoint) +import Network.Transport.TCP + ( createTransportExposeInternals + , TransportInternals(socketBetween) + , defaultTCPParameters + ) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) + , LocalNode(localEndPoint) + ) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable (Serializable) + +import TestGenServer + +tests :: (NT.Transport, TransportInternals) -> [Test] +tests (transport, transportInternals) = [ + testGroup "GenServer" (genServerTests transport) + ] + +main :: IO () +main = do + Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters + defaultMain (tests transport) From de7e334b4a64209b1288fd230f39ae12930dfb15 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Wed, 5 Dec 2012 07:55:57 -0500 Subject: [PATCH 0321/2357] additional test related changes - wip --- distributed-process-platform.cabal | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0cbe46e7..d1a1c1be 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,7 +28,7 @@ executable dtp -- , stm , derive , distributed-static - , distributed-process + , distributed-process , distributed-process-simplelocalnet , network-transport , network-transport-tcp @@ -39,21 +39,31 @@ executable dtp hs-source-dirs: src ghc-options: -Wall -threaded -rtsopts if flag(developer) - ghc-options: -auto-all + ghc-options: -auto-all -caf-all -fforce-recomp test-suite unit-tests default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: tests - main-is: Properties.hs - ghc-options: -Wall -threaded -rtsopts + main-is: TestMain.hs + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + default-extensions: ScopedTypeVariables, + DeriveDataTypeable, + GeneralizedNewtypeDeriving build-depends: QuickCheck, base, + binary >= 0.5 && < 0.7, containers, bytestring, template-haskell, + HUnit >= 1.2 && < 1.3, test-framework, test-framework-quickcheck2, + test-framework-hunit >= 0.2 && < 0.3, text, time + , distributed-process + , distributed-process-simplelocalnet + , network-transport + , network-transport-tcp From 5a8915d87208be9d64a90f08a1587cd24180c5cd Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sat, 8 Dec 2012 07:28:32 -0500 Subject: [PATCH 0322/2357] removed Naive module --- src/Control/Distributed/Naive/Kitty.hs | 133 ------------------------- 1 file changed, 133 deletions(-) delete mode 100644 src/Control/Distributed/Naive/Kitty.hs diff --git a/src/Control/Distributed/Naive/Kitty.hs b/src/Control/Distributed/Naive/Kitty.hs deleted file mode 100644 index 88db7335..00000000 --- a/src/Control/Distributed/Naive/Kitty.hs +++ /dev/null @@ -1,133 +0,0 @@ -{-| -Attemp to tranlsate a basic, naive (non-OTP based) server from Erland to Cloud Haskell - -This is a naive Erlang server implementation in CloudHaskell whose main purpose is to ground -the evolution of the API into a proper form that is typed and leverages Haskell strenghts. - -This sample was taken from here: - -See: http://learnyousomeerlang.com/what-is-otp#the-basic-server --} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - --- --- -module(kitty_server). --- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Naive.Kitty - ( - startKitty, - orderCat, - returnCat, - closeShop, - Cat(..) - ) where -import Prelude hiding (catch) -import Control.Exception(SomeException) -import Data.Binary (Binary (..), putWord8, getWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) -import Control.Distributed.Process (Process, getSelfPid, ProcessId, liftIO, spawnLocal, catch, say, send, expect) - --- --- % Records/Data Types --- -record(cat, {name, color=green, description}). -type Color = String -type Description = String -type Name = String - -newtype Id a = Id ProcessId deriving Show - -data Cat = Cat { - catName :: Name, - catColor :: Color, - catDescr :: Description } - deriving (Show, Typeable) - -$( derive makeBinary ''Cat ) - -newtype CatId = CatId ProcessId deriving Show - -data CatCmd - = OrderCat String String String - | ReturnCat Cat - | CloseShop - deriving (Show, Typeable) - -$( derive makeBinary ''CatCmd ) - -data CatEv - = CatOrdered Cat - | ShopClosed - deriving (Show, Typeable) - -$( derive makeBinary ''CatEv ) - - --- --- %% Client API --- start_link() -> spawn_link(fun init/0). -startKitty :: [Cat] -> Process ProcessId -startKitty cats = spawnLocal $ initCat cats `catch` \e -> do - say $ show (e :: SomeException) - initCat cats -- restart ... likely a bad idea! - --- %% Synchronous call -orderCat :: ProcessId -> Name -> Color -> Description -> Process Cat -orderCat pid name color descr = do - say "-- Ordering cat ..." - from <- getSelfPid - send pid (from, OrderCat name color descr) - r@(CatOrdered cat) <- expect - say $ "-- Got REPLY: " ++ show r - return cat - --- %% async call -returnCat :: ProcessId -> Cat -> Process () -returnCat pid cat = do - say $ "-- ReturnCat: " ++ show cat - send pid ((), ReturnCat cat) - --- %% sync call -closeShop :: ProcessId -> Process () -closeShop pid = do - say "-- Closing shop ..." - from <- getSelfPid - send pid (from, CloseShop) - reply <- expect :: Process CatEv - say $ "-- Got REPLY: " ++ show reply - return () - --- --- %%% Server functions -initCat :: [Cat] -> Process () -initCat args = do - say "Starting Kitty ..." - loopCat args - -loopCat :: [Cat] -> Process () -loopCat cats = do - say $ "Kitty inventory: " ++ show cats - (from, cmd) <- expect - say $ "Got CMD from " ++ show from ++ " : " ++ show cmd - case cmd of - cmd@(OrderCat n c d) -> case cats of - [] -> do - send from $ CatOrdered (Cat n c d) - loopCat [] - x:xs -> do - send from $ CatOrdered x - loopCat xs - cmd@(ReturnCat cat) -> loopCat (cat:cats) - cmd@CloseShop -> do - send from ShopClosed - terminateKitty cats - _ -> do - say $ "Unknown CMD: " ++ show cmd - loopCat cats - --- %%% Private functions -terminateKitty :: [Cat] -> Process () -terminateKitty cats = do - mapM_ (\c -> say $ show c ++ " set free!") cats - return () From 0236b4936ef9847085cf350ce00318b6dfab3f27 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sat, 8 Dec 2012 07:28:32 -0500 Subject: [PATCH 0323/2357] removed Naive module --- src/Control/Distributed/Naive/Kitty.hs | 133 ------------------------- 1 file changed, 133 deletions(-) delete mode 100644 src/Control/Distributed/Naive/Kitty.hs diff --git a/src/Control/Distributed/Naive/Kitty.hs b/src/Control/Distributed/Naive/Kitty.hs deleted file mode 100644 index 88db7335..00000000 --- a/src/Control/Distributed/Naive/Kitty.hs +++ /dev/null @@ -1,133 +0,0 @@ -{-| -Attemp to tranlsate a basic, naive (non-OTP based) server from Erland to Cloud Haskell - -This is a naive Erlang server implementation in CloudHaskell whose main purpose is to ground -the evolution of the API into a proper form that is typed and leverages Haskell strenghts. - -This sample was taken from here: - -See: http://learnyousomeerlang.com/what-is-otp#the-basic-server --} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - --- --- -module(kitty_server). --- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Naive.Kitty - ( - startKitty, - orderCat, - returnCat, - closeShop, - Cat(..) - ) where -import Prelude hiding (catch) -import Control.Exception(SomeException) -import Data.Binary (Binary (..), putWord8, getWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) -import Control.Distributed.Process (Process, getSelfPid, ProcessId, liftIO, spawnLocal, catch, say, send, expect) - --- --- % Records/Data Types --- -record(cat, {name, color=green, description}). -type Color = String -type Description = String -type Name = String - -newtype Id a = Id ProcessId deriving Show - -data Cat = Cat { - catName :: Name, - catColor :: Color, - catDescr :: Description } - deriving (Show, Typeable) - -$( derive makeBinary ''Cat ) - -newtype CatId = CatId ProcessId deriving Show - -data CatCmd - = OrderCat String String String - | ReturnCat Cat - | CloseShop - deriving (Show, Typeable) - -$( derive makeBinary ''CatCmd ) - -data CatEv - = CatOrdered Cat - | ShopClosed - deriving (Show, Typeable) - -$( derive makeBinary ''CatEv ) - - --- --- %% Client API --- start_link() -> spawn_link(fun init/0). -startKitty :: [Cat] -> Process ProcessId -startKitty cats = spawnLocal $ initCat cats `catch` \e -> do - say $ show (e :: SomeException) - initCat cats -- restart ... likely a bad idea! - --- %% Synchronous call -orderCat :: ProcessId -> Name -> Color -> Description -> Process Cat -orderCat pid name color descr = do - say "-- Ordering cat ..." - from <- getSelfPid - send pid (from, OrderCat name color descr) - r@(CatOrdered cat) <- expect - say $ "-- Got REPLY: " ++ show r - return cat - --- %% async call -returnCat :: ProcessId -> Cat -> Process () -returnCat pid cat = do - say $ "-- ReturnCat: " ++ show cat - send pid ((), ReturnCat cat) - --- %% sync call -closeShop :: ProcessId -> Process () -closeShop pid = do - say "-- Closing shop ..." - from <- getSelfPid - send pid (from, CloseShop) - reply <- expect :: Process CatEv - say $ "-- Got REPLY: " ++ show reply - return () - --- --- %%% Server functions -initCat :: [Cat] -> Process () -initCat args = do - say "Starting Kitty ..." - loopCat args - -loopCat :: [Cat] -> Process () -loopCat cats = do - say $ "Kitty inventory: " ++ show cats - (from, cmd) <- expect - say $ "Got CMD from " ++ show from ++ " : " ++ show cmd - case cmd of - cmd@(OrderCat n c d) -> case cats of - [] -> do - send from $ CatOrdered (Cat n c d) - loopCat [] - x:xs -> do - send from $ CatOrdered x - loopCat xs - cmd@(ReturnCat cat) -> loopCat (cat:cats) - cmd@CloseShop -> do - send from ShopClosed - terminateKitty cats - _ -> do - say $ "Unknown CMD: " ++ show cmd - loopCat cats - --- %%% Private functions -terminateKitty :: [Cat] -> Process () -terminateKitty cats = do - mapM_ (\c -> say $ show c ++ " set free!") cats - return () From 76088aacb4440542be5ac0d6dc3b61c3664eb093 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sat, 8 Dec 2012 07:28:32 -0500 Subject: [PATCH 0324/2357] removed Naive module --- src/Control/Distributed/Naive/Kitty.hs | 133 ------------------------- 1 file changed, 133 deletions(-) delete mode 100644 src/Control/Distributed/Naive/Kitty.hs diff --git a/src/Control/Distributed/Naive/Kitty.hs b/src/Control/Distributed/Naive/Kitty.hs deleted file mode 100644 index 88db7335..00000000 --- a/src/Control/Distributed/Naive/Kitty.hs +++ /dev/null @@ -1,133 +0,0 @@ -{-| -Attemp to tranlsate a basic, naive (non-OTP based) server from Erland to Cloud Haskell - -This is a naive Erlang server implementation in CloudHaskell whose main purpose is to ground -the evolution of the API into a proper form that is typed and leverages Haskell strenghts. - -This sample was taken from here: - -See: http://learnyousomeerlang.com/what-is-otp#the-basic-server --} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - --- --- -module(kitty_server). --- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Naive.Kitty - ( - startKitty, - orderCat, - returnCat, - closeShop, - Cat(..) - ) where -import Prelude hiding (catch) -import Control.Exception(SomeException) -import Data.Binary (Binary (..), putWord8, getWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) -import Control.Distributed.Process (Process, getSelfPid, ProcessId, liftIO, spawnLocal, catch, say, send, expect) - --- --- % Records/Data Types --- -record(cat, {name, color=green, description}). -type Color = String -type Description = String -type Name = String - -newtype Id a = Id ProcessId deriving Show - -data Cat = Cat { - catName :: Name, - catColor :: Color, - catDescr :: Description } - deriving (Show, Typeable) - -$( derive makeBinary ''Cat ) - -newtype CatId = CatId ProcessId deriving Show - -data CatCmd - = OrderCat String String String - | ReturnCat Cat - | CloseShop - deriving (Show, Typeable) - -$( derive makeBinary ''CatCmd ) - -data CatEv - = CatOrdered Cat - | ShopClosed - deriving (Show, Typeable) - -$( derive makeBinary ''CatEv ) - - --- --- %% Client API --- start_link() -> spawn_link(fun init/0). -startKitty :: [Cat] -> Process ProcessId -startKitty cats = spawnLocal $ initCat cats `catch` \e -> do - say $ show (e :: SomeException) - initCat cats -- restart ... likely a bad idea! - --- %% Synchronous call -orderCat :: ProcessId -> Name -> Color -> Description -> Process Cat -orderCat pid name color descr = do - say "-- Ordering cat ..." - from <- getSelfPid - send pid (from, OrderCat name color descr) - r@(CatOrdered cat) <- expect - say $ "-- Got REPLY: " ++ show r - return cat - --- %% async call -returnCat :: ProcessId -> Cat -> Process () -returnCat pid cat = do - say $ "-- ReturnCat: " ++ show cat - send pid ((), ReturnCat cat) - --- %% sync call -closeShop :: ProcessId -> Process () -closeShop pid = do - say "-- Closing shop ..." - from <- getSelfPid - send pid (from, CloseShop) - reply <- expect :: Process CatEv - say $ "-- Got REPLY: " ++ show reply - return () - --- --- %%% Server functions -initCat :: [Cat] -> Process () -initCat args = do - say "Starting Kitty ..." - loopCat args - -loopCat :: [Cat] -> Process () -loopCat cats = do - say $ "Kitty inventory: " ++ show cats - (from, cmd) <- expect - say $ "Got CMD from " ++ show from ++ " : " ++ show cmd - case cmd of - cmd@(OrderCat n c d) -> case cats of - [] -> do - send from $ CatOrdered (Cat n c d) - loopCat [] - x:xs -> do - send from $ CatOrdered x - loopCat xs - cmd@(ReturnCat cat) -> loopCat (cat:cats) - cmd@CloseShop -> do - send from ShopClosed - terminateKitty cats - _ -> do - say $ "Unknown CMD: " ++ show cmd - loopCat cats - --- %%% Private functions -terminateKitty :: [Cat] -> Process () -terminateKitty cats = do - mapM_ (\c -> say $ show c ++ " set free!") cats - return () From 3413ea433abb38ccff8880ca70a18c8d22392459 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sat, 8 Dec 2012 07:28:32 -0500 Subject: [PATCH 0325/2357] removed Naive module --- src/Control/Distributed/Naive/Kitty.hs | 133 ------------------------- 1 file changed, 133 deletions(-) delete mode 100644 src/Control/Distributed/Naive/Kitty.hs diff --git a/src/Control/Distributed/Naive/Kitty.hs b/src/Control/Distributed/Naive/Kitty.hs deleted file mode 100644 index 88db7335..00000000 --- a/src/Control/Distributed/Naive/Kitty.hs +++ /dev/null @@ -1,133 +0,0 @@ -{-| -Attemp to tranlsate a basic, naive (non-OTP based) server from Erland to Cloud Haskell - -This is a naive Erlang server implementation in CloudHaskell whose main purpose is to ground -the evolution of the API into a proper form that is typed and leverages Haskell strenghts. - -This sample was taken from here: - -See: http://learnyousomeerlang.com/what-is-otp#the-basic-server --} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - --- --- -module(kitty_server). --- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Naive.Kitty - ( - startKitty, - orderCat, - returnCat, - closeShop, - Cat(..) - ) where -import Prelude hiding (catch) -import Control.Exception(SomeException) -import Data.Binary (Binary (..), putWord8, getWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) -import Control.Distributed.Process (Process, getSelfPid, ProcessId, liftIO, spawnLocal, catch, say, send, expect) - --- --- % Records/Data Types --- -record(cat, {name, color=green, description}). -type Color = String -type Description = String -type Name = String - -newtype Id a = Id ProcessId deriving Show - -data Cat = Cat { - catName :: Name, - catColor :: Color, - catDescr :: Description } - deriving (Show, Typeable) - -$( derive makeBinary ''Cat ) - -newtype CatId = CatId ProcessId deriving Show - -data CatCmd - = OrderCat String String String - | ReturnCat Cat - | CloseShop - deriving (Show, Typeable) - -$( derive makeBinary ''CatCmd ) - -data CatEv - = CatOrdered Cat - | ShopClosed - deriving (Show, Typeable) - -$( derive makeBinary ''CatEv ) - - --- --- %% Client API --- start_link() -> spawn_link(fun init/0). -startKitty :: [Cat] -> Process ProcessId -startKitty cats = spawnLocal $ initCat cats `catch` \e -> do - say $ show (e :: SomeException) - initCat cats -- restart ... likely a bad idea! - --- %% Synchronous call -orderCat :: ProcessId -> Name -> Color -> Description -> Process Cat -orderCat pid name color descr = do - say "-- Ordering cat ..." - from <- getSelfPid - send pid (from, OrderCat name color descr) - r@(CatOrdered cat) <- expect - say $ "-- Got REPLY: " ++ show r - return cat - --- %% async call -returnCat :: ProcessId -> Cat -> Process () -returnCat pid cat = do - say $ "-- ReturnCat: " ++ show cat - send pid ((), ReturnCat cat) - --- %% sync call -closeShop :: ProcessId -> Process () -closeShop pid = do - say "-- Closing shop ..." - from <- getSelfPid - send pid (from, CloseShop) - reply <- expect :: Process CatEv - say $ "-- Got REPLY: " ++ show reply - return () - --- --- %%% Server functions -initCat :: [Cat] -> Process () -initCat args = do - say "Starting Kitty ..." - loopCat args - -loopCat :: [Cat] -> Process () -loopCat cats = do - say $ "Kitty inventory: " ++ show cats - (from, cmd) <- expect - say $ "Got CMD from " ++ show from ++ " : " ++ show cmd - case cmd of - cmd@(OrderCat n c d) -> case cats of - [] -> do - send from $ CatOrdered (Cat n c d) - loopCat [] - x:xs -> do - send from $ CatOrdered x - loopCat xs - cmd@(ReturnCat cat) -> loopCat (cat:cats) - cmd@CloseShop -> do - send from ShopClosed - terminateKitty cats - _ -> do - say $ "Unknown CMD: " ++ show cmd - loopCat cats - --- %%% Private functions -terminateKitty :: [Cat] -> Process () -terminateKitty cats = do - mapM_ (\c -> say $ show c ++ " set free!") cats - return () From eb36cc69e4212a758a7834b01937e882c7423cfc Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 12:59:14 +0000 Subject: [PATCH 0326/2357] add minimal travis config --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..999bd37b --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: haskell From 10f2bce68bc27de3f1fa319308b49bfce5a2d3cb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 12:59:14 +0000 Subject: [PATCH 0327/2357] add minimal travis config --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..999bd37b --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: haskell From f7a57b897c394c7636410e6a0ecf21819b9e8df7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 12:59:14 +0000 Subject: [PATCH 0328/2357] add minimal travis config --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..999bd37b --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: haskell From 8f9e57ba72ffd73c53d3fa0ab4309b2bfe91be2c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 12:59:14 +0000 Subject: [PATCH 0329/2357] add minimal travis config --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..999bd37b --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: haskell From 7afb839c9b2aa34259f1c6cef015c81cd4893d36 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:27 +0000 Subject: [PATCH 0330/2357] LICENSE => LICENCE --- LICENCE | 22 ++++++++++++++++++++++ LICENSE | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENCE delete mode 100644 LICENSE diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..dc1e0389 --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +Copyright (c) 2005 - 2013 Nebularis. + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 30404ce4..00000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file From 60ac70e28466c06e8d0d8aa117cff868c4dc32ec Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:27 +0000 Subject: [PATCH 0331/2357] LICENSE => LICENCE --- LICENCE | 22 ++++++++++++++++++++++ LICENSE | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENCE delete mode 100644 LICENSE diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..dc1e0389 --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +Copyright (c) 2005 - 2013 Nebularis. + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 30404ce4..00000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file From f7ec12ccca4b35d55b80012cdd950da76f78231f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:27 +0000 Subject: [PATCH 0332/2357] LICENSE => LICENCE --- LICENCE | 22 ++++++++++++++++++++++ LICENSE | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENCE delete mode 100644 LICENSE diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..dc1e0389 --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +Copyright (c) 2005 - 2013 Nebularis. + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 30404ce4..00000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file From a30c7d8d4834a7f15d34120a65ca335238a76a6f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:27 +0000 Subject: [PATCH 0333/2357] LICENSE => LICENCE --- LICENCE | 22 ++++++++++++++++++++++ LICENSE | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENCE delete mode 100644 LICENSE diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..dc1e0389 --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +Copyright (c) 2005 - 2013 Nebularis. + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 30404ce4..00000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file From d315f4112fcf72658520bd14cd286ea6731eb183 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:38 +0000 Subject: [PATCH 0334/2357] ignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index de74fe65..7a995ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ cabal-dev *.chs.h *.sublime-* *.sublime-* -*.lksh? \ No newline at end of file +*.lksh? +.dist-buildwrapper +.project From 36f8ed87cb4d4deca15b5ce29384c9f7980d5eb4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:38 +0000 Subject: [PATCH 0335/2357] ignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index de74fe65..7a995ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ cabal-dev *.chs.h *.sublime-* *.sublime-* -*.lksh? \ No newline at end of file +*.lksh? +.dist-buildwrapper +.project From 1eb3a40e03b4876ce9c9854f314dd2c92c562292 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:38 +0000 Subject: [PATCH 0336/2357] ignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index de74fe65..7a995ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ cabal-dev *.chs.h *.sublime-* *.sublime-* -*.lksh? \ No newline at end of file +*.lksh? +.dist-buildwrapper +.project From 32c420fcd854907cdcfde7afb80c8cd7997e1daa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:26:38 +0000 Subject: [PATCH 0337/2357] ignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index de74fe65..7a995ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ cabal-dev *.chs.h *.sublime-* *.sublime-* -*.lksh? \ No newline at end of file +*.lksh? +.dist-buildwrapper +.project From cad74a77097115807596fe6ed7c80b0030e2b9c5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:28:19 +0000 Subject: [PATCH 0338/2357] no more Main.hs - closes issue #16 --- distributed-process-platform.cabal | 118 +++++++++++++---------------- src/Main.hs | 89 ---------------------- 2 files changed, 53 insertions(+), 154 deletions(-) delete mode 100644 src/Main.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..8207fa9a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,69 +1,57 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.10 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.8 +build-type: Simple +license: BSD3 +license-file: LICENCE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform -flag developer - description: operate in developer mode - default: True +library + build-depends: + base >= 4, + distributed-process, + derive, + distributed-static, + binary, + mtl, + transformers + hs-source-dirs: src + ghc-options: -Wall + exposed-modules: Control.Distributed.Platform.GenProcess, + Control.Distributed.Platform.GenServer + other-modules: Control.Distributed.Platform.Timer -executable dtp - default-language: Haskell2010 - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - default-extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts - if flag(developer) - ghc-options: -auto-all -caf-all -fforce-recomp - -test-suite unit-tests - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: TestMain.hs - ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - default-extensions: ScopedTypeVariables, - DeriveDataTypeable, - GeneralizedNewtypeDeriving - build-depends: QuickCheck, - base, - binary >= 0.5 && < 0.7, - containers, - bytestring, - template-haskell, - HUnit >= 1.2 && < 1.3, - test-framework, - test-framework-quickcheck2, - test-framework-hunit >= 0.2 && < 0.3, - text, - time - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp +test-suite TestTimer + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: Control.Distributed.Platform.Timer + extensions: CPP + main-is: TestMain.hs diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index 643dd633..00000000 --- a/src/Main.hs +++ /dev/null @@ -1,89 +0,0 @@ -module Main where - -import Control.Distributed.Examples.Counter -import Control.Distributed.Examples.Kitty - -import Control.Exception (AsyncException (..), - SomeException, catchJust) -import Control.Monad (void) -import System.IO.Error (IOError) - -import Control.Distributed.Static (initRemoteTable) -import Network.Transport (closeTransport) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) - -import Control.Distributed.Process (Process, ProcessId, - getSelfPid, liftIO, - newChan, say, spawnLocal) -import Control.Distributed.Process.Node (LocalNode, newLocalNode, - runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) - putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - - stopCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - kittyTransactions kPid (n - 1) From f9fe1bffc59332e249d99a11ef9afb34732621cc Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:28:19 +0000 Subject: [PATCH 0339/2357] no more Main.hs - closes issue #16 --- distributed-process-platform.cabal | 118 +++++++++++++---------------- src/Main.hs | 89 ---------------------- 2 files changed, 53 insertions(+), 154 deletions(-) delete mode 100644 src/Main.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..8207fa9a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,69 +1,57 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.10 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.8 +build-type: Simple +license: BSD3 +license-file: LICENCE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform -flag developer - description: operate in developer mode - default: True +library + build-depends: + base >= 4, + distributed-process, + derive, + distributed-static, + binary, + mtl, + transformers + hs-source-dirs: src + ghc-options: -Wall + exposed-modules: Control.Distributed.Platform.GenProcess, + Control.Distributed.Platform.GenServer + other-modules: Control.Distributed.Platform.Timer -executable dtp - default-language: Haskell2010 - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - default-extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts - if flag(developer) - ghc-options: -auto-all -caf-all -fforce-recomp - -test-suite unit-tests - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: TestMain.hs - ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - default-extensions: ScopedTypeVariables, - DeriveDataTypeable, - GeneralizedNewtypeDeriving - build-depends: QuickCheck, - base, - binary >= 0.5 && < 0.7, - containers, - bytestring, - template-haskell, - HUnit >= 1.2 && < 1.3, - test-framework, - test-framework-quickcheck2, - test-framework-hunit >= 0.2 && < 0.3, - text, - time - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp +test-suite TestTimer + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: Control.Distributed.Platform.Timer + extensions: CPP + main-is: TestMain.hs diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index 643dd633..00000000 --- a/src/Main.hs +++ /dev/null @@ -1,89 +0,0 @@ -module Main where - -import Control.Distributed.Examples.Counter -import Control.Distributed.Examples.Kitty - -import Control.Exception (AsyncException (..), - SomeException, catchJust) -import Control.Monad (void) -import System.IO.Error (IOError) - -import Control.Distributed.Static (initRemoteTable) -import Network.Transport (closeTransport) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) - -import Control.Distributed.Process (Process, ProcessId, - getSelfPid, liftIO, - newChan, say, spawnLocal) -import Control.Distributed.Process.Node (LocalNode, newLocalNode, - runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) - putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - - stopCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - kittyTransactions kPid (n - 1) From 69ae1309ecbf6f9f8925348cd0075f842dfe9bea Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:28:19 +0000 Subject: [PATCH 0340/2357] no more Main.hs - closes issue #16 --- distributed-process-platform.cabal | 118 +++++++++++++---------------- src/Main.hs | 89 ---------------------- 2 files changed, 53 insertions(+), 154 deletions(-) delete mode 100644 src/Main.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..8207fa9a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,69 +1,57 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.10 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.8 +build-type: Simple +license: BSD3 +license-file: LICENCE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform -flag developer - description: operate in developer mode - default: True +library + build-depends: + base >= 4, + distributed-process, + derive, + distributed-static, + binary, + mtl, + transformers + hs-source-dirs: src + ghc-options: -Wall + exposed-modules: Control.Distributed.Platform.GenProcess, + Control.Distributed.Platform.GenServer + other-modules: Control.Distributed.Platform.Timer -executable dtp - default-language: Haskell2010 - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - default-extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts - if flag(developer) - ghc-options: -auto-all -caf-all -fforce-recomp - -test-suite unit-tests - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: TestMain.hs - ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - default-extensions: ScopedTypeVariables, - DeriveDataTypeable, - GeneralizedNewtypeDeriving - build-depends: QuickCheck, - base, - binary >= 0.5 && < 0.7, - containers, - bytestring, - template-haskell, - HUnit >= 1.2 && < 1.3, - test-framework, - test-framework-quickcheck2, - test-framework-hunit >= 0.2 && < 0.3, - text, - time - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp +test-suite TestTimer + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: Control.Distributed.Platform.Timer + extensions: CPP + main-is: TestMain.hs diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index 643dd633..00000000 --- a/src/Main.hs +++ /dev/null @@ -1,89 +0,0 @@ -module Main where - -import Control.Distributed.Examples.Counter -import Control.Distributed.Examples.Kitty - -import Control.Exception (AsyncException (..), - SomeException, catchJust) -import Control.Monad (void) -import System.IO.Error (IOError) - -import Control.Distributed.Static (initRemoteTable) -import Network.Transport (closeTransport) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) - -import Control.Distributed.Process (Process, ProcessId, - getSelfPid, liftIO, - newChan, say, spawnLocal) -import Control.Distributed.Process.Node (LocalNode, newLocalNode, - runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) - putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - - stopCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - kittyTransactions kPid (n - 1) From bbd1c3a2e0bf6ea9973ad7f36f9c11600c30d28b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 8 Dec 2012 13:28:19 +0000 Subject: [PATCH 0341/2357] no more Main.hs - closes issue #16 --- distributed-process-platform.cabal | 118 +++++++++++++---------------- src/Main.hs | 89 ---------------------- 2 files changed, 53 insertions(+), 154 deletions(-) delete mode 100644 src/Main.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..8207fa9a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -1,69 +1,57 @@ -name: distributed-process-platform -version: 0.1.0 -cabal-version: >=1.10 -build-type: Simple -license: BSD3 -license-file: LICENSE -stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform -synopsis: TODO -description: TODO -category: Control -tested-with: GHC ==7.4.1 -data-dir: "" +name: distributed-process-platform +version: 0.1.0 +cabal-version: >=1.8 +build-type: Simple +license: BSD3 +license-file: LICENCE +stability: experimental +homepage: http://github.com/hyperthunk/distributed-process-platform +synopsis: TODO +description: TODO +category: Control +tested-with: GHC ==7.4.1 +data-dir: "" -source-repository head - type: git - location: https://github.com/hyperthunk/distributed-process-platform +source-repository head + type: git + location: https://github.com/hyperthunk/distributed-process-platform -flag developer - description: operate in developer mode - default: True +library + build-depends: + base >= 4, + distributed-process, + derive, + distributed-static, + binary, + mtl, + transformers + hs-source-dirs: src + ghc-options: -Wall + exposed-modules: Control.Distributed.Platform.GenProcess, + Control.Distributed.Platform.GenServer + other-modules: Control.Distributed.Platform.Timer -executable dtp - default-language: Haskell2010 - main-is: Main.hs - build-depends: base >=4.4 && <5, binary >=0.5 && <0.6 - , mtl --- , stm - , derive - , distributed-static - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp - buildable: True - default-extensions: UndecidableInstances ExistentialQuantification - ScopedTypeVariables FlexibleInstances CPP BangPatterns - GeneralizedNewtypeDeriving GADTs DeriveDataTypeable - hs-source-dirs: src - ghc-options: -Wall -threaded -rtsopts - if flag(developer) - ghc-options: -auto-all -caf-all -fforce-recomp - -test-suite unit-tests - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: tests - main-is: TestMain.hs - ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - default-extensions: ScopedTypeVariables, - DeriveDataTypeable, - GeneralizedNewtypeDeriving - build-depends: QuickCheck, - base, - binary >= 0.5 && < 0.7, - containers, - bytestring, - template-haskell, - HUnit >= 1.2 && < 1.3, - test-framework, - test-framework-quickcheck2, - test-framework-hunit >= 0.2 && < 0.3, - text, - time - , distributed-process - , distributed-process-simplelocalnet - , network-transport - , network-transport-tcp +test-suite TestTimer + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: Control.Distributed.Platform.Timer + extensions: CPP + main-is: TestMain.hs diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index 643dd633..00000000 --- a/src/Main.hs +++ /dev/null @@ -1,89 +0,0 @@ -module Main where - -import Control.Distributed.Examples.Counter -import Control.Distributed.Examples.Kitty - -import Control.Exception (AsyncException (..), - SomeException, catchJust) -import Control.Monad (void) -import System.IO.Error (IOError) - -import Control.Distributed.Static (initRemoteTable) -import Network.Transport (closeTransport) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) - -import Control.Distributed.Process (Process, ProcessId, - getSelfPid, liftIO, - newChan, say, spawnLocal) -import Control.Distributed.Process.Node (LocalNode, newLocalNode, - runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) - putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - - stopCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - kittyTransactions kPid (n - 1) From 91e097baebb7ecd2f2157d566abfa87291060d9b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:23:38 +0000 Subject: [PATCH 0342/2357] cosmetic (ish) tidy up --- tests/TestGenServer.hs | 4 ++-- tests/TestMain.hs | 14 +------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e166fa8e..5b760cac 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,9 +1,9 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-} module TestGenServer where import Data.Binary (Binary(..)) import Data.Typeable (Typeable) -import Data.Foldable (forM_) -import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent (forkIO) import Control.Concurrent.MVar ( MVar , newEmptyMVar diff --git a/tests/TestMain.hs b/tests/TestMain.hs index b3e4cef9..060a82fc 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -8,20 +8,8 @@ module Main where import Control.Applicative import Control.Monad -import Data.Data (Data, Typeable) -import qualified Data.Map as Map -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time (Day (..), LocalTime (..), - TimeOfDay (..), - TimeZone (..), - ZonedTime (..), - hoursToTimeZone) import Test.Framework (Test, defaultMain, testGroup) -import Test.Framework.Providers.QuickCheck2 (testProperty) -import Test.QuickCheck (Arbitrary (..), Gen, - choose) import qualified Network.Transport as NT (Transport, closeEndPoint) import Network.Transport.TCP @@ -41,7 +29,7 @@ import TestGenServer tests :: (NT.Transport, TransportInternals) -> [Test] tests (transport, transportInternals) = [ - testGroup "GenServer" (genServerTests transport) + testGroup "GenServer" (genServerTests transport) ] main :: IO () From f16986628c0926de0937dd8e8dc4a462c3bea84e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:23:38 +0000 Subject: [PATCH 0343/2357] cosmetic (ish) tidy up --- tests/TestGenServer.hs | 4 ++-- tests/TestMain.hs | 14 +------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e166fa8e..5b760cac 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,9 +1,9 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-} module TestGenServer where import Data.Binary (Binary(..)) import Data.Typeable (Typeable) -import Data.Foldable (forM_) -import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent (forkIO) import Control.Concurrent.MVar ( MVar , newEmptyMVar diff --git a/tests/TestMain.hs b/tests/TestMain.hs index b3e4cef9..060a82fc 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -8,20 +8,8 @@ module Main where import Control.Applicative import Control.Monad -import Data.Data (Data, Typeable) -import qualified Data.Map as Map -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time (Day (..), LocalTime (..), - TimeOfDay (..), - TimeZone (..), - ZonedTime (..), - hoursToTimeZone) import Test.Framework (Test, defaultMain, testGroup) -import Test.Framework.Providers.QuickCheck2 (testProperty) -import Test.QuickCheck (Arbitrary (..), Gen, - choose) import qualified Network.Transport as NT (Transport, closeEndPoint) import Network.Transport.TCP @@ -41,7 +29,7 @@ import TestGenServer tests :: (NT.Transport, TransportInternals) -> [Test] tests (transport, transportInternals) = [ - testGroup "GenServer" (genServerTests transport) + testGroup "GenServer" (genServerTests transport) ] main :: IO () From fe408907e19874f061f8e67c8b2965c2405da590 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:23:38 +0000 Subject: [PATCH 0344/2357] cosmetic (ish) tidy up --- tests/TestGenServer.hs | 4 ++-- tests/TestMain.hs | 14 +------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e166fa8e..5b760cac 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,9 +1,9 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-} module TestGenServer where import Data.Binary (Binary(..)) import Data.Typeable (Typeable) -import Data.Foldable (forM_) -import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent (forkIO) import Control.Concurrent.MVar ( MVar , newEmptyMVar diff --git a/tests/TestMain.hs b/tests/TestMain.hs index b3e4cef9..060a82fc 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -8,20 +8,8 @@ module Main where import Control.Applicative import Control.Monad -import Data.Data (Data, Typeable) -import qualified Data.Map as Map -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time (Day (..), LocalTime (..), - TimeOfDay (..), - TimeZone (..), - ZonedTime (..), - hoursToTimeZone) import Test.Framework (Test, defaultMain, testGroup) -import Test.Framework.Providers.QuickCheck2 (testProperty) -import Test.QuickCheck (Arbitrary (..), Gen, - choose) import qualified Network.Transport as NT (Transport, closeEndPoint) import Network.Transport.TCP @@ -41,7 +29,7 @@ import TestGenServer tests :: (NT.Transport, TransportInternals) -> [Test] tests (transport, transportInternals) = [ - testGroup "GenServer" (genServerTests transport) + testGroup "GenServer" (genServerTests transport) ] main :: IO () From 1e4aaaeeeac2b0ea15cc6efcc34826f735eaf0f5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:28:49 +0000 Subject: [PATCH 0345/2357] pass all relevant transport data to test generators --- tests/TestGenServer.hs | 4 ++-- tests/TestMain.hs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 5b760cac..9b6c5ea6 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -82,8 +82,8 @@ testPing transport = do takeMVar clientDone -genServerTests :: NT.Transport -> [Test] -genServerTests transport = [ +genServerTests :: (NT.Transport, TransportInternals) -> [Test] +genServerTests (transport, _) = [ testGroup "Basic features" [ testCase "Ping" (testPing transport) ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 060a82fc..187a367c 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -28,11 +28,11 @@ import Control.Distributed.Process.Serializable (Serializable) import TestGenServer tests :: (NT.Transport, TransportInternals) -> [Test] -tests (transport, transportInternals) = [ - testGroup "GenServer" (genServerTests transport) +tests transportConfig = [ + testGroup "GenServer" (genServerTests transportConfig) ] main :: IO () main = do Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) + defaultMain (tests transport) \ No newline at end of file From 44a9faf2c67026414c662f4749c6b63ee99ad20a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:28:49 +0000 Subject: [PATCH 0346/2357] pass all relevant transport data to test generators --- tests/TestGenServer.hs | 4 ++-- tests/TestMain.hs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 5b760cac..9b6c5ea6 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -82,8 +82,8 @@ testPing transport = do takeMVar clientDone -genServerTests :: NT.Transport -> [Test] -genServerTests transport = [ +genServerTests :: (NT.Transport, TransportInternals) -> [Test] +genServerTests (transport, _) = [ testGroup "Basic features" [ testCase "Ping" (testPing transport) ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 060a82fc..187a367c 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -28,11 +28,11 @@ import Control.Distributed.Process.Serializable (Serializable) import TestGenServer tests :: (NT.Transport, TransportInternals) -> [Test] -tests (transport, transportInternals) = [ - testGroup "GenServer" (genServerTests transport) +tests transportConfig = [ + testGroup "GenServer" (genServerTests transportConfig) ] main :: IO () main = do Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) + defaultMain (tests transport) \ No newline at end of file From ae3d564e92acd7d5105d3cdf72e1970d296a7ff7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:28:49 +0000 Subject: [PATCH 0347/2357] pass all relevant transport data to test generators --- tests/TestGenServer.hs | 4 ++-- tests/TestMain.hs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 5b760cac..9b6c5ea6 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -82,8 +82,8 @@ testPing transport = do takeMVar clientDone -genServerTests :: NT.Transport -> [Test] -genServerTests transport = [ +genServerTests :: (NT.Transport, TransportInternals) -> [Test] +genServerTests (transport, _) = [ testGroup "Basic features" [ testCase "Ping" (testPing transport) ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 060a82fc..187a367c 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -28,11 +28,11 @@ import Control.Distributed.Process.Serializable (Serializable) import TestGenServer tests :: (NT.Transport, TransportInternals) -> [Test] -tests (transport, transportInternals) = [ - testGroup "GenServer" (genServerTests transport) +tests transportConfig = [ + testGroup "GenServer" (genServerTests transportConfig) ] main :: IO () main = do Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) + defaultMain (tests transport) \ No newline at end of file From 5cd39c9f5eb59906d47b516eaa3650da1d2dfb1c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:31:14 +0000 Subject: [PATCH 0348/2357] (less) whitespace --- src/Control/Distributed/Platform/GenServer.hs | 71 +------------------ 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..01d6d3c6 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -69,24 +69,16 @@ import Data.Typeable (Typeable) -- | Process name type Name = String - - -- | ServerId type ServerId = ProcessId - - -- | Timeout data Timeout = Timeout Int | NoTimeout - - -- | Server monad type Server s = ST.StateT s Process - - -- | Initialize handler result data InitResult = InitOk Timeout @@ -98,8 +90,6 @@ initOk t = return (InitOk t) initStop :: String -> Server s InitResult initStop reason = return (InitStop reason) - - -- | Terminate reason data TerminateReason = TerminateNormal @@ -108,8 +98,6 @@ data TerminateReason deriving (Show, Typeable) $(derive makeBinary ''TerminateReason) - - -- | The result of a call data CallResult a = CallOk a @@ -126,8 +114,6 @@ callForward sid = return (CallForward sid) callStop :: a -> String -> Server s (CallResult a) callStop resp reason = return (CallStop resp reason) - - -- | The result of a cast data CastResult = CastOk @@ -143,24 +129,17 @@ castForward sid = return (CastForward sid) castStop :: String -> Server s CastResult castStop reason = return (CastStop reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type CallHandler s a b = a -> Server s (CallResult b) type CastHandler s a = a -> Server s CastResult - - - -- | Adds routing metadata to the actual payload data Message a = Message ProcessId a deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Management message -- TODO is there a std way of terminating a process from another process? data ManageServer = TerminateServer TerminateReason @@ -169,8 +148,8 @@ $(derive makeBinary ''ManageServer) -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state -data MessageDispatcher s - = forall a . (Serializable a) => MessageDispatcher { +data MessageDispatcher s = + forall a . (Serializable a) => MessageDispatcher { dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) } | forall a . (Serializable a) => MessageDispatcherIf { @@ -181,28 +160,20 @@ data MessageDispatcher s dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) } - -- | Matches messages using a dispatcher class MessageMatcher d where matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - - -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) - - -- | Constructs a call message dispatcher --- handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s handleCall = handleCallIf (const True) - - handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s handleCallIf cond handler = MessageDispatcherIf { dispatcher = (\state m@(Message cid req) -> do @@ -222,15 +193,11 @@ handleCallIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ req) -> cond req } - - -- | Constructs a cast message dispatcher -- handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s handleCast = handleCastIf (const True) - - -- | handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s handleCastIf cond handler = MessageDispatcherIf { @@ -247,8 +214,6 @@ handleCastIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ msg) -> cond msg } - - -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's @@ -265,8 +230,6 @@ handleAny handler = MessageDispatcherAny { ) } - - -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler @@ -274,8 +237,6 @@ data LocalServer s = LocalServer { terminateHandler :: TerminateHandler s -- ^ termination handler } - - ---- | Default record ---- Starting point for creating new servers defaultServer :: LocalServer s @@ -285,8 +246,6 @@ defaultServer = LocalServer { terminateHandler = \_ -> return () } - - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- @@ -297,8 +256,6 @@ startServer state handlers = spawnLocal $ do ST.runStateT (processServer handlers) state return () - - -- TODO startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerLink handlers = undefined @@ -307,8 +264,6 @@ startServerLink handlers = undefined --ref <- monitor them --return (them, ref) - - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do @@ -323,8 +278,6 @@ callServer sid timeout rq = do Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time - - -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do @@ -332,31 +285,22 @@ castServer sid msg = do say $ "Casting server " ++ show cid send sid (Message cid msg) - - -- | Stops a server identified by it's ServerId stopServer :: ServerId -> TerminateReason -> Process () stopServer sid reason = castServer sid (TerminateServer reason) - - -- | Get the server state getState :: Server s s getState = ST.get - - -- | Put the server state putState :: s -> Server s () putState = ST.put - - -- | Modify the server state modifyState :: (s -> s) -> Server s () modifyState = ST.modify - -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- @@ -372,8 +316,6 @@ processServer localServer = do InitStop r -> return (TerminateReason r) processTerminate localServer tr - - -- | initialize server processInit :: LocalServer s -> Server s InitResult processInit localServer = do @@ -381,8 +323,6 @@ processInit localServer = do ir <- initHandler localServer return ir - - -- | server loop processLoop :: LocalServer s -> Timeout -> Server s TerminateReason processLoop localServer t = do @@ -391,8 +331,6 @@ processLoop localServer t = do Just r -> return r Nothing -> processLoop localServer t - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -413,17 +351,12 @@ processReceive ds timeout = do trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | terminate server processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do trace $ "Server terminating: " ++ show reason (terminateHandler localServer) reason - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = ST.lift . say $ msg - From 1da866e7232e2c5f7fd58250414e630043cc9091 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:31:14 +0000 Subject: [PATCH 0349/2357] (less) whitespace --- src/Control/Distributed/Platform/GenServer.hs | 71 +------------------ 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..01d6d3c6 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -69,24 +69,16 @@ import Data.Typeable (Typeable) -- | Process name type Name = String - - -- | ServerId type ServerId = ProcessId - - -- | Timeout data Timeout = Timeout Int | NoTimeout - - -- | Server monad type Server s = ST.StateT s Process - - -- | Initialize handler result data InitResult = InitOk Timeout @@ -98,8 +90,6 @@ initOk t = return (InitOk t) initStop :: String -> Server s InitResult initStop reason = return (InitStop reason) - - -- | Terminate reason data TerminateReason = TerminateNormal @@ -108,8 +98,6 @@ data TerminateReason deriving (Show, Typeable) $(derive makeBinary ''TerminateReason) - - -- | The result of a call data CallResult a = CallOk a @@ -126,8 +114,6 @@ callForward sid = return (CallForward sid) callStop :: a -> String -> Server s (CallResult a) callStop resp reason = return (CallStop resp reason) - - -- | The result of a cast data CastResult = CastOk @@ -143,24 +129,17 @@ castForward sid = return (CastForward sid) castStop :: String -> Server s CastResult castStop reason = return (CastStop reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type CallHandler s a b = a -> Server s (CallResult b) type CastHandler s a = a -> Server s CastResult - - - -- | Adds routing metadata to the actual payload data Message a = Message ProcessId a deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Management message -- TODO is there a std way of terminating a process from another process? data ManageServer = TerminateServer TerminateReason @@ -169,8 +148,8 @@ $(derive makeBinary ''ManageServer) -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state -data MessageDispatcher s - = forall a . (Serializable a) => MessageDispatcher { +data MessageDispatcher s = + forall a . (Serializable a) => MessageDispatcher { dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) } | forall a . (Serializable a) => MessageDispatcherIf { @@ -181,28 +160,20 @@ data MessageDispatcher s dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) } - -- | Matches messages using a dispatcher class MessageMatcher d where matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - - -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) - - -- | Constructs a call message dispatcher --- handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s handleCall = handleCallIf (const True) - - handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s handleCallIf cond handler = MessageDispatcherIf { dispatcher = (\state m@(Message cid req) -> do @@ -222,15 +193,11 @@ handleCallIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ req) -> cond req } - - -- | Constructs a cast message dispatcher -- handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s handleCast = handleCastIf (const True) - - -- | handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s handleCastIf cond handler = MessageDispatcherIf { @@ -247,8 +214,6 @@ handleCastIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ msg) -> cond msg } - - -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's @@ -265,8 +230,6 @@ handleAny handler = MessageDispatcherAny { ) } - - -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler @@ -274,8 +237,6 @@ data LocalServer s = LocalServer { terminateHandler :: TerminateHandler s -- ^ termination handler } - - ---- | Default record ---- Starting point for creating new servers defaultServer :: LocalServer s @@ -285,8 +246,6 @@ defaultServer = LocalServer { terminateHandler = \_ -> return () } - - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- @@ -297,8 +256,6 @@ startServer state handlers = spawnLocal $ do ST.runStateT (processServer handlers) state return () - - -- TODO startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerLink handlers = undefined @@ -307,8 +264,6 @@ startServerLink handlers = undefined --ref <- monitor them --return (them, ref) - - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do @@ -323,8 +278,6 @@ callServer sid timeout rq = do Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time - - -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do @@ -332,31 +285,22 @@ castServer sid msg = do say $ "Casting server " ++ show cid send sid (Message cid msg) - - -- | Stops a server identified by it's ServerId stopServer :: ServerId -> TerminateReason -> Process () stopServer sid reason = castServer sid (TerminateServer reason) - - -- | Get the server state getState :: Server s s getState = ST.get - - -- | Put the server state putState :: s -> Server s () putState = ST.put - - -- | Modify the server state modifyState :: (s -> s) -> Server s () modifyState = ST.modify - -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- @@ -372,8 +316,6 @@ processServer localServer = do InitStop r -> return (TerminateReason r) processTerminate localServer tr - - -- | initialize server processInit :: LocalServer s -> Server s InitResult processInit localServer = do @@ -381,8 +323,6 @@ processInit localServer = do ir <- initHandler localServer return ir - - -- | server loop processLoop :: LocalServer s -> Timeout -> Server s TerminateReason processLoop localServer t = do @@ -391,8 +331,6 @@ processLoop localServer t = do Just r -> return r Nothing -> processLoop localServer t - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -413,17 +351,12 @@ processReceive ds timeout = do trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | terminate server processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do trace $ "Server terminating: " ++ show reason (terminateHandler localServer) reason - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = ST.lift . say $ msg - From a18a63c72807ccd53660e7f90e3bc641120ac23e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:31:14 +0000 Subject: [PATCH 0350/2357] (less) whitespace --- src/Control/Distributed/Platform/GenServer.hs | 71 +------------------ 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..01d6d3c6 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -69,24 +69,16 @@ import Data.Typeable (Typeable) -- | Process name type Name = String - - -- | ServerId type ServerId = ProcessId - - -- | Timeout data Timeout = Timeout Int | NoTimeout - - -- | Server monad type Server s = ST.StateT s Process - - -- | Initialize handler result data InitResult = InitOk Timeout @@ -98,8 +90,6 @@ initOk t = return (InitOk t) initStop :: String -> Server s InitResult initStop reason = return (InitStop reason) - - -- | Terminate reason data TerminateReason = TerminateNormal @@ -108,8 +98,6 @@ data TerminateReason deriving (Show, Typeable) $(derive makeBinary ''TerminateReason) - - -- | The result of a call data CallResult a = CallOk a @@ -126,8 +114,6 @@ callForward sid = return (CallForward sid) callStop :: a -> String -> Server s (CallResult a) callStop resp reason = return (CallStop resp reason) - - -- | The result of a cast data CastResult = CastOk @@ -143,24 +129,17 @@ castForward sid = return (CastForward sid) castStop :: String -> Server s CastResult castStop reason = return (CastStop reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type CallHandler s a b = a -> Server s (CallResult b) type CastHandler s a = a -> Server s CastResult - - - -- | Adds routing metadata to the actual payload data Message a = Message ProcessId a deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Management message -- TODO is there a std way of terminating a process from another process? data ManageServer = TerminateServer TerminateReason @@ -169,8 +148,8 @@ $(derive makeBinary ''ManageServer) -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state -data MessageDispatcher s - = forall a . (Serializable a) => MessageDispatcher { +data MessageDispatcher s = + forall a . (Serializable a) => MessageDispatcher { dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) } | forall a . (Serializable a) => MessageDispatcherIf { @@ -181,28 +160,20 @@ data MessageDispatcher s dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) } - -- | Matches messages using a dispatcher class MessageMatcher d where matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - - -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) - - -- | Constructs a call message dispatcher --- handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s handleCall = handleCallIf (const True) - - handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s handleCallIf cond handler = MessageDispatcherIf { dispatcher = (\state m@(Message cid req) -> do @@ -222,15 +193,11 @@ handleCallIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ req) -> cond req } - - -- | Constructs a cast message dispatcher -- handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s handleCast = handleCastIf (const True) - - -- | handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s handleCastIf cond handler = MessageDispatcherIf { @@ -247,8 +214,6 @@ handleCastIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ msg) -> cond msg } - - -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's @@ -265,8 +230,6 @@ handleAny handler = MessageDispatcherAny { ) } - - -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler @@ -274,8 +237,6 @@ data LocalServer s = LocalServer { terminateHandler :: TerminateHandler s -- ^ termination handler } - - ---- | Default record ---- Starting point for creating new servers defaultServer :: LocalServer s @@ -285,8 +246,6 @@ defaultServer = LocalServer { terminateHandler = \_ -> return () } - - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- @@ -297,8 +256,6 @@ startServer state handlers = spawnLocal $ do ST.runStateT (processServer handlers) state return () - - -- TODO startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerLink handlers = undefined @@ -307,8 +264,6 @@ startServerLink handlers = undefined --ref <- monitor them --return (them, ref) - - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do @@ -323,8 +278,6 @@ callServer sid timeout rq = do Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time - - -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do @@ -332,31 +285,22 @@ castServer sid msg = do say $ "Casting server " ++ show cid send sid (Message cid msg) - - -- | Stops a server identified by it's ServerId stopServer :: ServerId -> TerminateReason -> Process () stopServer sid reason = castServer sid (TerminateServer reason) - - -- | Get the server state getState :: Server s s getState = ST.get - - -- | Put the server state putState :: s -> Server s () putState = ST.put - - -- | Modify the server state modifyState :: (s -> s) -> Server s () modifyState = ST.modify - -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- @@ -372,8 +316,6 @@ processServer localServer = do InitStop r -> return (TerminateReason r) processTerminate localServer tr - - -- | initialize server processInit :: LocalServer s -> Server s InitResult processInit localServer = do @@ -381,8 +323,6 @@ processInit localServer = do ir <- initHandler localServer return ir - - -- | server loop processLoop :: LocalServer s -> Timeout -> Server s TerminateReason processLoop localServer t = do @@ -391,8 +331,6 @@ processLoop localServer t = do Just r -> return r Nothing -> processLoop localServer t - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -413,17 +351,12 @@ processReceive ds timeout = do trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | terminate server processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do trace $ "Server terminating: " ++ show reason (terminateHandler localServer) reason - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = ST.lift . say $ msg - From d79011a2d19b14cef82caba1bbc635a9594f2acd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:31:14 +0000 Subject: [PATCH 0351/2357] (less) whitespace --- src/Control/Distributed/Platform/GenServer.hs | 71 +------------------ 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..01d6d3c6 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -69,24 +69,16 @@ import Data.Typeable (Typeable) -- | Process name type Name = String - - -- | ServerId type ServerId = ProcessId - - -- | Timeout data Timeout = Timeout Int | NoTimeout - - -- | Server monad type Server s = ST.StateT s Process - - -- | Initialize handler result data InitResult = InitOk Timeout @@ -98,8 +90,6 @@ initOk t = return (InitOk t) initStop :: String -> Server s InitResult initStop reason = return (InitStop reason) - - -- | Terminate reason data TerminateReason = TerminateNormal @@ -108,8 +98,6 @@ data TerminateReason deriving (Show, Typeable) $(derive makeBinary ''TerminateReason) - - -- | The result of a call data CallResult a = CallOk a @@ -126,8 +114,6 @@ callForward sid = return (CallForward sid) callStop :: a -> String -> Server s (CallResult a) callStop resp reason = return (CallStop resp reason) - - -- | The result of a cast data CastResult = CastOk @@ -143,24 +129,17 @@ castForward sid = return (CastForward sid) castStop :: String -> Server s CastResult castStop reason = return (CastStop reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type CallHandler s a b = a -> Server s (CallResult b) type CastHandler s a = a -> Server s CastResult - - - -- | Adds routing metadata to the actual payload data Message a = Message ProcessId a deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Management message -- TODO is there a std way of terminating a process from another process? data ManageServer = TerminateServer TerminateReason @@ -169,8 +148,8 @@ $(derive makeBinary ''ManageServer) -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state -data MessageDispatcher s - = forall a . (Serializable a) => MessageDispatcher { +data MessageDispatcher s = + forall a . (Serializable a) => MessageDispatcher { dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) } | forall a . (Serializable a) => MessageDispatcherIf { @@ -181,28 +160,20 @@ data MessageDispatcher s dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) } - -- | Matches messages using a dispatcher class MessageMatcher d where matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - - -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) - - -- | Constructs a call message dispatcher --- handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s handleCall = handleCallIf (const True) - - handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s handleCallIf cond handler = MessageDispatcherIf { dispatcher = (\state m@(Message cid req) -> do @@ -222,15 +193,11 @@ handleCallIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ req) -> cond req } - - -- | Constructs a cast message dispatcher -- handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s handleCast = handleCastIf (const True) - - -- | handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s handleCastIf cond handler = MessageDispatcherIf { @@ -247,8 +214,6 @@ handleCastIf cond handler = MessageDispatcherIf { dispatchIf = \state (Message _ msg) -> cond msg } - - -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's @@ -265,8 +230,6 @@ handleAny handler = MessageDispatcherAny { ) } - - -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler @@ -274,8 +237,6 @@ data LocalServer s = LocalServer { terminateHandler :: TerminateHandler s -- ^ termination handler } - - ---- | Default record ---- Starting point for creating new servers defaultServer :: LocalServer s @@ -285,8 +246,6 @@ defaultServer = LocalServer { terminateHandler = \_ -> return () } - - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- @@ -297,8 +256,6 @@ startServer state handlers = spawnLocal $ do ST.runStateT (processServer handlers) state return () - - -- TODO startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerLink handlers = undefined @@ -307,8 +264,6 @@ startServerLink handlers = undefined --ref <- monitor them --return (them, ref) - - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do @@ -323,8 +278,6 @@ callServer sid timeout rq = do Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time - - -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do @@ -332,31 +285,22 @@ castServer sid msg = do say $ "Casting server " ++ show cid send sid (Message cid msg) - - -- | Stops a server identified by it's ServerId stopServer :: ServerId -> TerminateReason -> Process () stopServer sid reason = castServer sid (TerminateServer reason) - - -- | Get the server state getState :: Server s s getState = ST.get - - -- | Put the server state putState :: s -> Server s () putState = ST.put - - -- | Modify the server state modifyState :: (s -> s) -> Server s () modifyState = ST.modify - -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- @@ -372,8 +316,6 @@ processServer localServer = do InitStop r -> return (TerminateReason r) processTerminate localServer tr - - -- | initialize server processInit :: LocalServer s -> Server s InitResult processInit localServer = do @@ -381,8 +323,6 @@ processInit localServer = do ir <- initHandler localServer return ir - - -- | server loop processLoop :: LocalServer s -> Timeout -> Server s TerminateReason processLoop localServer t = do @@ -391,8 +331,6 @@ processLoop localServer t = do Just r -> return r Nothing -> processLoop localServer t - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -413,17 +351,12 @@ processReceive ds timeout = do trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | terminate server processTerminate :: LocalServer s -> TerminateReason -> Server s () processTerminate localServer reason = do trace $ "Server terminating: " ++ show reason (terminateHandler localServer) reason - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = ST.lift . say $ msg - From 197a055dd58a6a8dc55b705c1a787bab9e7b7740 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:32:02 +0000 Subject: [PATCH 0352/2357] fairly certain we *do* want to propagate the state change here --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 01d6d3c6..b0343a44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -345,7 +345,7 @@ processReceive ds timeout = do mayResult <- ST.lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do trace "Receive timed out ..." From cd8759ca11d98f2abf422ab1154560b8d8284625 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:32:02 +0000 Subject: [PATCH 0353/2357] fairly certain we *do* want to propagate the state change here --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 01d6d3c6..b0343a44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -345,7 +345,7 @@ processReceive ds timeout = do mayResult <- ST.lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do trace "Receive timed out ..." From db90bbe38c54177e6727e149b6c04574bf44ed39 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:32:02 +0000 Subject: [PATCH 0354/2357] fairly certain we *do* want to propagate the state change here --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 01d6d3c6..b0343a44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -345,7 +345,7 @@ processReceive ds timeout = do mayResult <- ST.lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do trace "Receive timed out ..." From 437953e53e29b097c1d385fc0a81ce47b814e680 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:32:02 +0000 Subject: [PATCH 0355/2357] fairly certain we *do* want to propagate the state change here --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 01d6d3c6..b0343a44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -345,7 +345,7 @@ processReceive ds timeout = do mayResult <- ST.lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do trace "Receive timed out ..." From c43f7d168906a3b5af3f1aa0636e132c7f539b07 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:36:07 +0000 Subject: [PATCH 0356/2357] stop all those compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b0343a44..438a2d4c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,8 +41,9 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, + Match, + Process, + ProcessId, expect, expectTimeout, getSelfPid, match, @@ -66,9 +67,6 @@ import Data.Typeable (Typeable) -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - -- | ServerId type ServerId = ProcessId @@ -166,9 +164,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s @@ -190,7 +188,7 @@ handleCallIf cond handler = MessageDispatcherIf { send cid resp return (s', Just (TerminateReason reason)) ), - dispatchIf = \state (Message _ req) -> cond req + dispatchIf = \_ (Message _ req) -> cond req } -- | Constructs a cast message dispatcher @@ -211,7 +209,7 @@ handleCastIf cond handler = MessageDispatcherIf { send sid m return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \_ (Message _ msg) -> cond msg } -- | Constructs a dispatcher for any message @@ -253,17 +251,9 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state + _ <- ST.runStateT (processServer handlers) state return () --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do From 414e7619ae930ef92ab43bfe739fd0ac0401abf4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:36:07 +0000 Subject: [PATCH 0357/2357] stop all those compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b0343a44..438a2d4c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,8 +41,9 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, + Match, + Process, + ProcessId, expect, expectTimeout, getSelfPid, match, @@ -66,9 +67,6 @@ import Data.Typeable (Typeable) -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - -- | ServerId type ServerId = ProcessId @@ -166,9 +164,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s @@ -190,7 +188,7 @@ handleCallIf cond handler = MessageDispatcherIf { send cid resp return (s', Just (TerminateReason reason)) ), - dispatchIf = \state (Message _ req) -> cond req + dispatchIf = \_ (Message _ req) -> cond req } -- | Constructs a cast message dispatcher @@ -211,7 +209,7 @@ handleCastIf cond handler = MessageDispatcherIf { send sid m return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \_ (Message _ msg) -> cond msg } -- | Constructs a dispatcher for any message @@ -253,17 +251,9 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state + _ <- ST.runStateT (processServer handlers) state return () --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do From fb7d4ffc0d98e301dc466982e2b6602e9ff38c3a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:36:07 +0000 Subject: [PATCH 0358/2357] stop all those compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b0343a44..438a2d4c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,8 +41,9 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, + Match, + Process, + ProcessId, expect, expectTimeout, getSelfPid, match, @@ -66,9 +67,6 @@ import Data.Typeable (Typeable) -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - -- | ServerId type ServerId = ProcessId @@ -166,9 +164,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s @@ -190,7 +188,7 @@ handleCallIf cond handler = MessageDispatcherIf { send cid resp return (s', Just (TerminateReason reason)) ), - dispatchIf = \state (Message _ req) -> cond req + dispatchIf = \_ (Message _ req) -> cond req } -- | Constructs a cast message dispatcher @@ -211,7 +209,7 @@ handleCastIf cond handler = MessageDispatcherIf { send sid m return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \_ (Message _ msg) -> cond msg } -- | Constructs a dispatcher for any message @@ -253,17 +251,9 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state + _ <- ST.runStateT (processServer handlers) state return () --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do From 01d7647f9746c0e772f79b3c7191d74d0417c903 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:36:07 +0000 Subject: [PATCH 0359/2357] stop all those compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b0343a44..438a2d4c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,8 +41,9 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, + Match, + Process, + ProcessId, expect, expectTimeout, getSelfPid, match, @@ -66,9 +67,6 @@ import Data.Typeable (Typeable) -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - -- | ServerId type ServerId = ProcessId @@ -166,9 +164,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s @@ -190,7 +188,7 @@ handleCallIf cond handler = MessageDispatcherIf { send cid resp return (s', Just (TerminateReason reason)) ), - dispatchIf = \state (Message _ req) -> cond req + dispatchIf = \_ (Message _ req) -> cond req } -- | Constructs a cast message dispatcher @@ -211,7 +209,7 @@ handleCastIf cond handler = MessageDispatcherIf { send sid m return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \_ (Message _ msg) -> cond msg } -- | Constructs a dispatcher for any message @@ -253,17 +251,9 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state + _ <- ST.runStateT (processServer handlers) state return () --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) - -- | call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do From 69d30fc7fd54e3419008caf1013feddb8bcbb2b5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:40:20 +0000 Subject: [PATCH 0360/2357] one for the 'make' crowd --- Makefile | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..707f404d --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +## ---------------------------------------------------------------------------- +## +## Copyright (c) 2005 - 2012 Nebularis. +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +## IN THE SOFTWARE. +## ---------------------------------------------------------------------------- + +.PHONY: all +all: build + +.PHONY: test +test: build + cabal test + +.PHONY: build +build: configure + cabal build + +.PHONY: configure +configure: ./dist/setup-config + +./dist/setup-config: + cabal configure --enable-tests + +.PHONY: clean +clean: + cabal clean From 006b75111224acfe531d762a1a9bbb82497a8559 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:40:20 +0000 Subject: [PATCH 0361/2357] one for the 'make' crowd --- Makefile | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..707f404d --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +## ---------------------------------------------------------------------------- +## +## Copyright (c) 2005 - 2012 Nebularis. +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +## IN THE SOFTWARE. +## ---------------------------------------------------------------------------- + +.PHONY: all +all: build + +.PHONY: test +test: build + cabal test + +.PHONY: build +build: configure + cabal build + +.PHONY: configure +configure: ./dist/setup-config + +./dist/setup-config: + cabal configure --enable-tests + +.PHONY: clean +clean: + cabal clean From 02e2f3391b0774cd380663ac6930a873082996f1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 11:40:20 +0000 Subject: [PATCH 0362/2357] one for the 'make' crowd --- Makefile | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..707f404d --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +## ---------------------------------------------------------------------------- +## +## Copyright (c) 2005 - 2012 Nebularis. +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +## IN THE SOFTWARE. +## ---------------------------------------------------------------------------- + +.PHONY: all +all: build + +.PHONY: test +test: build + cabal test + +.PHONY: build +build: configure + cabal build + +.PHONY: configure +configure: ./dist/setup-config + +./dist/setup-config: + cabal configure --enable-tests + +.PHONY: clean +clean: + cabal clean From fa08563edb6223eb21e1f6a14d9e8cdbccbe40a0 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0363/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 1 + src/Control/Distributed/Examples/Counter.hs | 46 ++- src/Control/Distributed/Examples/Kitty.hs | 29 +- src/Control/Distributed/Platform/GenServer.hs | 358 +++++++++--------- src/Main.hs | 7 +- 5 files changed, 239 insertions(+), 202 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..9cd65ca0 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,6 +27,7 @@ executable dtp , mtl -- , stm , derive + , transformers , distributed-static , distributed-process , distributed-process-simplelocalnet diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 1e850614..d0bfa3b1 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + c <- getState + trace $ "Counter init: " ++ show c + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Counter terminate: " ++ show r, + msgHandlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid NoTimeout IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid NoTimeout GetCount + return c @@ -88,19 +95,18 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 9620660b..60af1b19 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -7,6 +7,7 @@ module Control.Distributed.Examples.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,12 +69,25 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + cs <- getState + trace $ "Kitty init: " ++ show cs + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Kitty terminate: " ++ show r, + msgHandlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do @@ -106,22 +120,21 @@ closeShop sid = do handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..1173b61a 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -40,36 +36,43 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, - expect, - expectTimeout, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal) -import Control.Distributed.Process.Serializable (Serializable) -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) +import Control.Applicative (Applicative) +import Control.Exception (Exception, SomeException) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, + Process, ProcessId, + catchExit, exit, + expect, + expectTimeout, + getSelfPid, link, + match, matchAny, + matchIf, monitor, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + terminate) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Monad (void) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Data.Binary (Binary (..), + getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - - -- | ServerId type ServerId = ProcessId @@ -83,7 +86,10 @@ data Timeout = Timeout Int -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -111,62 +117,40 @@ $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) - -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) - +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk - -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) - -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult - +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { from :: ProcessId, payload :: a } + | CastMessage { from :: ProcessId, payload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s @@ -198,53 +182,41 @@ instance MessageMatcher MessageDispatcher where -- | Constructs a call message dispatcher -- -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \state (Message _ req) -> cond req -} - - - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - - - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage cid payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \state msg -> cond (payload msg) } @@ -252,15 +224,15 @@ handleCastIf cond handler = MessageDispatcherIf { -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -293,28 +265,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH handlers s + initH = initHandler ls + terminateH = terminateHandler ls + handlers = msgHandlers ls + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) --- | call a server identified by it's ServerId + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of NoTimeout -> expect Timeout time -> do @@ -329,14 +312,16 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason @@ -362,34 +347,36 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r @@ -400,30 +387,59 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of NoTimeout -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do putState s return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason +-- | Log a trace message using the underlying Process's say +trace :: String -> Server s () +trace msg = lift . say $ msg --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = ST.lift . say $ msg +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state + + + + +--bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c +--bracket = undefined + +--bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c +--bracket_ = undefined + +--finally :: Server s a -> Server s b -> Server s a + +--finally :: Process a -> Process b -> Process a +--finally a sequel = bracket_ (return ()) sequel a + +--myserver :: Server Int () +--myserver = do +-- receive (\msg -> +-- case msg of +-- \ResetCount -> undefined +-- \IncrementCount -> undefined) +-- receive (\DoNothing -> undefined) +-- return () + + diff --git a/src/Main.hs b/src/Main.hs index 643dd633..e32c084e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -41,8 +41,8 @@ main = do putStrLn "Transport created." localNode <- newLocalNode transport initRemoteTable putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) + runProcess localNode (kittyTest 10) `catch` \e -> print (e :: IOError) + runProcess localNode counterTest `catch` \e -> print (e :: IOError) putStrLn "Server started!" getChar return () @@ -62,7 +62,6 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid return () @@ -76,6 +75,8 @@ kittyTest n = do kittyTransactions kPid n say "-- Closing kitty shop ..." closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid return () From 4ace2f226b6fd988d2c91f574f87a9a5af27d8bd Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0364/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 1 + src/Control/Distributed/Examples/Counter.hs | 46 ++- src/Control/Distributed/Examples/Kitty.hs | 29 +- src/Control/Distributed/Platform/GenServer.hs | 358 +++++++++--------- src/Main.hs | 7 +- 5 files changed, 239 insertions(+), 202 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..9cd65ca0 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,6 +27,7 @@ executable dtp , mtl -- , stm , derive + , transformers , distributed-static , distributed-process , distributed-process-simplelocalnet diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 1e850614..d0bfa3b1 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + c <- getState + trace $ "Counter init: " ++ show c + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Counter terminate: " ++ show r, + msgHandlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid NoTimeout IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid NoTimeout GetCount + return c @@ -88,19 +95,18 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 9620660b..60af1b19 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -7,6 +7,7 @@ module Control.Distributed.Examples.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,12 +69,25 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + cs <- getState + trace $ "Kitty init: " ++ show cs + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Kitty terminate: " ++ show r, + msgHandlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do @@ -106,22 +120,21 @@ closeShop sid = do handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..1173b61a 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -40,36 +36,43 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, - expect, - expectTimeout, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal) -import Control.Distributed.Process.Serializable (Serializable) -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) +import Control.Applicative (Applicative) +import Control.Exception (Exception, SomeException) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, + Process, ProcessId, + catchExit, exit, + expect, + expectTimeout, + getSelfPid, link, + match, matchAny, + matchIf, monitor, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + terminate) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Monad (void) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Data.Binary (Binary (..), + getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - - -- | ServerId type ServerId = ProcessId @@ -83,7 +86,10 @@ data Timeout = Timeout Int -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -111,62 +117,40 @@ $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) - -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) - +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk - -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) - -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult - +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { from :: ProcessId, payload :: a } + | CastMessage { from :: ProcessId, payload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s @@ -198,53 +182,41 @@ instance MessageMatcher MessageDispatcher where -- | Constructs a call message dispatcher -- -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \state (Message _ req) -> cond req -} - - - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - - - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage cid payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \state msg -> cond (payload msg) } @@ -252,15 +224,15 @@ handleCastIf cond handler = MessageDispatcherIf { -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -293,28 +265,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH handlers s + initH = initHandler ls + terminateH = terminateHandler ls + handlers = msgHandlers ls + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) --- | call a server identified by it's ServerId + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of NoTimeout -> expect Timeout time -> do @@ -329,14 +312,16 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason @@ -362,34 +347,36 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r @@ -400,30 +387,59 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of NoTimeout -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do putState s return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason +-- | Log a trace message using the underlying Process's say +trace :: String -> Server s () +trace msg = lift . say $ msg --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = ST.lift . say $ msg +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state + + + + +--bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c +--bracket = undefined + +--bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c +--bracket_ = undefined + +--finally :: Server s a -> Server s b -> Server s a + +--finally :: Process a -> Process b -> Process a +--finally a sequel = bracket_ (return ()) sequel a + +--myserver :: Server Int () +--myserver = do +-- receive (\msg -> +-- case msg of +-- \ResetCount -> undefined +-- \IncrementCount -> undefined) +-- receive (\DoNothing -> undefined) +-- return () + + diff --git a/src/Main.hs b/src/Main.hs index 643dd633..e32c084e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -41,8 +41,8 @@ main = do putStrLn "Transport created." localNode <- newLocalNode transport initRemoteTable putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) + runProcess localNode (kittyTest 10) `catch` \e -> print (e :: IOError) + runProcess localNode counterTest `catch` \e -> print (e :: IOError) putStrLn "Server started!" getChar return () @@ -62,7 +62,6 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid return () @@ -76,6 +75,8 @@ kittyTest n = do kittyTransactions kPid n say "-- Closing kitty shop ..." closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid return () From bceb3a17e73265bb56cd7aa9b3f2e033155614ec Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0365/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 1 + src/Control/Distributed/Examples/Counter.hs | 46 ++- src/Control/Distributed/Examples/Kitty.hs | 29 +- src/Control/Distributed/Platform/GenServer.hs | 358 +++++++++--------- src/Main.hs | 7 +- 5 files changed, 239 insertions(+), 202 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..9cd65ca0 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,6 +27,7 @@ executable dtp , mtl -- , stm , derive + , transformers , distributed-static , distributed-process , distributed-process-simplelocalnet diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 1e850614..d0bfa3b1 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + c <- getState + trace $ "Counter init: " ++ show c + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Counter terminate: " ++ show r, + msgHandlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid NoTimeout IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid NoTimeout GetCount + return c @@ -88,19 +95,18 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 9620660b..60af1b19 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -7,6 +7,7 @@ module Control.Distributed.Examples.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,12 +69,25 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + cs <- getState + trace $ "Kitty init: " ++ show cs + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Kitty terminate: " ++ show r, + msgHandlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do @@ -106,22 +120,21 @@ closeShop sid = do handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..1173b61a 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -40,36 +36,43 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, - expect, - expectTimeout, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal) -import Control.Distributed.Process.Serializable (Serializable) -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) +import Control.Applicative (Applicative) +import Control.Exception (Exception, SomeException) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, + Process, ProcessId, + catchExit, exit, + expect, + expectTimeout, + getSelfPid, link, + match, matchAny, + matchIf, monitor, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + terminate) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Monad (void) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Data.Binary (Binary (..), + getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - - -- | ServerId type ServerId = ProcessId @@ -83,7 +86,10 @@ data Timeout = Timeout Int -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -111,62 +117,40 @@ $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) - -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) - +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk - -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) - -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult - +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { from :: ProcessId, payload :: a } + | CastMessage { from :: ProcessId, payload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s @@ -198,53 +182,41 @@ instance MessageMatcher MessageDispatcher where -- | Constructs a call message dispatcher -- -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \state (Message _ req) -> cond req -} - - - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - - - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage cid payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \state msg -> cond (payload msg) } @@ -252,15 +224,15 @@ handleCastIf cond handler = MessageDispatcherIf { -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -293,28 +265,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH handlers s + initH = initHandler ls + terminateH = terminateHandler ls + handlers = msgHandlers ls + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) --- | call a server identified by it's ServerId + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of NoTimeout -> expect Timeout time -> do @@ -329,14 +312,16 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason @@ -362,34 +347,36 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r @@ -400,30 +387,59 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of NoTimeout -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do putState s return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason +-- | Log a trace message using the underlying Process's say +trace :: String -> Server s () +trace msg = lift . say $ msg --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = ST.lift . say $ msg +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state + + + + +--bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c +--bracket = undefined + +--bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c +--bracket_ = undefined + +--finally :: Server s a -> Server s b -> Server s a + +--finally :: Process a -> Process b -> Process a +--finally a sequel = bracket_ (return ()) sequel a + +--myserver :: Server Int () +--myserver = do +-- receive (\msg -> +-- case msg of +-- \ResetCount -> undefined +-- \IncrementCount -> undefined) +-- receive (\DoNothing -> undefined) +-- return () + + diff --git a/src/Main.hs b/src/Main.hs index 643dd633..e32c084e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -41,8 +41,8 @@ main = do putStrLn "Transport created." localNode <- newLocalNode transport initRemoteTable putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) + runProcess localNode (kittyTest 10) `catch` \e -> print (e :: IOError) + runProcess localNode counterTest `catch` \e -> print (e :: IOError) putStrLn "Server started!" getChar return () @@ -62,7 +62,6 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid return () @@ -76,6 +75,8 @@ kittyTest n = do kittyTransactions kPid n say "-- Closing kitty shop ..." closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid return () From 1c3e8bd09c8ec024c88bb1e3890092c5f8dca4b0 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0366/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 1 + src/Control/Distributed/Examples/Counter.hs | 46 ++- src/Control/Distributed/Examples/Kitty.hs | 29 +- src/Control/Distributed/Platform/GenServer.hs | 358 +++++++++--------- src/Main.hs | 7 +- 5 files changed, 239 insertions(+), 202 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1a1c1be..9cd65ca0 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,6 +27,7 @@ executable dtp , mtl -- , stm , derive + , transformers , distributed-static , distributed-process , distributed-process-simplelocalnet diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index 1e850614..d0bfa3b1 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + c <- getState + trace $ "Counter init: " ++ show c + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Counter terminate: " ++ show r, + msgHandlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid NoTimeout IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid NoTimeout GetCount + return c @@ -88,19 +95,18 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 9620660b..60af1b19 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -7,6 +7,7 @@ module Control.Distributed.Examples.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,12 +69,25 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + cs <- getState + trace $ "Kitty init: " ++ show cs + initOk NoTimeout, + terminateHandler = \r -> + trace $ "Kitty terminate: " ++ show r, + msgHandlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do @@ -106,22 +120,21 @@ closeShop sid = do handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c60432a4..1173b61a 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -40,36 +36,43 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, MonitorRef, - Process, ProcessId, - expect, - expectTimeout, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal) -import Control.Distributed.Process.Serializable (Serializable) -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) +import Control.Applicative (Applicative) +import Control.Exception (Exception, SomeException) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, + Process, ProcessId, + catchExit, exit, + expect, + expectTimeout, + getSelfPid, link, + match, matchAny, + matchIf, monitor, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + terminate) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Monad (void) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Data.Binary (Binary (..), + getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- --- | Process name -type Name = String - - -- | ServerId type ServerId = ProcessId @@ -83,7 +86,10 @@ data Timeout = Timeout Int -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -111,62 +117,40 @@ $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) - -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) - +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk - -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) - -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult - +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { from :: ProcessId, payload :: a } + | CastMessage { from :: ProcessId, payload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s @@ -198,53 +182,41 @@ instance MessageMatcher MessageDispatcher where -- | Constructs a call message dispatcher -- -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \state (Message _ req) -> cond req -} - - - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - - - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage cid payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \state (Message _ msg) -> cond msg + dispatchIf = \state msg -> cond (payload msg) } @@ -252,15 +224,15 @@ handleCastIf cond handler = MessageDispatcherIf { -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -293,28 +265,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH handlers s + initH = initHandler ls + terminateH = terminateHandler ls + handlers = msgHandlers ls + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- TODO -startServerLink :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerLink handlers = undefined - --us <- getSelfPid - --them <- spawn nid (cpLink us `seqCP` proc) - --ref <- monitor them - --return (them, ref) +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) --- | call a server identified by it's ServerId + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of NoTimeout -> expect Timeout time -> do @@ -329,14 +312,16 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason @@ -362,34 +347,36 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r @@ -400,30 +387,59 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of NoTimeout -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do putState s return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason +-- | Log a trace message using the underlying Process's say +trace :: String -> Server s () +trace msg = lift . say $ msg --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = ST.lift . say $ msg +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state + + + + +--bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c +--bracket = undefined + +--bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c +--bracket_ = undefined + +--finally :: Server s a -> Server s b -> Server s a + +--finally :: Process a -> Process b -> Process a +--finally a sequel = bracket_ (return ()) sequel a + +--myserver :: Server Int () +--myserver = do +-- receive (\msg -> +-- case msg of +-- \ResetCount -> undefined +-- \IncrementCount -> undefined) +-- receive (\DoNothing -> undefined) +-- return () + + diff --git a/src/Main.hs b/src/Main.hs index 643dd633..e32c084e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -41,8 +41,8 @@ main = do putStrLn "Transport created." localNode <- newLocalNode transport initRemoteTable putStrLn "Local node created." - runProcess localNode (kittyTest 1000) `catch` \e -> print (e :: IOError) - --runProcess localNode counterTest `catch` \e -> print (e :: IOError) + runProcess localNode (kittyTest 10) `catch` \e -> print (e :: IOError) + runProcess localNode counterTest `catch` \e -> print (e :: IOError) putStrLn "Server started!" getChar return () @@ -62,7 +62,6 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid return () @@ -76,6 +75,8 @@ kittyTest n = do kittyTransactions kPid n say "-- Closing kitty shop ..." closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid return () From 710c11ee0bde7ee6d3ceb149dc47e2095d920a49 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0367/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..74780ff1 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From a05577165286ea40eed9443de0243ccd4eabd7ec Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0368/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..74780ff1 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From dd27d1d9041fb59296a00603d47028d0c937b9f6 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0369/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..74780ff1 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From 0e2ff6469bdef674a10d0b0bb73b15d680c24f72 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0370/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..74780ff1 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From 8503ec9f5662f27ec5cdcdd7e30c25280db08e56 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0371/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From 184726c820d50d31c120372b6acf3ff3f812af15 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0372/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From c403788325e723de686be24af3da563224f6cb3e Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0373/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From e37e4ca2d09af4b36e3762a9c8ad18c2b87a0436 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0374/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 1173b61a..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -419,27 +419,3 @@ runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state - - ---bracket :: Server s a -> (a -> Server s b) -> (a -> Server s c) -> Server s c ---bracket = undefined - ---bracket_ :: Server s a -> (a -> Server s b) -> Server s c -> Server s c ---bracket_ = undefined - ---finally :: Server s a -> Server s b -> Server s a - ---finally :: Process a -> Process b -> Process a ---finally a sequel = bracket_ (return ()) sequel a - ---myserver :: Server Int () ---myserver = do --- receive (\msg -> --- case msg of --- \ResetCount -> undefined --- \IncrementCount -> undefined) --- receive (\DoNothing -> undefined) --- return () - - - From eba5492583d3914cb6c2e5ef861a65f8a8b6a078 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:21:11 -0500 Subject: [PATCH 0375/2357] cleaned up junk code (part 2) --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 74780ff1..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 08581006f0ad3916f708023ed9a5f9866723253a Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:21:11 -0500 Subject: [PATCH 0376/2357] cleaned up junk code (part 2) --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 74780ff1..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 72b5d5a2b1cf6f2547ec1c3c40702c474faa4fdf Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:21:11 -0500 Subject: [PATCH 0377/2357] cleaned up junk code (part 2) --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 74780ff1..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From d8b6b82c6791ff258fadc5b23bf827ece452717c Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:21:11 -0500 Subject: [PATCH 0378/2357] cleaned up junk code (part 2) --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 74780ff1..a40f580d 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -86,7 +86,7 @@ data Timeout = Timeout Int -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From f1b1bc59ca0015aa4a09d50ee1315c059b976cce Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 21:12:56 +0000 Subject: [PATCH 0379/2357] introduce common types; merge timer; refactor tests --- distributed-process-platform.cabal | 2 +- .../Distributed/Platform/GenProcess.hs | 244 ++++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 16 +- .../Distributed/Platform/Internal/Types.hs | 38 +++ src/Control/Distributed/Platform/Timer.hs | 168 ++++++++++++ tests/TestGenProcess.hs | 5 + tests/TestGenServer.hs | 11 +- tests/TestMain.hs | 42 ++- 8 files changed, 486 insertions(+), 40 deletions(-) create mode 100644 src/Control/Distributed/Platform/GenProcess.hs create mode 100644 src/Control/Distributed/Platform/Internal/Types.hs create mode 100644 src/Control/Distributed/Platform/Timer.hs create mode 100644 tests/TestGenProcess.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8207fa9a..2349f3b1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,7 +27,7 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: Control.Distributed.Platform.GenProcess, + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs new file mode 100644 index 00000000..2d893c62 --- /dev/null +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -0,0 +1,244 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Control.Distributed.Platform.GenProcess where + +-- TODO: define API and hide internals... + +import qualified Control.Distributed.Process as BaseProcess +import qualified Control.Monad.State as ST (StateT, get, + lift, modify, + put, runStateT) + +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer (intervalToMs) +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + + +type ServerName = String +type ServerPid = BaseProcess.ProcessId + +data ServerId = ServerProcess ServerPid | NamedServer ServerName + +data Recipient a = SendToPid BaseProcess.ProcessId | + SendToPort (BaseProcess.SendPort a) + +-- | Initialize handler result +data InitResult = + InitOk Timeout + | InitStop String + +-- | Terminate reason +data TerminateReason = + TerminateNormal + | TerminateShutdown + | TerminateReason String + deriving (Show, Typeable) +$(derive makeBinary ''TerminateReason) + +data ReplyTo = ReplyTo BaseProcess.ProcessId | None + deriving (Typeable, Show) +$(derive makeBinary ''ReplyTo) + +-- | The result of a call +data ProcessAction = + ProcessContinue + | ProcessTimeout Timeout + | ProcessStop String + deriving (Typeable) +$(derive makeBinary ''ProcessAction) + +type Process s = ST.StateT s BaseProcess.Process + +-- | Handlers +type InitHandler s = Process s InitResult +type TerminateHandler s = TerminateReason -> Process s () +type RequestHandler s a = Message a -> Process s ProcessAction + +-- | Contains the actual payload and possibly additional routing metadata +data Message a = Message ReplyTo a + deriving (Show, Typeable) +$(derive makeBinary ''Message) + +data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) + deriving (Typeable) +$(derive makeBinary ''Rpc) + +-- | Dispatcher that knows how to dispatch messages to a handler +data Dispatcher s = + forall a . (Serializable a) => + Dispatch { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction) } | + forall a . (Serializable a) => + DispatchIf { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction), + condition :: s -> Message a -> Bool } + +-- dispatching to implementation callbacks + +-- | Matches messages using a dispatcher +class Dispatchable d where + matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) + +-- | Matches messages to a MessageDispatcher +instance Dispatchable Dispatcher where + matchMessage s (Dispatch d ) = BaseProcess.match (d s) + matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) + + +data Behaviour s = Behaviour { + initHandler :: InitHandler s -- ^ initialization handler + , dispatchers :: [Dispatcher s] + , terminateHandler :: TerminateHandler s -- ^ termination handler + } + +-- | Management message +-- TODO is there a std way of terminating a process from another process? +data Termination = Terminate TerminateReason + deriving (Show, Typeable) +$(derive makeBinary ''Termination) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Start a new server and return it's id +-- start :: Behaviour s -> Process ProcessId +-- start handlers = spawnLocal $ runProcess handlers + +reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () +reply (ReplyTo pid) m = BaseProcess.send pid m +reply _ _ = return () + +replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> + BaseProcess.Process () +replyVia p m = BaseProcess.sendChan p m + +-- | Given a state, behaviour specificiation and spawn function, +-- starts a new server and return its id. The spawn function is typically +-- one taken from "Control.Distributed.Process". +-- see 'Control.Distributed.Process.spawn' +-- 'Control.Distributed.Process.spawnLocal' +-- 'Control.Distributed.Process.spawnLink' +-- 'Control.Distributed.Process.spawnMonitor' +-- 'Control.Distributed.Process.spawnSupervised' +start :: + s -> Behaviour s -> + (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> + BaseProcess.Process BaseProcess.ProcessId +start state handlers spawn = spawn $ do + _ <- ST.runStateT (runProc handlers) state + return () + +send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () +send s m = do + let msg = (Message None m) + case s of + ServerProcess pid -> BaseProcess.send pid msg + NamedServer name -> BaseProcess.nsend name msg + +-- process request handling + +handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s +handleRequest = handleRequestIf (const True) + +handleRequestIf :: (Serializable a) => (a -> Bool) -> + RequestHandler s a -> Dispatcher s +handleRequestIf cond handler = DispatchIf { + dispatch = (\state m@(Message _ _) -> do + (r, s') <- ST.runStateT (handler m) state + return (s', r) + ), + condition = \_ (Message _ req) -> cond req +} + +-- process state management + +-- | gets the process state +getState :: Process s s +getState = ST.get + +-- | sets the process state +putState :: s -> Process s () +putState = ST.put + +-- | modifies the server state +modifyState :: (s -> s) -> Process s () +modifyState = ST.modify + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | server process +runProc :: Behaviour s -> Process s () +runProc s = do + ir <- init s + tr <- case ir of + InitOk t -> do + trace $ "Server ready to receive messages!" + loop s t + InitStop r -> return (TerminateReason r) + terminate s tr + +-- | initialize server +init :: Behaviour s -> Process s InitResult +init s = do + trace $ "Server initializing ... " + ir <- initHandler s + return ir + +loop :: Behaviour s -> Timeout -> Process s TerminateReason +loop s t = do + s' <- processReceive (dispatchers s) t + nextAction s s' + where nextAction :: Behaviour s -> ProcessAction -> + Process s TerminateReason + nextAction b ProcessContinue = loop b t + nextAction b (ProcessTimeout t') = loop b t' + nextAction _ (ProcessStop r) = return (TerminateReason r) + +processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction +processReceive ds timeout = do + s <- getState + let ms = map (matchMessage s) ds + -- TODO: should we drain the message queue to avoid selective receive here? + case timeout of + Infinity -> do + (s', r) <- ST.lift $ BaseProcess.receiveWait ms + putState s' + return r + Timeout t -> do + result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms + case result of + Just (s', r) -> do + putState s' + return r + Nothing -> do + return $ ProcessStop "timed out" + +terminate :: Behaviour s -> TerminateReason -> Process s () +terminate s reason = do + trace $ "Server terminating: " ++ show reason + (terminateHandler s) reason + +-- | Log a trace message using the underlying Process's say +trace :: String -> Process s () +trace msg = ST.lift . BaseProcess.say $ msg + +-- data Upgrade = ??? +-- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to +-- a remote pid? if so then we may hot server-code loading quite easily... + diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 438a2d4c..b64b736f 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -52,6 +52,8 @@ import Control.Distributed.Process (AbstractMessage (forw receiveWait, say, send, spawnLocal) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer import qualified Control.Monad.State as ST (StateT, get, lift, modify, put, @@ -70,10 +72,6 @@ import Data.Typeable (Typeable) -- | ServerId type ServerId = ProcessId --- | Timeout -data Timeout = Timeout Int - | NoTimeout - -- | Server monad type Server s = ST.StateT s Process @@ -239,7 +237,7 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk NoTimeout, + initHandler = return $ InitOk Infinity, msgHandlers = [], terminateHandler = \_ -> return () } @@ -261,9 +259,9 @@ callServer sid timeout rq = do say $ "Calling server " ++ show cid send sid (Message cid rq) case timeout of - NoTimeout -> expect + Infinity -> expect Timeout time -> do - mayResp <- expectTimeout time + mayResp <- expectTimeout (intervalToMs time) case mayResp of Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time @@ -327,12 +325,12 @@ processReceive ds timeout = do s <- getState let ms = map (matchMessage s) ds case timeout of - NoTimeout -> do + Infinity -> do (s', r) <- ST.lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs new file mode 100644 index 00000000..a0721940 --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +-- | Types used throughout the Cloud Haskell framework +-- +module Control.Distributed.Platform.Internal.Types ( + TimeUnit(..) + , TimeInterval(..) + , Timeout(..) + ) where + +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | Defines the time unit for a Timeout value +data TimeUnit = Hours | Minutes | Seconds | Millis + deriving (Typeable, Show) +$(derive makeBinary ''TimeUnit) + +data TimeInterval = TimeInterval TimeUnit Int + deriving (Typeable, Show) +$(derive makeBinary ''TimeInterval) + +-- | Defines a Timeout value (and unit of measure) or +-- sets it to infinity (no timeout) +data Timeout = Timeout TimeInterval | Infinity + deriving (Typeable, Show) +$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs new file mode 100644 index 00000000..e074e510 --- /dev/null +++ b/src/Control/Distributed/Platform/Timer.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module Control.Distributed.Platform.Timer ( + TimerRef + , TimeInterval(..) + , TimeUnit(..) + , Tick(Tick) + , sleep + , sendAfter + , runAfter + , startTimer + , ticker + , periodically + , resetTimer + , cancelTimer + , flushTimer + -- time interval handling + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Process +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | an opaque reference to a timer +type TimerRef = ProcessId + +-- | cancellation message sent to timers +data TimerConfig = Reset | Cancel + deriving (Typeable, Show) +$(derive makeBinary ''TimerConfig) + +-- | represents a 'tick' event that timers can generate +data Tick = Tick + deriving (Typeable, Eq) +$(derive makeBinary ''Tick) + +data SleepingPill = SleepingPill + deriving (Typeable) +$(derive makeBinary ''SleepingPill) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- | blocks the calling Process for the specified TimeInterval. Note that this +-- function assumes that a blocking receive is the most efficient approach to +-- acheiving this, so expect the runtime semantics (particularly with regards +-- scheduling) to differ from threadDelay and/or operating system specific +-- functions that offer the same results. +sleep :: TimeInterval -> Process () +sleep t = do + let ms = intervalToMs t + _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) + (\_ -> return ())] + return () + +-- | starts a timer which sends the supplied message to the destination process +-- after the specified time interval. +sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +sendAfter t pid msg = runAfter t (mkSender pid msg) + +-- | runs the supplied process action(s) after `t' has elapsed +runAfter :: TimeInterval -> Process () -> Process TimerRef +runAfter t p = spawnLocal $ runTimer t p True + +-- | starts a timer that repeatedly sends the supplied message to the destination +-- process each time the specified time interval elapses. To stop messages from +-- being sent in future, cancelTimer can be called. +startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +startTimer t pid msg = periodically t (mkSender pid msg) + +-- | runs the supplied process action(s) repeatedly at intervals of `t' +periodically :: TimeInterval -> Process () -> Process TimerRef +periodically t p = spawnLocal $ runTimer t p False + +-- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- a timer's messages are prevented from being delivered to the target process. +-- Also note that resetting an ongoing timer (started using the `startTimer' or +-- `periodically' functions) will only cause the current elapsed period to time +-- out, after which the timer will continue running. To stop a long-running +-- timer, you should use `cancelTimer' instead. +resetTimer :: TimerRef -> Process () +resetTimer = (flip send) Reset + +cancelTimer :: TimerRef -> Process () +cancelTimer = (flip send) Cancel + +-- | cancels a running timer and flushes any viable timer messages from the +-- process' message queue. This function should only be called by the process +-- expecting to receive the timer's messages! +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer ref ignore t = do + cancelTimer ref + -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing + _ <- receiveTimeout (intervalToMs t) [ + matchIf (\x -> x == ignore) + (\_ -> return ()) ] + return () + +-- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +ticker :: TimeInterval -> ProcessId -> Process TimerRef +ticker t pid = startTimer t pid Tick + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- runs the timer process +runTimer :: TimeInterval -> Process () -> Bool -> Process () +runTimer t proc cancelOnReset = do + cancel <- expectTimeout (intervalToMs t) + -- say $ "cancel = " ++ (show cancel) ++ "\n" + case cancel of + Nothing -> runProc cancelOnReset + Just Cancel -> return () + Just Reset -> if cancelOnReset then return () + else runTimer t proc cancelOnReset + where runProc True = proc + runProc False = proc >> runTimer t proc cancelOnReset + +-- create a 'sender' action for dispatching `msg' to `pid' +mkSender :: (Serializable a) => ProcessId -> a -> Process () +mkSender pid msg = do + -- say "sending\n" + send pid msg diff --git a/tests/TestGenProcess.hs b/tests/TestGenProcess.hs new file mode 100644 index 00000000..7a570a72 --- /dev/null +++ b/tests/TestGenProcess.hs @@ -0,0 +1,5 @@ +module TestGenProcess where + +import Test.HUnit + +test1 = TestCase (assertEqual "1 == 2" 1 2) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 9b6c5ea6..ecee315f 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -81,10 +81,13 @@ testPing transport = do takeMVar clientDone - -genServerTests :: (NT.Transport, TransportInternals) -> [Test] -genServerTests (transport, _) = [ +tests :: NT.Transport -> [Test] +tests transport = [ testGroup "Basic features" [ - testCase "Ping" (testPing transport) + testCase "Ping" (testPing transport) ] ] + +genServerTests :: NT.Transport -> TransportInternals -> IO [Test] +genServerTests transport _ = do + return (tests transport) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 187a367c..c6f75c46 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,33 +6,23 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Control.Applicative -import Control.Monad -import Test.Framework (Test, defaultMain, - testGroup) +import Test.Framework (Test, defaultMain, testGroup) +import qualified Network.Transport as NT +import Network.Transport.TCP +import TestGenServer (genServerTests) +import TestTimer (timerTests) -import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP - ( createTransportExposeInternals - , TransportInternals(socketBetween) - , defaultTCPParameters - ) -import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable (Serializable) - -import TestGenServer - -tests :: (NT.Transport, TransportInternals) -> [Test] -tests transportConfig = [ - testGroup "GenServer" (genServerTests transportConfig) - ] +tests :: NT.Transport -> TransportInternals -> IO [Test] +tests transport internals = do + gsTestGroup <- genServerTests transport internals + timerTestGroup <- timerTests transport internals + return [ + testGroup "GenServer" gsTestGroup + , testGroup "Timer" timerTestGroup ] main :: IO () main = do - Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) \ No newline at end of file + Right (transport, internals) <- createTransportExposeInternals + "127.0.0.1" "8080" defaultTCPParameters + testData <- tests transport internals + defaultMain testData From 3511088a18511642543f0601128edfa4b8709861 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 21:12:56 +0000 Subject: [PATCH 0380/2357] introduce common types; merge timer; refactor tests --- distributed-process-platform.cabal | 2 +- .../Distributed/Platform/GenProcess.hs | 244 ++++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 16 +- .../Distributed/Platform/Internal/Types.hs | 38 +++ src/Control/Distributed/Platform/Timer.hs | 168 ++++++++++++ tests/TestGenProcess.hs | 5 + tests/TestGenServer.hs | 11 +- tests/TestMain.hs | 42 ++- 8 files changed, 486 insertions(+), 40 deletions(-) create mode 100644 src/Control/Distributed/Platform/GenProcess.hs create mode 100644 src/Control/Distributed/Platform/Internal/Types.hs create mode 100644 src/Control/Distributed/Platform/Timer.hs create mode 100644 tests/TestGenProcess.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8207fa9a..2349f3b1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,7 +27,7 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: Control.Distributed.Platform.GenProcess, + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs new file mode 100644 index 00000000..2d893c62 --- /dev/null +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -0,0 +1,244 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Control.Distributed.Platform.GenProcess where + +-- TODO: define API and hide internals... + +import qualified Control.Distributed.Process as BaseProcess +import qualified Control.Monad.State as ST (StateT, get, + lift, modify, + put, runStateT) + +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer (intervalToMs) +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + + +type ServerName = String +type ServerPid = BaseProcess.ProcessId + +data ServerId = ServerProcess ServerPid | NamedServer ServerName + +data Recipient a = SendToPid BaseProcess.ProcessId | + SendToPort (BaseProcess.SendPort a) + +-- | Initialize handler result +data InitResult = + InitOk Timeout + | InitStop String + +-- | Terminate reason +data TerminateReason = + TerminateNormal + | TerminateShutdown + | TerminateReason String + deriving (Show, Typeable) +$(derive makeBinary ''TerminateReason) + +data ReplyTo = ReplyTo BaseProcess.ProcessId | None + deriving (Typeable, Show) +$(derive makeBinary ''ReplyTo) + +-- | The result of a call +data ProcessAction = + ProcessContinue + | ProcessTimeout Timeout + | ProcessStop String + deriving (Typeable) +$(derive makeBinary ''ProcessAction) + +type Process s = ST.StateT s BaseProcess.Process + +-- | Handlers +type InitHandler s = Process s InitResult +type TerminateHandler s = TerminateReason -> Process s () +type RequestHandler s a = Message a -> Process s ProcessAction + +-- | Contains the actual payload and possibly additional routing metadata +data Message a = Message ReplyTo a + deriving (Show, Typeable) +$(derive makeBinary ''Message) + +data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) + deriving (Typeable) +$(derive makeBinary ''Rpc) + +-- | Dispatcher that knows how to dispatch messages to a handler +data Dispatcher s = + forall a . (Serializable a) => + Dispatch { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction) } | + forall a . (Serializable a) => + DispatchIf { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction), + condition :: s -> Message a -> Bool } + +-- dispatching to implementation callbacks + +-- | Matches messages using a dispatcher +class Dispatchable d where + matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) + +-- | Matches messages to a MessageDispatcher +instance Dispatchable Dispatcher where + matchMessage s (Dispatch d ) = BaseProcess.match (d s) + matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) + + +data Behaviour s = Behaviour { + initHandler :: InitHandler s -- ^ initialization handler + , dispatchers :: [Dispatcher s] + , terminateHandler :: TerminateHandler s -- ^ termination handler + } + +-- | Management message +-- TODO is there a std way of terminating a process from another process? +data Termination = Terminate TerminateReason + deriving (Show, Typeable) +$(derive makeBinary ''Termination) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Start a new server and return it's id +-- start :: Behaviour s -> Process ProcessId +-- start handlers = spawnLocal $ runProcess handlers + +reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () +reply (ReplyTo pid) m = BaseProcess.send pid m +reply _ _ = return () + +replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> + BaseProcess.Process () +replyVia p m = BaseProcess.sendChan p m + +-- | Given a state, behaviour specificiation and spawn function, +-- starts a new server and return its id. The spawn function is typically +-- one taken from "Control.Distributed.Process". +-- see 'Control.Distributed.Process.spawn' +-- 'Control.Distributed.Process.spawnLocal' +-- 'Control.Distributed.Process.spawnLink' +-- 'Control.Distributed.Process.spawnMonitor' +-- 'Control.Distributed.Process.spawnSupervised' +start :: + s -> Behaviour s -> + (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> + BaseProcess.Process BaseProcess.ProcessId +start state handlers spawn = spawn $ do + _ <- ST.runStateT (runProc handlers) state + return () + +send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () +send s m = do + let msg = (Message None m) + case s of + ServerProcess pid -> BaseProcess.send pid msg + NamedServer name -> BaseProcess.nsend name msg + +-- process request handling + +handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s +handleRequest = handleRequestIf (const True) + +handleRequestIf :: (Serializable a) => (a -> Bool) -> + RequestHandler s a -> Dispatcher s +handleRequestIf cond handler = DispatchIf { + dispatch = (\state m@(Message _ _) -> do + (r, s') <- ST.runStateT (handler m) state + return (s', r) + ), + condition = \_ (Message _ req) -> cond req +} + +-- process state management + +-- | gets the process state +getState :: Process s s +getState = ST.get + +-- | sets the process state +putState :: s -> Process s () +putState = ST.put + +-- | modifies the server state +modifyState :: (s -> s) -> Process s () +modifyState = ST.modify + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | server process +runProc :: Behaviour s -> Process s () +runProc s = do + ir <- init s + tr <- case ir of + InitOk t -> do + trace $ "Server ready to receive messages!" + loop s t + InitStop r -> return (TerminateReason r) + terminate s tr + +-- | initialize server +init :: Behaviour s -> Process s InitResult +init s = do + trace $ "Server initializing ... " + ir <- initHandler s + return ir + +loop :: Behaviour s -> Timeout -> Process s TerminateReason +loop s t = do + s' <- processReceive (dispatchers s) t + nextAction s s' + where nextAction :: Behaviour s -> ProcessAction -> + Process s TerminateReason + nextAction b ProcessContinue = loop b t + nextAction b (ProcessTimeout t') = loop b t' + nextAction _ (ProcessStop r) = return (TerminateReason r) + +processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction +processReceive ds timeout = do + s <- getState + let ms = map (matchMessage s) ds + -- TODO: should we drain the message queue to avoid selective receive here? + case timeout of + Infinity -> do + (s', r) <- ST.lift $ BaseProcess.receiveWait ms + putState s' + return r + Timeout t -> do + result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms + case result of + Just (s', r) -> do + putState s' + return r + Nothing -> do + return $ ProcessStop "timed out" + +terminate :: Behaviour s -> TerminateReason -> Process s () +terminate s reason = do + trace $ "Server terminating: " ++ show reason + (terminateHandler s) reason + +-- | Log a trace message using the underlying Process's say +trace :: String -> Process s () +trace msg = ST.lift . BaseProcess.say $ msg + +-- data Upgrade = ??? +-- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to +-- a remote pid? if so then we may hot server-code loading quite easily... + diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 438a2d4c..b64b736f 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -52,6 +52,8 @@ import Control.Distributed.Process (AbstractMessage (forw receiveWait, say, send, spawnLocal) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer import qualified Control.Monad.State as ST (StateT, get, lift, modify, put, @@ -70,10 +72,6 @@ import Data.Typeable (Typeable) -- | ServerId type ServerId = ProcessId --- | Timeout -data Timeout = Timeout Int - | NoTimeout - -- | Server monad type Server s = ST.StateT s Process @@ -239,7 +237,7 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk NoTimeout, + initHandler = return $ InitOk Infinity, msgHandlers = [], terminateHandler = \_ -> return () } @@ -261,9 +259,9 @@ callServer sid timeout rq = do say $ "Calling server " ++ show cid send sid (Message cid rq) case timeout of - NoTimeout -> expect + Infinity -> expect Timeout time -> do - mayResp <- expectTimeout time + mayResp <- expectTimeout (intervalToMs time) case mayResp of Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time @@ -327,12 +325,12 @@ processReceive ds timeout = do s <- getState let ms = map (matchMessage s) ds case timeout of - NoTimeout -> do + Infinity -> do (s', r) <- ST.lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs new file mode 100644 index 00000000..a0721940 --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +-- | Types used throughout the Cloud Haskell framework +-- +module Control.Distributed.Platform.Internal.Types ( + TimeUnit(..) + , TimeInterval(..) + , Timeout(..) + ) where + +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | Defines the time unit for a Timeout value +data TimeUnit = Hours | Minutes | Seconds | Millis + deriving (Typeable, Show) +$(derive makeBinary ''TimeUnit) + +data TimeInterval = TimeInterval TimeUnit Int + deriving (Typeable, Show) +$(derive makeBinary ''TimeInterval) + +-- | Defines a Timeout value (and unit of measure) or +-- sets it to infinity (no timeout) +data Timeout = Timeout TimeInterval | Infinity + deriving (Typeable, Show) +$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs new file mode 100644 index 00000000..e074e510 --- /dev/null +++ b/src/Control/Distributed/Platform/Timer.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module Control.Distributed.Platform.Timer ( + TimerRef + , TimeInterval(..) + , TimeUnit(..) + , Tick(Tick) + , sleep + , sendAfter + , runAfter + , startTimer + , ticker + , periodically + , resetTimer + , cancelTimer + , flushTimer + -- time interval handling + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Process +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | an opaque reference to a timer +type TimerRef = ProcessId + +-- | cancellation message sent to timers +data TimerConfig = Reset | Cancel + deriving (Typeable, Show) +$(derive makeBinary ''TimerConfig) + +-- | represents a 'tick' event that timers can generate +data Tick = Tick + deriving (Typeable, Eq) +$(derive makeBinary ''Tick) + +data SleepingPill = SleepingPill + deriving (Typeable) +$(derive makeBinary ''SleepingPill) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- | blocks the calling Process for the specified TimeInterval. Note that this +-- function assumes that a blocking receive is the most efficient approach to +-- acheiving this, so expect the runtime semantics (particularly with regards +-- scheduling) to differ from threadDelay and/or operating system specific +-- functions that offer the same results. +sleep :: TimeInterval -> Process () +sleep t = do + let ms = intervalToMs t + _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) + (\_ -> return ())] + return () + +-- | starts a timer which sends the supplied message to the destination process +-- after the specified time interval. +sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +sendAfter t pid msg = runAfter t (mkSender pid msg) + +-- | runs the supplied process action(s) after `t' has elapsed +runAfter :: TimeInterval -> Process () -> Process TimerRef +runAfter t p = spawnLocal $ runTimer t p True + +-- | starts a timer that repeatedly sends the supplied message to the destination +-- process each time the specified time interval elapses. To stop messages from +-- being sent in future, cancelTimer can be called. +startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +startTimer t pid msg = periodically t (mkSender pid msg) + +-- | runs the supplied process action(s) repeatedly at intervals of `t' +periodically :: TimeInterval -> Process () -> Process TimerRef +periodically t p = spawnLocal $ runTimer t p False + +-- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- a timer's messages are prevented from being delivered to the target process. +-- Also note that resetting an ongoing timer (started using the `startTimer' or +-- `periodically' functions) will only cause the current elapsed period to time +-- out, after which the timer will continue running. To stop a long-running +-- timer, you should use `cancelTimer' instead. +resetTimer :: TimerRef -> Process () +resetTimer = (flip send) Reset + +cancelTimer :: TimerRef -> Process () +cancelTimer = (flip send) Cancel + +-- | cancels a running timer and flushes any viable timer messages from the +-- process' message queue. This function should only be called by the process +-- expecting to receive the timer's messages! +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer ref ignore t = do + cancelTimer ref + -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing + _ <- receiveTimeout (intervalToMs t) [ + matchIf (\x -> x == ignore) + (\_ -> return ()) ] + return () + +-- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +ticker :: TimeInterval -> ProcessId -> Process TimerRef +ticker t pid = startTimer t pid Tick + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- runs the timer process +runTimer :: TimeInterval -> Process () -> Bool -> Process () +runTimer t proc cancelOnReset = do + cancel <- expectTimeout (intervalToMs t) + -- say $ "cancel = " ++ (show cancel) ++ "\n" + case cancel of + Nothing -> runProc cancelOnReset + Just Cancel -> return () + Just Reset -> if cancelOnReset then return () + else runTimer t proc cancelOnReset + where runProc True = proc + runProc False = proc >> runTimer t proc cancelOnReset + +-- create a 'sender' action for dispatching `msg' to `pid' +mkSender :: (Serializable a) => ProcessId -> a -> Process () +mkSender pid msg = do + -- say "sending\n" + send pid msg diff --git a/tests/TestGenProcess.hs b/tests/TestGenProcess.hs new file mode 100644 index 00000000..7a570a72 --- /dev/null +++ b/tests/TestGenProcess.hs @@ -0,0 +1,5 @@ +module TestGenProcess where + +import Test.HUnit + +test1 = TestCase (assertEqual "1 == 2" 1 2) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 9b6c5ea6..ecee315f 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -81,10 +81,13 @@ testPing transport = do takeMVar clientDone - -genServerTests :: (NT.Transport, TransportInternals) -> [Test] -genServerTests (transport, _) = [ +tests :: NT.Transport -> [Test] +tests transport = [ testGroup "Basic features" [ - testCase "Ping" (testPing transport) + testCase "Ping" (testPing transport) ] ] + +genServerTests :: NT.Transport -> TransportInternals -> IO [Test] +genServerTests transport _ = do + return (tests transport) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 187a367c..c6f75c46 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,33 +6,23 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Control.Applicative -import Control.Monad -import Test.Framework (Test, defaultMain, - testGroup) +import Test.Framework (Test, defaultMain, testGroup) +import qualified Network.Transport as NT +import Network.Transport.TCP +import TestGenServer (genServerTests) +import TestTimer (timerTests) -import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP - ( createTransportExposeInternals - , TransportInternals(socketBetween) - , defaultTCPParameters - ) -import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable (Serializable) - -import TestGenServer - -tests :: (NT.Transport, TransportInternals) -> [Test] -tests transportConfig = [ - testGroup "GenServer" (genServerTests transportConfig) - ] +tests :: NT.Transport -> TransportInternals -> IO [Test] +tests transport internals = do + gsTestGroup <- genServerTests transport internals + timerTestGroup <- timerTests transport internals + return [ + testGroup "GenServer" gsTestGroup + , testGroup "Timer" timerTestGroup ] main :: IO () main = do - Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) \ No newline at end of file + Right (transport, internals) <- createTransportExposeInternals + "127.0.0.1" "8080" defaultTCPParameters + testData <- tests transport internals + defaultMain testData From 6ac88750578d58d91fa53edc90dca7e242188243 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 21:12:56 +0000 Subject: [PATCH 0381/2357] introduce common types; merge timer; refactor tests --- distributed-process-platform.cabal | 2 +- .../Distributed/Platform/GenProcess.hs | 244 ++++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 16 +- .../Distributed/Platform/Internal/Types.hs | 38 +++ src/Control/Distributed/Platform/Timer.hs | 168 ++++++++++++ tests/TestGenProcess.hs | 5 + tests/TestGenServer.hs | 11 +- tests/TestMain.hs | 42 ++- 8 files changed, 486 insertions(+), 40 deletions(-) create mode 100644 src/Control/Distributed/Platform/GenProcess.hs create mode 100644 src/Control/Distributed/Platform/Internal/Types.hs create mode 100644 src/Control/Distributed/Platform/Timer.hs create mode 100644 tests/TestGenProcess.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8207fa9a..2349f3b1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,7 +27,7 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: Control.Distributed.Platform.GenProcess, + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs new file mode 100644 index 00000000..2d893c62 --- /dev/null +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -0,0 +1,244 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Control.Distributed.Platform.GenProcess where + +-- TODO: define API and hide internals... + +import qualified Control.Distributed.Process as BaseProcess +import qualified Control.Monad.State as ST (StateT, get, + lift, modify, + put, runStateT) + +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer (intervalToMs) +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + + +type ServerName = String +type ServerPid = BaseProcess.ProcessId + +data ServerId = ServerProcess ServerPid | NamedServer ServerName + +data Recipient a = SendToPid BaseProcess.ProcessId | + SendToPort (BaseProcess.SendPort a) + +-- | Initialize handler result +data InitResult = + InitOk Timeout + | InitStop String + +-- | Terminate reason +data TerminateReason = + TerminateNormal + | TerminateShutdown + | TerminateReason String + deriving (Show, Typeable) +$(derive makeBinary ''TerminateReason) + +data ReplyTo = ReplyTo BaseProcess.ProcessId | None + deriving (Typeable, Show) +$(derive makeBinary ''ReplyTo) + +-- | The result of a call +data ProcessAction = + ProcessContinue + | ProcessTimeout Timeout + | ProcessStop String + deriving (Typeable) +$(derive makeBinary ''ProcessAction) + +type Process s = ST.StateT s BaseProcess.Process + +-- | Handlers +type InitHandler s = Process s InitResult +type TerminateHandler s = TerminateReason -> Process s () +type RequestHandler s a = Message a -> Process s ProcessAction + +-- | Contains the actual payload and possibly additional routing metadata +data Message a = Message ReplyTo a + deriving (Show, Typeable) +$(derive makeBinary ''Message) + +data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) + deriving (Typeable) +$(derive makeBinary ''Rpc) + +-- | Dispatcher that knows how to dispatch messages to a handler +data Dispatcher s = + forall a . (Serializable a) => + Dispatch { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction) } | + forall a . (Serializable a) => + DispatchIf { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction), + condition :: s -> Message a -> Bool } + +-- dispatching to implementation callbacks + +-- | Matches messages using a dispatcher +class Dispatchable d where + matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) + +-- | Matches messages to a MessageDispatcher +instance Dispatchable Dispatcher where + matchMessage s (Dispatch d ) = BaseProcess.match (d s) + matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) + + +data Behaviour s = Behaviour { + initHandler :: InitHandler s -- ^ initialization handler + , dispatchers :: [Dispatcher s] + , terminateHandler :: TerminateHandler s -- ^ termination handler + } + +-- | Management message +-- TODO is there a std way of terminating a process from another process? +data Termination = Terminate TerminateReason + deriving (Show, Typeable) +$(derive makeBinary ''Termination) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Start a new server and return it's id +-- start :: Behaviour s -> Process ProcessId +-- start handlers = spawnLocal $ runProcess handlers + +reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () +reply (ReplyTo pid) m = BaseProcess.send pid m +reply _ _ = return () + +replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> + BaseProcess.Process () +replyVia p m = BaseProcess.sendChan p m + +-- | Given a state, behaviour specificiation and spawn function, +-- starts a new server and return its id. The spawn function is typically +-- one taken from "Control.Distributed.Process". +-- see 'Control.Distributed.Process.spawn' +-- 'Control.Distributed.Process.spawnLocal' +-- 'Control.Distributed.Process.spawnLink' +-- 'Control.Distributed.Process.spawnMonitor' +-- 'Control.Distributed.Process.spawnSupervised' +start :: + s -> Behaviour s -> + (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> + BaseProcess.Process BaseProcess.ProcessId +start state handlers spawn = spawn $ do + _ <- ST.runStateT (runProc handlers) state + return () + +send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () +send s m = do + let msg = (Message None m) + case s of + ServerProcess pid -> BaseProcess.send pid msg + NamedServer name -> BaseProcess.nsend name msg + +-- process request handling + +handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s +handleRequest = handleRequestIf (const True) + +handleRequestIf :: (Serializable a) => (a -> Bool) -> + RequestHandler s a -> Dispatcher s +handleRequestIf cond handler = DispatchIf { + dispatch = (\state m@(Message _ _) -> do + (r, s') <- ST.runStateT (handler m) state + return (s', r) + ), + condition = \_ (Message _ req) -> cond req +} + +-- process state management + +-- | gets the process state +getState :: Process s s +getState = ST.get + +-- | sets the process state +putState :: s -> Process s () +putState = ST.put + +-- | modifies the server state +modifyState :: (s -> s) -> Process s () +modifyState = ST.modify + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | server process +runProc :: Behaviour s -> Process s () +runProc s = do + ir <- init s + tr <- case ir of + InitOk t -> do + trace $ "Server ready to receive messages!" + loop s t + InitStop r -> return (TerminateReason r) + terminate s tr + +-- | initialize server +init :: Behaviour s -> Process s InitResult +init s = do + trace $ "Server initializing ... " + ir <- initHandler s + return ir + +loop :: Behaviour s -> Timeout -> Process s TerminateReason +loop s t = do + s' <- processReceive (dispatchers s) t + nextAction s s' + where nextAction :: Behaviour s -> ProcessAction -> + Process s TerminateReason + nextAction b ProcessContinue = loop b t + nextAction b (ProcessTimeout t') = loop b t' + nextAction _ (ProcessStop r) = return (TerminateReason r) + +processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction +processReceive ds timeout = do + s <- getState + let ms = map (matchMessage s) ds + -- TODO: should we drain the message queue to avoid selective receive here? + case timeout of + Infinity -> do + (s', r) <- ST.lift $ BaseProcess.receiveWait ms + putState s' + return r + Timeout t -> do + result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms + case result of + Just (s', r) -> do + putState s' + return r + Nothing -> do + return $ ProcessStop "timed out" + +terminate :: Behaviour s -> TerminateReason -> Process s () +terminate s reason = do + trace $ "Server terminating: " ++ show reason + (terminateHandler s) reason + +-- | Log a trace message using the underlying Process's say +trace :: String -> Process s () +trace msg = ST.lift . BaseProcess.say $ msg + +-- data Upgrade = ??? +-- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to +-- a remote pid? if so then we may hot server-code loading quite easily... + diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 438a2d4c..b64b736f 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -52,6 +52,8 @@ import Control.Distributed.Process (AbstractMessage (forw receiveWait, say, send, spawnLocal) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer import qualified Control.Monad.State as ST (StateT, get, lift, modify, put, @@ -70,10 +72,6 @@ import Data.Typeable (Typeable) -- | ServerId type ServerId = ProcessId --- | Timeout -data Timeout = Timeout Int - | NoTimeout - -- | Server monad type Server s = ST.StateT s Process @@ -239,7 +237,7 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk NoTimeout, + initHandler = return $ InitOk Infinity, msgHandlers = [], terminateHandler = \_ -> return () } @@ -261,9 +259,9 @@ callServer sid timeout rq = do say $ "Calling server " ++ show cid send sid (Message cid rq) case timeout of - NoTimeout -> expect + Infinity -> expect Timeout time -> do - mayResp <- expectTimeout time + mayResp <- expectTimeout (intervalToMs time) case mayResp of Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time @@ -327,12 +325,12 @@ processReceive ds timeout = do s <- getState let ms = map (matchMessage s) ds case timeout of - NoTimeout -> do + Infinity -> do (s', r) <- ST.lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs new file mode 100644 index 00000000..a0721940 --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +-- | Types used throughout the Cloud Haskell framework +-- +module Control.Distributed.Platform.Internal.Types ( + TimeUnit(..) + , TimeInterval(..) + , Timeout(..) + ) where + +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | Defines the time unit for a Timeout value +data TimeUnit = Hours | Minutes | Seconds | Millis + deriving (Typeable, Show) +$(derive makeBinary ''TimeUnit) + +data TimeInterval = TimeInterval TimeUnit Int + deriving (Typeable, Show) +$(derive makeBinary ''TimeInterval) + +-- | Defines a Timeout value (and unit of measure) or +-- sets it to infinity (no timeout) +data Timeout = Timeout TimeInterval | Infinity + deriving (Typeable, Show) +$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs new file mode 100644 index 00000000..e074e510 --- /dev/null +++ b/src/Control/Distributed/Platform/Timer.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module Control.Distributed.Platform.Timer ( + TimerRef + , TimeInterval(..) + , TimeUnit(..) + , Tick(Tick) + , sleep + , sendAfter + , runAfter + , startTimer + , ticker + , periodically + , resetTimer + , cancelTimer + , flushTimer + -- time interval handling + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Process +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | an opaque reference to a timer +type TimerRef = ProcessId + +-- | cancellation message sent to timers +data TimerConfig = Reset | Cancel + deriving (Typeable, Show) +$(derive makeBinary ''TimerConfig) + +-- | represents a 'tick' event that timers can generate +data Tick = Tick + deriving (Typeable, Eq) +$(derive makeBinary ''Tick) + +data SleepingPill = SleepingPill + deriving (Typeable) +$(derive makeBinary ''SleepingPill) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- | blocks the calling Process for the specified TimeInterval. Note that this +-- function assumes that a blocking receive is the most efficient approach to +-- acheiving this, so expect the runtime semantics (particularly with regards +-- scheduling) to differ from threadDelay and/or operating system specific +-- functions that offer the same results. +sleep :: TimeInterval -> Process () +sleep t = do + let ms = intervalToMs t + _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) + (\_ -> return ())] + return () + +-- | starts a timer which sends the supplied message to the destination process +-- after the specified time interval. +sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +sendAfter t pid msg = runAfter t (mkSender pid msg) + +-- | runs the supplied process action(s) after `t' has elapsed +runAfter :: TimeInterval -> Process () -> Process TimerRef +runAfter t p = spawnLocal $ runTimer t p True + +-- | starts a timer that repeatedly sends the supplied message to the destination +-- process each time the specified time interval elapses. To stop messages from +-- being sent in future, cancelTimer can be called. +startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +startTimer t pid msg = periodically t (mkSender pid msg) + +-- | runs the supplied process action(s) repeatedly at intervals of `t' +periodically :: TimeInterval -> Process () -> Process TimerRef +periodically t p = spawnLocal $ runTimer t p False + +-- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- a timer's messages are prevented from being delivered to the target process. +-- Also note that resetting an ongoing timer (started using the `startTimer' or +-- `periodically' functions) will only cause the current elapsed period to time +-- out, after which the timer will continue running. To stop a long-running +-- timer, you should use `cancelTimer' instead. +resetTimer :: TimerRef -> Process () +resetTimer = (flip send) Reset + +cancelTimer :: TimerRef -> Process () +cancelTimer = (flip send) Cancel + +-- | cancels a running timer and flushes any viable timer messages from the +-- process' message queue. This function should only be called by the process +-- expecting to receive the timer's messages! +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer ref ignore t = do + cancelTimer ref + -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing + _ <- receiveTimeout (intervalToMs t) [ + matchIf (\x -> x == ignore) + (\_ -> return ()) ] + return () + +-- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +ticker :: TimeInterval -> ProcessId -> Process TimerRef +ticker t pid = startTimer t pid Tick + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- runs the timer process +runTimer :: TimeInterval -> Process () -> Bool -> Process () +runTimer t proc cancelOnReset = do + cancel <- expectTimeout (intervalToMs t) + -- say $ "cancel = " ++ (show cancel) ++ "\n" + case cancel of + Nothing -> runProc cancelOnReset + Just Cancel -> return () + Just Reset -> if cancelOnReset then return () + else runTimer t proc cancelOnReset + where runProc True = proc + runProc False = proc >> runTimer t proc cancelOnReset + +-- create a 'sender' action for dispatching `msg' to `pid' +mkSender :: (Serializable a) => ProcessId -> a -> Process () +mkSender pid msg = do + -- say "sending\n" + send pid msg diff --git a/tests/TestGenProcess.hs b/tests/TestGenProcess.hs new file mode 100644 index 00000000..7a570a72 --- /dev/null +++ b/tests/TestGenProcess.hs @@ -0,0 +1,5 @@ +module TestGenProcess where + +import Test.HUnit + +test1 = TestCase (assertEqual "1 == 2" 1 2) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 9b6c5ea6..ecee315f 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -81,10 +81,13 @@ testPing transport = do takeMVar clientDone - -genServerTests :: (NT.Transport, TransportInternals) -> [Test] -genServerTests (transport, _) = [ +tests :: NT.Transport -> [Test] +tests transport = [ testGroup "Basic features" [ - testCase "Ping" (testPing transport) + testCase "Ping" (testPing transport) ] ] + +genServerTests :: NT.Transport -> TransportInternals -> IO [Test] +genServerTests transport _ = do + return (tests transport) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 187a367c..c6f75c46 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,33 +6,23 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Control.Applicative -import Control.Monad -import Test.Framework (Test, defaultMain, - testGroup) +import Test.Framework (Test, defaultMain, testGroup) +import qualified Network.Transport as NT +import Network.Transport.TCP +import TestGenServer (genServerTests) +import TestTimer (timerTests) -import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP - ( createTransportExposeInternals - , TransportInternals(socketBetween) - , defaultTCPParameters - ) -import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable (Serializable) - -import TestGenServer - -tests :: (NT.Transport, TransportInternals) -> [Test] -tests transportConfig = [ - testGroup "GenServer" (genServerTests transportConfig) - ] +tests :: NT.Transport -> TransportInternals -> IO [Test] +tests transport internals = do + gsTestGroup <- genServerTests transport internals + timerTestGroup <- timerTests transport internals + return [ + testGroup "GenServer" gsTestGroup + , testGroup "Timer" timerTestGroup ] main :: IO () main = do - Right transport <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters - defaultMain (tests transport) \ No newline at end of file + Right (transport, internals) <- createTransportExposeInternals + "127.0.0.1" "8080" defaultTCPParameters + testData <- tests transport internals + defaultMain testData From 8591bcf367983595a28602ef5cac92c052d91bfe Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 9 Dec 2012 21:12:56 +0000 Subject: [PATCH 0382/2357] introduce common types; merge timer; refactor tests --- distributed-process-platform.cabal | 2 +- .../Distributed/Platform/GenProcess.hs | 244 ++++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 16 +- .../Distributed/Platform/Internal/Types.hs | 38 +++ src/Control/Distributed/Platform/Timer.hs | 168 ++++++++++++ 5 files changed, 458 insertions(+), 10 deletions(-) create mode 100644 src/Control/Distributed/Platform/GenProcess.hs create mode 100644 src/Control/Distributed/Platform/Internal/Types.hs create mode 100644 src/Control/Distributed/Platform/Timer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8207fa9a..2349f3b1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -27,7 +27,7 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: Control.Distributed.Platform.GenProcess, + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs new file mode 100644 index 00000000..2d893c62 --- /dev/null +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -0,0 +1,244 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +module Control.Distributed.Platform.GenProcess where + +-- TODO: define API and hide internals... + +import qualified Control.Distributed.Process as BaseProcess +import qualified Control.Monad.State as ST (StateT, get, + lift, modify, + put, runStateT) + +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer (intervalToMs) +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + + +type ServerName = String +type ServerPid = BaseProcess.ProcessId + +data ServerId = ServerProcess ServerPid | NamedServer ServerName + +data Recipient a = SendToPid BaseProcess.ProcessId | + SendToPort (BaseProcess.SendPort a) + +-- | Initialize handler result +data InitResult = + InitOk Timeout + | InitStop String + +-- | Terminate reason +data TerminateReason = + TerminateNormal + | TerminateShutdown + | TerminateReason String + deriving (Show, Typeable) +$(derive makeBinary ''TerminateReason) + +data ReplyTo = ReplyTo BaseProcess.ProcessId | None + deriving (Typeable, Show) +$(derive makeBinary ''ReplyTo) + +-- | The result of a call +data ProcessAction = + ProcessContinue + | ProcessTimeout Timeout + | ProcessStop String + deriving (Typeable) +$(derive makeBinary ''ProcessAction) + +type Process s = ST.StateT s BaseProcess.Process + +-- | Handlers +type InitHandler s = Process s InitResult +type TerminateHandler s = TerminateReason -> Process s () +type RequestHandler s a = Message a -> Process s ProcessAction + +-- | Contains the actual payload and possibly additional routing metadata +data Message a = Message ReplyTo a + deriving (Show, Typeable) +$(derive makeBinary ''Message) + +data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) + deriving (Typeable) +$(derive makeBinary ''Rpc) + +-- | Dispatcher that knows how to dispatch messages to a handler +data Dispatcher s = + forall a . (Serializable a) => + Dispatch { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction) } | + forall a . (Serializable a) => + DispatchIf { dispatch :: s -> Message a -> + BaseProcess.Process (s, ProcessAction), + condition :: s -> Message a -> Bool } + +-- dispatching to implementation callbacks + +-- | Matches messages using a dispatcher +class Dispatchable d where + matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) + +-- | Matches messages to a MessageDispatcher +instance Dispatchable Dispatcher where + matchMessage s (Dispatch d ) = BaseProcess.match (d s) + matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) + + +data Behaviour s = Behaviour { + initHandler :: InitHandler s -- ^ initialization handler + , dispatchers :: [Dispatcher s] + , terminateHandler :: TerminateHandler s -- ^ termination handler + } + +-- | Management message +-- TODO is there a std way of terminating a process from another process? +data Termination = Terminate TerminateReason + deriving (Show, Typeable) +$(derive makeBinary ''Termination) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Start a new server and return it's id +-- start :: Behaviour s -> Process ProcessId +-- start handlers = spawnLocal $ runProcess handlers + +reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () +reply (ReplyTo pid) m = BaseProcess.send pid m +reply _ _ = return () + +replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> + BaseProcess.Process () +replyVia p m = BaseProcess.sendChan p m + +-- | Given a state, behaviour specificiation and spawn function, +-- starts a new server and return its id. The spawn function is typically +-- one taken from "Control.Distributed.Process". +-- see 'Control.Distributed.Process.spawn' +-- 'Control.Distributed.Process.spawnLocal' +-- 'Control.Distributed.Process.spawnLink' +-- 'Control.Distributed.Process.spawnMonitor' +-- 'Control.Distributed.Process.spawnSupervised' +start :: + s -> Behaviour s -> + (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> + BaseProcess.Process BaseProcess.ProcessId +start state handlers spawn = spawn $ do + _ <- ST.runStateT (runProc handlers) state + return () + +send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () +send s m = do + let msg = (Message None m) + case s of + ServerProcess pid -> BaseProcess.send pid msg + NamedServer name -> BaseProcess.nsend name msg + +-- process request handling + +handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s +handleRequest = handleRequestIf (const True) + +handleRequestIf :: (Serializable a) => (a -> Bool) -> + RequestHandler s a -> Dispatcher s +handleRequestIf cond handler = DispatchIf { + dispatch = (\state m@(Message _ _) -> do + (r, s') <- ST.runStateT (handler m) state + return (s', r) + ), + condition = \_ (Message _ req) -> cond req +} + +-- process state management + +-- | gets the process state +getState :: Process s s +getState = ST.get + +-- | sets the process state +putState :: s -> Process s () +putState = ST.put + +-- | modifies the server state +modifyState :: (s -> s) -> Process s () +modifyState = ST.modify + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | server process +runProc :: Behaviour s -> Process s () +runProc s = do + ir <- init s + tr <- case ir of + InitOk t -> do + trace $ "Server ready to receive messages!" + loop s t + InitStop r -> return (TerminateReason r) + terminate s tr + +-- | initialize server +init :: Behaviour s -> Process s InitResult +init s = do + trace $ "Server initializing ... " + ir <- initHandler s + return ir + +loop :: Behaviour s -> Timeout -> Process s TerminateReason +loop s t = do + s' <- processReceive (dispatchers s) t + nextAction s s' + where nextAction :: Behaviour s -> ProcessAction -> + Process s TerminateReason + nextAction b ProcessContinue = loop b t + nextAction b (ProcessTimeout t') = loop b t' + nextAction _ (ProcessStop r) = return (TerminateReason r) + +processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction +processReceive ds timeout = do + s <- getState + let ms = map (matchMessage s) ds + -- TODO: should we drain the message queue to avoid selective receive here? + case timeout of + Infinity -> do + (s', r) <- ST.lift $ BaseProcess.receiveWait ms + putState s' + return r + Timeout t -> do + result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms + case result of + Just (s', r) -> do + putState s' + return r + Nothing -> do + return $ ProcessStop "timed out" + +terminate :: Behaviour s -> TerminateReason -> Process s () +terminate s reason = do + trace $ "Server terminating: " ++ show reason + (terminateHandler s) reason + +-- | Log a trace message using the underlying Process's say +trace :: String -> Process s () +trace msg = ST.lift . BaseProcess.say $ msg + +-- data Upgrade = ??? +-- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to +-- a remote pid? if so then we may hot server-code loading quite easily... + diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 438a2d4c..b64b736f 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -52,6 +52,8 @@ import Control.Distributed.Process (AbstractMessage (forw receiveWait, say, send, spawnLocal) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer import qualified Control.Monad.State as ST (StateT, get, lift, modify, put, @@ -70,10 +72,6 @@ import Data.Typeable (Typeable) -- | ServerId type ServerId = ProcessId --- | Timeout -data Timeout = Timeout Int - | NoTimeout - -- | Server monad type Server s = ST.StateT s Process @@ -239,7 +237,7 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk NoTimeout, + initHandler = return $ InitOk Infinity, msgHandlers = [], terminateHandler = \_ -> return () } @@ -261,9 +259,9 @@ callServer sid timeout rq = do say $ "Calling server " ++ show cid send sid (Message cid rq) case timeout of - NoTimeout -> expect + Infinity -> expect Timeout time -> do - mayResp <- expectTimeout time + mayResp <- expectTimeout (intervalToMs time) case mayResp of Just msg -> return msg Nothing -> error $ "timeout! value = " ++ show time @@ -327,12 +325,12 @@ processReceive ds timeout = do s <- getState let ms = map (matchMessage s) ds case timeout of - NoTimeout -> do + Infinity -> do (s', r) <- ST.lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout t ms + mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs new file mode 100644 index 00000000..a0721940 --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +-- | Types used throughout the Cloud Haskell framework +-- +module Control.Distributed.Platform.Internal.Types ( + TimeUnit(..) + , TimeInterval(..) + , Timeout(..) + ) where + +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | Defines the time unit for a Timeout value +data TimeUnit = Hours | Minutes | Seconds | Millis + deriving (Typeable, Show) +$(derive makeBinary ''TimeUnit) + +data TimeInterval = TimeInterval TimeUnit Int + deriving (Typeable, Show) +$(derive makeBinary ''TimeInterval) + +-- | Defines a Timeout value (and unit of measure) or +-- sets it to infinity (no timeout) +data Timeout = Timeout TimeInterval | Infinity + deriving (Typeable, Show) +$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs new file mode 100644 index 00000000..e074e510 --- /dev/null +++ b/src/Control/Distributed/Platform/Timer.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module Control.Distributed.Platform.Timer ( + TimerRef + , TimeInterval(..) + , TimeUnit(..) + , Tick(Tick) + , sleep + , sendAfter + , runAfter + , startTimer + , ticker + , periodically + , resetTimer + , cancelTimer + , flushTimer + -- time interval handling + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Process +import Control.Distributed.Process.Serializable +import Control.Distributed.Platform.Internal.Types +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (init) + +-- | an opaque reference to a timer +type TimerRef = ProcessId + +-- | cancellation message sent to timers +data TimerConfig = Reset | Cancel + deriving (Typeable, Show) +$(derive makeBinary ''TimerConfig) + +-- | represents a 'tick' event that timers can generate +data Tick = Tick + deriving (Typeable, Eq) +$(derive makeBinary ''Tick) + +data SleepingPill = SleepingPill + deriving (Typeable) +$(derive makeBinary ''SleepingPill) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- | blocks the calling Process for the specified TimeInterval. Note that this +-- function assumes that a blocking receive is the most efficient approach to +-- acheiving this, so expect the runtime semantics (particularly with regards +-- scheduling) to differ from threadDelay and/or operating system specific +-- functions that offer the same results. +sleep :: TimeInterval -> Process () +sleep t = do + let ms = intervalToMs t + _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) + (\_ -> return ())] + return () + +-- | starts a timer which sends the supplied message to the destination process +-- after the specified time interval. +sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +sendAfter t pid msg = runAfter t (mkSender pid msg) + +-- | runs the supplied process action(s) after `t' has elapsed +runAfter :: TimeInterval -> Process () -> Process TimerRef +runAfter t p = spawnLocal $ runTimer t p True + +-- | starts a timer that repeatedly sends the supplied message to the destination +-- process each time the specified time interval elapses. To stop messages from +-- being sent in future, cancelTimer can be called. +startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +startTimer t pid msg = periodically t (mkSender pid msg) + +-- | runs the supplied process action(s) repeatedly at intervals of `t' +periodically :: TimeInterval -> Process () -> Process TimerRef +periodically t p = spawnLocal $ runTimer t p False + +-- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- a timer's messages are prevented from being delivered to the target process. +-- Also note that resetting an ongoing timer (started using the `startTimer' or +-- `periodically' functions) will only cause the current elapsed period to time +-- out, after which the timer will continue running. To stop a long-running +-- timer, you should use `cancelTimer' instead. +resetTimer :: TimerRef -> Process () +resetTimer = (flip send) Reset + +cancelTimer :: TimerRef -> Process () +cancelTimer = (flip send) Cancel + +-- | cancels a running timer and flushes any viable timer messages from the +-- process' message queue. This function should only be called by the process +-- expecting to receive the timer's messages! +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer ref ignore t = do + cancelTimer ref + -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing + _ <- receiveTimeout (intervalToMs t) [ + matchIf (\x -> x == ignore) + (\_ -> return ()) ] + return () + +-- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +ticker :: TimeInterval -> ProcessId -> Process TimerRef +ticker t pid = startTimer t pid Tick + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- runs the timer process +runTimer :: TimeInterval -> Process () -> Bool -> Process () +runTimer t proc cancelOnReset = do + cancel <- expectTimeout (intervalToMs t) + -- say $ "cancel = " ++ (show cancel) ++ "\n" + case cancel of + Nothing -> runProc cancelOnReset + Just Cancel -> return () + Just Reset -> if cancelOnReset then return () + else runTimer t proc cancelOnReset + where runProc True = proc + runProc False = proc >> runTimer t proc cancelOnReset + +-- create a 'sender' action for dispatching `msg' to `pid' +mkSender :: (Serializable a) => ProcessId -> a -> Process () +mkSender pid msg = do + -- say "sending\n" + send pid msg From 413fae3dbac3a6ce81cffa6e6675ca89e04f786a Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0383/2357] Few cosmetic changes --- src/Control/Distributed/Examples/Counter.hs | 2 +- src/Control/Distributed/Examples/Kitty.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 35 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index d0bfa3b1..f2d636e3 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -56,7 +56,7 @@ startCounter count = startServer count defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Counter terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleCounter, handle handleReset ] diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 60af1b19..9aaf5566 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -75,7 +75,7 @@ startKitty cats = startServer cats defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Kitty terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleKitty, handle handleReturn ]} diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index a40f580d..a94db783 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -29,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -37,11 +39,11 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Applicative (Applicative) -import Control.Exception (Exception, SomeException) +import Control.Exception (SomeException) import Control.Distributed.Process (AbstractMessage, Match, MonitorRef, Process, ProcessId, - catchExit, exit, + exit, expect, expectTimeout, getSelfPid, link, @@ -49,12 +51,9 @@ import Control.Distributed.Process (AbstractMessage, matchIf, monitor, receiveTimeout, receiveWait, say, - send, spawnLocal, - terminate) + send, spawnLocal) import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Monad (void) import Control.Monad.IO.Class (MonadIO) import qualified Control.Monad.State as ST (MonadState, MonadTrans, @@ -144,8 +143,8 @@ type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload data Message a = - CallMessage { from :: ProcessId, payload :: a } - | CastMessage { from :: ProcessId, payload :: a } + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) @@ -174,9 +173,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) @@ -206,7 +205,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -216,7 +215,7 @@ handleIf cond handler = MessageDispatcherIf { send sid msg return (s', Nothing) ), - dispatchIf = \state msg -> cond (payload msg) + dispatchIf = \_ msg -> cond (msgPayload msg) } @@ -242,7 +241,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -253,7 +252,7 @@ data LocalServer s = LocalServer { defaultServer :: LocalServer s defaultServer = LocalServer { initHandler = return $ InitOk NoTimeout, - msgHandlers = [], + handlers = [], terminateHandler = \_ -> return () } @@ -267,10 +266,10 @@ defaultServer = LocalServer { startServer :: s -> LocalServer s -> Process ServerId startServer s ls = spawnLocal proc where - proc = processServer initH terminateH handlers s + proc = processServer initH terminateH hs s initH = initHandler ls terminateH = terminateHandler ls - handlers = msgHandlers ls + hs = handlers ls @@ -394,7 +393,7 @@ processReceive ds timeout = do mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do --trace "Receive timed out ..." From 445851c1272ff57bd831338451c97284333dd00c Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0384/2357] Few cosmetic changes --- src/Control/Distributed/Examples/Counter.hs | 2 +- src/Control/Distributed/Examples/Kitty.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 35 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index d0bfa3b1..f2d636e3 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -56,7 +56,7 @@ startCounter count = startServer count defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Counter terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleCounter, handle handleReset ] diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 60af1b19..9aaf5566 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -75,7 +75,7 @@ startKitty cats = startServer cats defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Kitty terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleKitty, handle handleReturn ]} diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index a40f580d..a94db783 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -29,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -37,11 +39,11 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Applicative (Applicative) -import Control.Exception (Exception, SomeException) +import Control.Exception (SomeException) import Control.Distributed.Process (AbstractMessage, Match, MonitorRef, Process, ProcessId, - catchExit, exit, + exit, expect, expectTimeout, getSelfPid, link, @@ -49,12 +51,9 @@ import Control.Distributed.Process (AbstractMessage, matchIf, monitor, receiveTimeout, receiveWait, say, - send, spawnLocal, - terminate) + send, spawnLocal) import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Monad (void) import Control.Monad.IO.Class (MonadIO) import qualified Control.Monad.State as ST (MonadState, MonadTrans, @@ -144,8 +143,8 @@ type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload data Message a = - CallMessage { from :: ProcessId, payload :: a } - | CastMessage { from :: ProcessId, payload :: a } + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) @@ -174,9 +173,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) @@ -206,7 +205,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -216,7 +215,7 @@ handleIf cond handler = MessageDispatcherIf { send sid msg return (s', Nothing) ), - dispatchIf = \state msg -> cond (payload msg) + dispatchIf = \_ msg -> cond (msgPayload msg) } @@ -242,7 +241,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -253,7 +252,7 @@ data LocalServer s = LocalServer { defaultServer :: LocalServer s defaultServer = LocalServer { initHandler = return $ InitOk NoTimeout, - msgHandlers = [], + handlers = [], terminateHandler = \_ -> return () } @@ -267,10 +266,10 @@ defaultServer = LocalServer { startServer :: s -> LocalServer s -> Process ServerId startServer s ls = spawnLocal proc where - proc = processServer initH terminateH handlers s + proc = processServer initH terminateH hs s initH = initHandler ls terminateH = terminateHandler ls - handlers = msgHandlers ls + hs = handlers ls @@ -394,7 +393,7 @@ processReceive ds timeout = do mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do --trace "Receive timed out ..." From 28d9bc9829bd43ff64e9d7116a59f5aa170c4285 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0385/2357] Few cosmetic changes --- src/Control/Distributed/Examples/Counter.hs | 2 +- src/Control/Distributed/Examples/Kitty.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 35 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index d0bfa3b1..f2d636e3 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -56,7 +56,7 @@ startCounter count = startServer count defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Counter terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleCounter, handle handleReset ] diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 60af1b19..9aaf5566 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -75,7 +75,7 @@ startKitty cats = startServer cats defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Kitty terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleKitty, handle handleReturn ]} diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index a40f580d..a94db783 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -29,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -37,11 +39,11 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Applicative (Applicative) -import Control.Exception (Exception, SomeException) +import Control.Exception (SomeException) import Control.Distributed.Process (AbstractMessage, Match, MonitorRef, Process, ProcessId, - catchExit, exit, + exit, expect, expectTimeout, getSelfPid, link, @@ -49,12 +51,9 @@ import Control.Distributed.Process (AbstractMessage, matchIf, monitor, receiveTimeout, receiveWait, say, - send, spawnLocal, - terminate) + send, spawnLocal) import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Monad (void) import Control.Monad.IO.Class (MonadIO) import qualified Control.Monad.State as ST (MonadState, MonadTrans, @@ -144,8 +143,8 @@ type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload data Message a = - CallMessage { from :: ProcessId, payload :: a } - | CastMessage { from :: ProcessId, payload :: a } + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) @@ -174,9 +173,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) @@ -206,7 +205,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -216,7 +215,7 @@ handleIf cond handler = MessageDispatcherIf { send sid msg return (s', Nothing) ), - dispatchIf = \state msg -> cond (payload msg) + dispatchIf = \_ msg -> cond (msgPayload msg) } @@ -242,7 +241,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -253,7 +252,7 @@ data LocalServer s = LocalServer { defaultServer :: LocalServer s defaultServer = LocalServer { initHandler = return $ InitOk NoTimeout, - msgHandlers = [], + handlers = [], terminateHandler = \_ -> return () } @@ -267,10 +266,10 @@ defaultServer = LocalServer { startServer :: s -> LocalServer s -> Process ServerId startServer s ls = spawnLocal proc where - proc = processServer initH terminateH handlers s + proc = processServer initH terminateH hs s initH = initHandler ls terminateH = terminateHandler ls - handlers = msgHandlers ls + hs = handlers ls @@ -394,7 +393,7 @@ processReceive ds timeout = do mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do --trace "Receive timed out ..." From bfca95d1f0fbd7ec3166494b1d65b62320fe51fe Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0386/2357] Few cosmetic changes --- src/Control/Distributed/Examples/Counter.hs | 2 +- src/Control/Distributed/Examples/Kitty.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 35 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs index d0bfa3b1..f2d636e3 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/src/Control/Distributed/Examples/Counter.hs @@ -56,7 +56,7 @@ startCounter count = startServer count defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Counter terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleCounter, handle handleReset ] diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs index 60af1b19..9aaf5566 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/src/Control/Distributed/Examples/Kitty.hs @@ -75,7 +75,7 @@ startKitty cats = startServer cats defaultServer { initOk NoTimeout, terminateHandler = \r -> trace $ "Kitty terminate: " ++ show r, - msgHandlers = [ + handlers = [ handle handleKitty, handle handleReturn ]} diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index a40f580d..a94db783 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -29,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -37,11 +39,11 @@ module Control.Distributed.Platform.GenServer ( ) where import Control.Applicative (Applicative) -import Control.Exception (Exception, SomeException) +import Control.Exception (SomeException) import Control.Distributed.Process (AbstractMessage, Match, MonitorRef, Process, ProcessId, - catchExit, exit, + exit, expect, expectTimeout, getSelfPid, link, @@ -49,12 +51,9 @@ import Control.Distributed.Process (AbstractMessage, matchIf, monitor, receiveTimeout, receiveWait, say, - send, spawnLocal, - terminate) + send, spawnLocal) import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Monad (void) import Control.Monad.IO.Class (MonadIO) import qualified Control.Monad.State as ST (MonadState, MonadTrans, @@ -144,8 +143,8 @@ type Handler s a b = a -> Server s (Result b) -- | Adds routing metadata to the actual payload data Message a = - CallMessage { from :: ProcessId, payload :: a } - | CastMessage { from :: ProcessId, payload :: a } + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) @@ -174,9 +173,9 @@ class MessageMatcher d where -- | Matches messages to a MessageDispatcher instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher dispatcher) = match (dispatcher s) - matchMessage s (MessageDispatcherIf dispatcher cond) = matchIf (cond s) (dispatcher s) - matchMessage s (MessageDispatcherAny dispatcher) = matchAny (dispatcher s) + matchMessage s (MessageDispatcher d) = match (d s) + matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) + matchMessage s (MessageDispatcherAny d) = matchAny (d s) @@ -206,7 +205,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -216,7 +215,7 @@ handleIf cond handler = MessageDispatcherIf { send sid msg return (s', Nothing) ), - dispatchIf = \state msg -> cond (payload msg) + dispatchIf = \_ msg -> cond (msgPayload msg) } @@ -242,7 +241,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -253,7 +252,7 @@ data LocalServer s = LocalServer { defaultServer :: LocalServer s defaultServer = LocalServer { initHandler = return $ InitOk NoTimeout, - msgHandlers = [], + handlers = [], terminateHandler = \_ -> return () } @@ -267,10 +266,10 @@ defaultServer = LocalServer { startServer :: s -> LocalServer s -> Process ServerId startServer s ls = spawnLocal proc where - proc = processServer initH terminateH handlers s + proc = processServer initH terminateH hs s initH = initHandler ls terminateH = terminateHandler ls - handlers = msgHandlers ls + hs = handlers ls @@ -394,7 +393,7 @@ processReceive ds timeout = do mayResult <- lift $ receiveTimeout t ms case mayResult of Just (s', r) -> do - putState s + putState s' return r Nothing -> do --trace "Receive timed out ..." From 6bf30988a926a56afda07857ec190285b43547b1 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0387/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 14 +- src/Control/Distributed/Platform/GenServer.hs | 316 ++++++++++-------- .../Examples => tests/GenServer}/Counter.hs | 49 +-- .../Examples => tests/GenServer}/Kitty.hs | 37 +- tests/TestGenServer.hs | 159 ++++++--- 5 files changed, 354 insertions(+), 221 deletions(-) rename {src/Control/Distributed/Examples => tests/GenServer}/Counter.hs (72%) rename {src/Control/Distributed/Examples => tests/GenServer}/Kitty.hs (76%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2349f3b1..0ff5b162 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -16,8 +16,8 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform -library - build-depends: +library + build-depends: base >= 4, distributed-process, derive, @@ -27,19 +27,20 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer test-suite TestTimer type: exitcode-stdio-1.0 x-uses-tf: true - build-depends: + build-depends: base >= 4.4 && < 5, ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, network-transport >= 0.3 && < 0.4, + mtl, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -47,11 +48,10 @@ test-suite TestTimer test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers - hs-source-dirs: - src, + hs-source-dirs: + src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer extensions: CPP main-is: TestMain.hs - diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b64b736f..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -33,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -40,40 +38,53 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, Process, ProcessId, expect, expectTimeout, + monitor, link, + exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, send, spawnLocal) +import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- + -- | ServerId type ServerId = ProcessId -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) -- | Initialize handler result data InitResult @@ -95,52 +106,39 @@ data TerminateReason $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) + + -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) + -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state @@ -167,61 +165,56 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) - -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +-- +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) + + + +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \_ (Message _ req) -> cond req -} - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage _ payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \_ (Message _ msg) -> cond msg + dispatchIf = \_ msg -> cond (msgPayload msg) } -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -229,7 +222,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -237,9 +230,9 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - msgHandlers = [], - terminateHandler = \_ -> return () + initHandler = return $ InitOk Infinity, + handlers = [], + terminateHandler = \_ -> return () } -------------------------------------------------------------------------------- @@ -248,16 +241,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - _ <- ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH hs s + initH = initHandler ls + terminateH = terminateHandler ls + hs = handlers ls + + + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- | call a server identified by it's ServerId + + +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) + + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of Infinity -> expect Timeout time -> do @@ -270,12 +286,14 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason -- | Get the server state getState :: Server s s @@ -294,30 +312,38 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () + + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () + + -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r + + -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) @@ -326,25 +352,33 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of Infinity -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms + mayResult <- lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason + -- | Log a trace message using the underlying Process's say trace :: String -> Server s () -trace msg = ST.lift . say $ msg +trace msg = lift . say $ msg + + + +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state diff --git a/src/Control/Distributed/Examples/Counter.hs b/tests/GenServer/Counter.hs similarity index 72% rename from src/Control/Distributed/Examples/Counter.hs rename to tests/GenServer/Counter.hs index 1e850614..9a3effbb 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -1,6 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Examples.Counter( +module GenServer.Counter( startCounter, stopCounter, getCount, @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + --c <- getState + --trace $ "Counter init: " ++ show c + initOk Infinity, + terminateHandler = const (return ()), + --trace $ "Counter terminate: " ++ show r, + handlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid Infinity IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid Infinity GetCount + return c @@ -88,19 +95,19 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) +handleReset :: Handler Int ResetCount () handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/tests/GenServer/Kitty.hs similarity index 76% rename from src/Control/Distributed/Examples/Kitty.hs rename to tests/GenServer/Kitty.hs index 9620660b..36205a04 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -4,9 +4,10 @@ -- -- -module(kitty_server). -- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Examples.Kitty +module GenServer.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,16 +69,29 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + --cs <- getState + --trace $ "Kitty init: " ++ show cs + initOk Infinity, + terminateHandler = const $ return (), + --trace $ "Kitty terminate: " ++ show r, + handlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do - result <- callServer sid NoTimeout (OrderCat name color descr) + result <- callServer sid Infinity (OrderCat name color descr) case result of CatOrdered c -> return c _ -> error $ "Unexpected result " ++ show result @@ -93,7 +107,7 @@ returnCat sid cat = castServer sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () closeShop sid = do - result <- callServer sid NoTimeout CloseShop + result <- callServer sid Infinity CloseShop case result of ShopClosed -> return () _ -> error $ "Unexpected result " ++ show result @@ -104,24 +118,25 @@ closeShop sid = do -- %%% Server functions +handleKitty :: Handler [Cat] CatCmd CatEv handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed +handleReturn :: Handler [Cat] ReturnCat () handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index ecee315f..da8946ca 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,10 +1,15 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} module TestGenServer where -import Data.Binary (Binary(..)) +import Data.Binary (Binary (..), getWord8, + putWord8) import Data.Typeable (Typeable) -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar +import Data.DeriveTH +import Data.Foldable (forM_) +import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent.MVar ( MVar , newEmptyMVar , putMVar @@ -16,14 +21,14 @@ import Control.Exception (SomeException, throwIO) import qualified Control.Exception as Ex (catch) import Control.Applicative ((<$>), (<*>), pure, (<|>)) import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP +import Network.Transport.TCP ( createTransportExposeInternals , TransportInternals(socketBetween) , defaultTCPParameters ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) , LocalNode(localEndPoint) ) import Control.Distributed.Process.Node @@ -33,58 +38,130 @@ import Test.HUnit (Assertion) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase) +import Control.Distributed.Platform.GenServer +import GenServer.Counter +import GenServer.Kitty + -------------------------------------------------------------------------------- -- The tests proper -- -------------------------------------------------------------------------------- -newtype Ping = Ping ProcessId - deriving (Typeable, Binary, Show) +data Ping = Ping + deriving (Typeable, Show) +$(derive makeBinary ''Ping) -newtype Pong = Pong ProcessId - deriving (Typeable, Binary, Show) +data Pong = Pong + deriving (Typeable, Show) +$(derive makeBinary ''Pong) --- | The ping server from the paper -ping :: Process () -ping = do - Pong partner <- expect - self <- getSelfPid - send partner (Ping self) - ping --- | Basic ping test -testPing :: NT.Transport -> Assertion +-- | Test ping server +-- TODO fix this test! +testPing :: NT.Transport -> Assertion testPing transport = do - serverAddr <- newEmptyMVar - clientDone <- newEmptyMVar + initDone <- newEmptyMVar + pingDone <- newEmptyMVar + pongDone <- newEmptyMVar + terminateDone <- newEmptyMVar + + localNode <- newLocalNode transport initRemoteTable + + runProcess localNode $ do + say "Starting ..." + sid <- startServer (0 :: Int) defaultServer { + initHandler = do + trace "Init ..." + c <- getState + liftIO $ putMVar initDone c + initOk Infinity, + terminateHandler = \reason -> do + trace "Terminate ..." + c <- getState + liftIO $ putMVar terminateDone c + return (), + handlers = [ + handle (\Ping -> do + trace "Ping ..." + modifyState (1 +) + c <- getState + --liftIO $ putMVar pingDone c + ok Pong), + handle (\Pong -> do + trace "Pong ..." + modifyState (1 +) + c <- getState + --liftIO $ putMVar pongDone c + ok ()) + ] + } + --liftIO $ takeMVar initDone + --replicateM_ 10 $ do + Pong <- callServer sid Infinity Ping + --liftIO $ takeMVar pingDone + castServer sid Ping + --liftIO $ takeMVar pongDone + --return () + exit sid () + liftIO $ takeMVar terminateDone + return () + + + +-- | Test counter server +-- TODO split me! +testCounter :: NT.Transport -> Assertion +testCounter transport = do + serverDone <- newEmptyMVar + + localNode <- newLocalNode transport initRemoteTable + + runProcess localNode $ do + cid <- startCounter 0 + c <- getCount cid + incCount cid + incCount cid + c <- getCount cid + resetCount cid + c2 <- getCount cid + stopCounter cid + liftIO $ putMVar serverDone True + return () + + liftIO $ takeMVar serverDone + return () + - -- Server - forkIO $ do - localNode <- newLocalNode transport initRemoteTable - addr <- forkProcess localNode ping - putMVar serverAddr addr +-- | Test kitty server +-- TODO split me! +testKitty :: NT.Transport -> Assertion +testKitty transport = do + serverDone <- newEmptyMVar - -- Client - forkIO $ do - localNode <- newLocalNode transport initRemoteTable - pingServer <- readMVar serverAddr + localNode <- newLocalNode transport initRemoteTable - let numPings = 10000 + runProcess localNode $ do + kPid <- startKitty [Cat "c1" "black" "a black cat"] + replicateM_ 100 $ do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + closeShop kPid + stopKitty kPid + liftIO $ putMVar serverDone True + return () - runProcess localNode $ do - pid <- getSelfPid - replicateM_ numPings $ do - send pingServer (Pong pid) - Ping _ <- expect - return () + liftIO $ takeMVar serverDone + return () - putMVar clientDone () - takeMVar clientDone tests :: NT.Transport -> [Test] tests transport = [ testGroup "Basic features" [ - testCase "Ping" (testPing transport) + --testCase "Ping" (testPing transport), + testCase "Counter" (testCounter transport), + testCase "Kitty" (testKitty transport) ] ] From 0cddd8ed7b5827396be665793caf87ee99ca94f1 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0388/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 14 +- src/Control/Distributed/Platform/GenServer.hs | 316 ++++++++++-------- .../Examples => tests/GenServer}/Counter.hs | 49 +-- .../Examples => tests/GenServer}/Kitty.hs | 37 +- tests/TestGenServer.hs | 159 ++++++--- 5 files changed, 354 insertions(+), 221 deletions(-) rename {src/Control/Distributed/Examples => tests/GenServer}/Counter.hs (72%) rename {src/Control/Distributed/Examples => tests/GenServer}/Kitty.hs (76%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2349f3b1..0ff5b162 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -16,8 +16,8 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform -library - build-depends: +library + build-depends: base >= 4, distributed-process, derive, @@ -27,19 +27,20 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer test-suite TestTimer type: exitcode-stdio-1.0 x-uses-tf: true - build-depends: + build-depends: base >= 4.4 && < 5, ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, network-transport >= 0.3 && < 0.4, + mtl, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -47,11 +48,10 @@ test-suite TestTimer test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers - hs-source-dirs: - src, + hs-source-dirs: + src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer extensions: CPP main-is: TestMain.hs - diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b64b736f..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -33,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -40,40 +38,53 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, Process, ProcessId, expect, expectTimeout, + monitor, link, + exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, send, spawnLocal) +import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- + -- | ServerId type ServerId = ProcessId -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) -- | Initialize handler result data InitResult @@ -95,52 +106,39 @@ data TerminateReason $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) + + -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) + -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state @@ -167,61 +165,56 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) - -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +-- +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) + + + +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \_ (Message _ req) -> cond req -} - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage _ payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \_ (Message _ msg) -> cond msg + dispatchIf = \_ msg -> cond (msgPayload msg) } -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -229,7 +222,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -237,9 +230,9 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - msgHandlers = [], - terminateHandler = \_ -> return () + initHandler = return $ InitOk Infinity, + handlers = [], + terminateHandler = \_ -> return () } -------------------------------------------------------------------------------- @@ -248,16 +241,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - _ <- ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH hs s + initH = initHandler ls + terminateH = terminateHandler ls + hs = handlers ls + + + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- | call a server identified by it's ServerId + + +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) + + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of Infinity -> expect Timeout time -> do @@ -270,12 +286,14 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason -- | Get the server state getState :: Server s s @@ -294,30 +312,38 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () + + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () + + -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r + + -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) @@ -326,25 +352,33 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of Infinity -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms + mayResult <- lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason + -- | Log a trace message using the underlying Process's say trace :: String -> Server s () -trace msg = ST.lift . say $ msg +trace msg = lift . say $ msg + + + +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state diff --git a/src/Control/Distributed/Examples/Counter.hs b/tests/GenServer/Counter.hs similarity index 72% rename from src/Control/Distributed/Examples/Counter.hs rename to tests/GenServer/Counter.hs index 1e850614..9a3effbb 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -1,6 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Examples.Counter( +module GenServer.Counter( startCounter, stopCounter, getCount, @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + --c <- getState + --trace $ "Counter init: " ++ show c + initOk Infinity, + terminateHandler = const (return ()), + --trace $ "Counter terminate: " ++ show r, + handlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid Infinity IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid Infinity GetCount + return c @@ -88,19 +95,19 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) +handleReset :: Handler Int ResetCount () handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/tests/GenServer/Kitty.hs similarity index 76% rename from src/Control/Distributed/Examples/Kitty.hs rename to tests/GenServer/Kitty.hs index 9620660b..36205a04 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -4,9 +4,10 @@ -- -- -module(kitty_server). -- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Examples.Kitty +module GenServer.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,16 +69,29 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + --cs <- getState + --trace $ "Kitty init: " ++ show cs + initOk Infinity, + terminateHandler = const $ return (), + --trace $ "Kitty terminate: " ++ show r, + handlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do - result <- callServer sid NoTimeout (OrderCat name color descr) + result <- callServer sid Infinity (OrderCat name color descr) case result of CatOrdered c -> return c _ -> error $ "Unexpected result " ++ show result @@ -93,7 +107,7 @@ returnCat sid cat = castServer sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () closeShop sid = do - result <- callServer sid NoTimeout CloseShop + result <- callServer sid Infinity CloseShop case result of ShopClosed -> return () _ -> error $ "Unexpected result " ++ show result @@ -104,24 +118,25 @@ closeShop sid = do -- %%% Server functions +handleKitty :: Handler [Cat] CatCmd CatEv handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed +handleReturn :: Handler [Cat] ReturnCat () handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index ecee315f..da8946ca 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,10 +1,15 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} module TestGenServer where -import Data.Binary (Binary(..)) +import Data.Binary (Binary (..), getWord8, + putWord8) import Data.Typeable (Typeable) -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar +import Data.DeriveTH +import Data.Foldable (forM_) +import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent.MVar ( MVar , newEmptyMVar , putMVar @@ -16,14 +21,14 @@ import Control.Exception (SomeException, throwIO) import qualified Control.Exception as Ex (catch) import Control.Applicative ((<$>), (<*>), pure, (<|>)) import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP +import Network.Transport.TCP ( createTransportExposeInternals , TransportInternals(socketBetween) , defaultTCPParameters ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) , LocalNode(localEndPoint) ) import Control.Distributed.Process.Node @@ -33,58 +38,130 @@ import Test.HUnit (Assertion) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase) +import Control.Distributed.Platform.GenServer +import GenServer.Counter +import GenServer.Kitty + -------------------------------------------------------------------------------- -- The tests proper -- -------------------------------------------------------------------------------- -newtype Ping = Ping ProcessId - deriving (Typeable, Binary, Show) +data Ping = Ping + deriving (Typeable, Show) +$(derive makeBinary ''Ping) -newtype Pong = Pong ProcessId - deriving (Typeable, Binary, Show) +data Pong = Pong + deriving (Typeable, Show) +$(derive makeBinary ''Pong) --- | The ping server from the paper -ping :: Process () -ping = do - Pong partner <- expect - self <- getSelfPid - send partner (Ping self) - ping --- | Basic ping test -testPing :: NT.Transport -> Assertion +-- | Test ping server +-- TODO fix this test! +testPing :: NT.Transport -> Assertion testPing transport = do - serverAddr <- newEmptyMVar - clientDone <- newEmptyMVar + initDone <- newEmptyMVar + pingDone <- newEmptyMVar + pongDone <- newEmptyMVar + terminateDone <- newEmptyMVar + + localNode <- newLocalNode transport initRemoteTable + + runProcess localNode $ do + say "Starting ..." + sid <- startServer (0 :: Int) defaultServer { + initHandler = do + trace "Init ..." + c <- getState + liftIO $ putMVar initDone c + initOk Infinity, + terminateHandler = \reason -> do + trace "Terminate ..." + c <- getState + liftIO $ putMVar terminateDone c + return (), + handlers = [ + handle (\Ping -> do + trace "Ping ..." + modifyState (1 +) + c <- getState + --liftIO $ putMVar pingDone c + ok Pong), + handle (\Pong -> do + trace "Pong ..." + modifyState (1 +) + c <- getState + --liftIO $ putMVar pongDone c + ok ()) + ] + } + --liftIO $ takeMVar initDone + --replicateM_ 10 $ do + Pong <- callServer sid Infinity Ping + --liftIO $ takeMVar pingDone + castServer sid Ping + --liftIO $ takeMVar pongDone + --return () + exit sid () + liftIO $ takeMVar terminateDone + return () + + + +-- | Test counter server +-- TODO split me! +testCounter :: NT.Transport -> Assertion +testCounter transport = do + serverDone <- newEmptyMVar + + localNode <- newLocalNode transport initRemoteTable + + runProcess localNode $ do + cid <- startCounter 0 + c <- getCount cid + incCount cid + incCount cid + c <- getCount cid + resetCount cid + c2 <- getCount cid + stopCounter cid + liftIO $ putMVar serverDone True + return () + + liftIO $ takeMVar serverDone + return () + - -- Server - forkIO $ do - localNode <- newLocalNode transport initRemoteTable - addr <- forkProcess localNode ping - putMVar serverAddr addr +-- | Test kitty server +-- TODO split me! +testKitty :: NT.Transport -> Assertion +testKitty transport = do + serverDone <- newEmptyMVar - -- Client - forkIO $ do - localNode <- newLocalNode transport initRemoteTable - pingServer <- readMVar serverAddr + localNode <- newLocalNode transport initRemoteTable - let numPings = 10000 + runProcess localNode $ do + kPid <- startKitty [Cat "c1" "black" "a black cat"] + replicateM_ 100 $ do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + closeShop kPid + stopKitty kPid + liftIO $ putMVar serverDone True + return () - runProcess localNode $ do - pid <- getSelfPid - replicateM_ numPings $ do - send pingServer (Pong pid) - Ping _ <- expect - return () + liftIO $ takeMVar serverDone + return () - putMVar clientDone () - takeMVar clientDone tests :: NT.Transport -> [Test] tests transport = [ testGroup "Basic features" [ - testCase "Ping" (testPing transport) + --testCase "Ping" (testPing transport), + testCase "Counter" (testCounter transport), + testCase "Kitty" (testKitty transport) ] ] From 333510a3a43d5b5d4783e85b6bdad9bd2a842cb4 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0389/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 14 +- src/Control/Distributed/Platform/GenServer.hs | 316 ++++++++++-------- .../Examples => tests/GenServer}/Counter.hs | 49 +-- .../Examples => tests/GenServer}/Kitty.hs | 37 +- tests/TestGenServer.hs | 159 ++++++--- 5 files changed, 354 insertions(+), 221 deletions(-) rename {src/Control/Distributed/Examples => tests/GenServer}/Counter.hs (72%) rename {src/Control/Distributed/Examples => tests/GenServer}/Kitty.hs (76%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2349f3b1..0ff5b162 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -16,8 +16,8 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform -library - build-depends: +library + build-depends: base >= 4, distributed-process, derive, @@ -27,19 +27,20 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer test-suite TestTimer type: exitcode-stdio-1.0 x-uses-tf: true - build-depends: + build-depends: base >= 4.4 && < 5, ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, network-transport >= 0.3 && < 0.4, + mtl, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -47,11 +48,10 @@ test-suite TestTimer test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers - hs-source-dirs: - src, + hs-source-dirs: + src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer extensions: CPP main-is: TestMain.hs - diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b64b736f..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -33,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -40,40 +38,53 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, Process, ProcessId, expect, expectTimeout, + monitor, link, + exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, send, spawnLocal) +import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- + -- | ServerId type ServerId = ProcessId -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) -- | Initialize handler result data InitResult @@ -95,52 +106,39 @@ data TerminateReason $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) + + -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) + -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state @@ -167,61 +165,56 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) - -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +-- +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) + + + +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \_ (Message _ req) -> cond req -} - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage _ payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \_ (Message _ msg) -> cond msg + dispatchIf = \_ msg -> cond (msgPayload msg) } -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -229,7 +222,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -237,9 +230,9 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - msgHandlers = [], - terminateHandler = \_ -> return () + initHandler = return $ InitOk Infinity, + handlers = [], + terminateHandler = \_ -> return () } -------------------------------------------------------------------------------- @@ -248,16 +241,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - _ <- ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH hs s + initH = initHandler ls + terminateH = terminateHandler ls + hs = handlers ls + + + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- | call a server identified by it's ServerId + + +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) + + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of Infinity -> expect Timeout time -> do @@ -270,12 +286,14 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason -- | Get the server state getState :: Server s s @@ -294,30 +312,38 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () + + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () + + -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r + + -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) @@ -326,25 +352,33 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of Infinity -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms + mayResult <- lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason + -- | Log a trace message using the underlying Process's say trace :: String -> Server s () -trace msg = ST.lift . say $ msg +trace msg = lift . say $ msg + + + +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state diff --git a/src/Control/Distributed/Examples/Counter.hs b/tests/GenServer/Counter.hs similarity index 72% rename from src/Control/Distributed/Examples/Counter.hs rename to tests/GenServer/Counter.hs index 1e850614..9a3effbb 100644 --- a/src/Control/Distributed/Examples/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -1,6 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Examples.Counter( +module GenServer.Counter( startCounter, stopCounter, getCount, @@ -50,32 +50,39 @@ $(derive makeBinary ''ResetCount) -- | Start a counter server startCounter :: Int -> Process ServerId startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} + initHandler = do + --c <- getState + --trace $ "Counter init: " ++ show c + initOk Infinity, + terminateHandler = const (return ()), + --trace $ "Counter terminate: " ++ show r, + handlers = [ + handle handleCounter, + handle handleReset + ] +} -- | Stop the counter server stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal +stopCounter sid = stopServer sid () -- | Increment count incCount :: ServerId -> Process () incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () + CounterIncremented <- callServer sid Infinity IncrementCounter + return () -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c + Count c <- callServer sid Infinity GetCount + return c @@ -88,19 +95,19 @@ resetCount sid = castServer sid ResetCount -- IMPL -- -------------------------------------------------------------------------------- - +handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - + count <- getState + modifyState (+1) + if count > 10 + then stop CounterIncremented "Stopping because 'Count > 10'" + else ok CounterIncremented handleCounter GetCount = do - count <- getState - callOk (Count count) + count <- getState + ok (Count count) +handleReset :: Handler Int ResetCount () handleReset ResetCount = do - putState 0 - castOk + putState 0 + ok () diff --git a/src/Control/Distributed/Examples/Kitty.hs b/tests/GenServer/Kitty.hs similarity index 76% rename from src/Control/Distributed/Examples/Kitty.hs rename to tests/GenServer/Kitty.hs index 9620660b..36205a04 100644 --- a/src/Control/Distributed/Examples/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -4,9 +4,10 @@ -- -- -module(kitty_server). -- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Examples.Kitty +module GenServer.Kitty ( startKitty, + stopKitty, orderCat, returnCat, closeShop, @@ -68,16 +69,29 @@ $( derive makeBinary ''CatEv ) -- | Start a counter server startKitty :: [Cat] -> Process ServerId startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn + initHandler = do + --cs <- getState + --trace $ "Kitty init: " ++ show cs + initOk Infinity, + terminateHandler = const $ return (), + --trace $ "Kitty terminate: " ++ show r, + handlers = [ + handle handleKitty, + handle handleReturn ]} + +-- | Stop the kitty server +stopKitty :: ServerId -> Process () +stopKitty sid = stopServer sid () + + + -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do - result <- callServer sid NoTimeout (OrderCat name color descr) + result <- callServer sid Infinity (OrderCat name color descr) case result of CatOrdered c -> return c _ -> error $ "Unexpected result " ++ show result @@ -93,7 +107,7 @@ returnCat sid cat = castServer sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () closeShop sid = do - result <- callServer sid NoTimeout CloseShop + result <- callServer sid Infinity CloseShop case result of ShopClosed -> return () _ -> error $ "Unexpected result " ++ show result @@ -104,24 +118,25 @@ closeShop sid = do -- %%% Server functions +handleKitty :: Handler [Cat] CatCmd CatEv handleKitty (OrderCat name color descr) = do cats <- getState - trace $ "Kitty inventory: " ++ show cats case cats of [] -> do let cat = Cat name color descr putState (cat:cats) - callOk (CatOrdered cat) + ok (CatOrdered cat) (x:xs) -> do -- TODO find cat with same features putState xs - callOk (CatOrdered x) + ok (CatOrdered x) handleKitty CloseShop = do putState [] - callOk ShopClosed + ok ShopClosed +handleReturn :: Handler [Cat] ReturnCat () handleReturn (ReturnCat cat) = do modifyState (cat :) - castOk + ok () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index ecee315f..da8946ca 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,10 +1,15 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} module TestGenServer where -import Data.Binary (Binary(..)) +import Data.Binary (Binary (..), getWord8, + putWord8) import Data.Typeable (Typeable) -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar +import Data.DeriveTH +import Data.Foldable (forM_) +import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent.MVar ( MVar , newEmptyMVar , putMVar @@ -16,14 +21,14 @@ import Control.Exception (SomeException, throwIO) import qualified Control.Exception as Ex (catch) import Control.Applicative ((<$>), (<*>), pure, (<|>)) import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP +import Network.Transport.TCP ( createTransportExposeInternals , TransportInternals(socketBetween) , defaultTCPParameters ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) +import Control.Distributed.Process.Internal.Types + ( NodeId(nodeAddress) , LocalNode(localEndPoint) ) import Control.Distributed.Process.Node @@ -33,58 +38,130 @@ import Test.HUnit (Assertion) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase) +import Control.Distributed.Platform.GenServer +import GenServer.Counter +import GenServer.Kitty + -------------------------------------------------------------------------------- -- The tests proper -- -------------------------------------------------------------------------------- -newtype Ping = Ping ProcessId - deriving (Typeable, Binary, Show) +data Ping = Ping + deriving (Typeable, Show) +$(derive makeBinary ''Ping) -newtype Pong = Pong ProcessId - deriving (Typeable, Binary, Show) +data Pong = Pong + deriving (Typeable, Show) +$(derive makeBinary ''Pong) --- | The ping server from the paper -ping :: Process () -ping = do - Pong partner <- expect - self <- getSelfPid - send partner (Ping self) - ping --- | Basic ping test -testPing :: NT.Transport -> Assertion +-- | Test ping server +-- TODO fix this test! +testPing :: NT.Transport -> Assertion testPing transport = do - serverAddr <- newEmptyMVar - clientDone <- newEmptyMVar + initDone <- newEmptyMVar + pingDone <- newEmptyMVar + pongDone <- newEmptyMVar + terminateDone <- newEmptyMVar + + localNode <- newLocalNode transport initRemoteTable + + runProcess localNode $ do + say "Starting ..." + sid <- startServer (0 :: Int) defaultServer { + initHandler = do + trace "Init ..." + c <- getState + liftIO $ putMVar initDone c + initOk Infinity, + terminateHandler = \reason -> do + trace "Terminate ..." + c <- getState + liftIO $ putMVar terminateDone c + return (), + handlers = [ + handle (\Ping -> do + trace "Ping ..." + modifyState (1 +) + c <- getState + --liftIO $ putMVar pingDone c + ok Pong), + handle (\Pong -> do + trace "Pong ..." + modifyState (1 +) + c <- getState + --liftIO $ putMVar pongDone c + ok ()) + ] + } + --liftIO $ takeMVar initDone + --replicateM_ 10 $ do + Pong <- callServer sid Infinity Ping + --liftIO $ takeMVar pingDone + castServer sid Ping + --liftIO $ takeMVar pongDone + --return () + exit sid () + liftIO $ takeMVar terminateDone + return () + + + +-- | Test counter server +-- TODO split me! +testCounter :: NT.Transport -> Assertion +testCounter transport = do + serverDone <- newEmptyMVar + + localNode <- newLocalNode transport initRemoteTable + + runProcess localNode $ do + cid <- startCounter 0 + c <- getCount cid + incCount cid + incCount cid + c <- getCount cid + resetCount cid + c2 <- getCount cid + stopCounter cid + liftIO $ putMVar serverDone True + return () + + liftIO $ takeMVar serverDone + return () + - -- Server - forkIO $ do - localNode <- newLocalNode transport initRemoteTable - addr <- forkProcess localNode ping - putMVar serverAddr addr +-- | Test kitty server +-- TODO split me! +testKitty :: NT.Transport -> Assertion +testKitty transport = do + serverDone <- newEmptyMVar - -- Client - forkIO $ do - localNode <- newLocalNode transport initRemoteTable - pingServer <- readMVar serverAddr + localNode <- newLocalNode transport initRemoteTable - let numPings = 10000 + runProcess localNode $ do + kPid <- startKitty [Cat "c1" "black" "a black cat"] + replicateM_ 100 $ do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + closeShop kPid + stopKitty kPid + liftIO $ putMVar serverDone True + return () - runProcess localNode $ do - pid <- getSelfPid - replicateM_ numPings $ do - send pingServer (Pong pid) - Ping _ <- expect - return () + liftIO $ takeMVar serverDone + return () - putMVar clientDone () - takeMVar clientDone tests :: NT.Transport -> [Test] tests transport = [ testGroup "Basic features" [ - testCase "Ping" (testPing transport) + --testCase "Ping" (testPing transport), + testCase "Counter" (testCounter transport), + testCase "Kitty" (testKitty transport) ] ] From fd1d816d23cbecb15034e0303521acd4a920c0e5 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 12:02:45 -0500 Subject: [PATCH 0390/2357] Now calling terminateHandler on exceptions & cleaned up the implementation a bit --- distributed-process-platform.cabal | 14 +- src/Control/Distributed/Examples/Counter.hs | 106 ------ src/Control/Distributed/Examples/Kitty.hs | 127 ------- src/Control/Distributed/Platform/GenServer.hs | 316 ++++++++++-------- 4 files changed, 182 insertions(+), 381 deletions(-) delete mode 100644 src/Control/Distributed/Examples/Counter.hs delete mode 100644 src/Control/Distributed/Examples/Kitty.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2349f3b1..0ff5b162 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -16,8 +16,8 @@ source-repository head type: git location: https://github.com/hyperthunk/distributed-process-platform -library - build-depends: +library + build-depends: base >= 4, distributed-process, derive, @@ -27,19 +27,20 @@ library transformers hs-source-dirs: src ghc-options: -Wall - exposed-modules: + exposed-modules: Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer test-suite TestTimer type: exitcode-stdio-1.0 x-uses-tf: true - build-depends: + build-depends: base >= 4.4 && < 5, ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, network-transport >= 0.3 && < 0.4, + mtl, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -47,11 +48,10 @@ test-suite TestTimer test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers - hs-source-dirs: - src, + hs-source-dirs: + src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer extensions: CPP main-is: TestMain.hs - diff --git a/src/Control/Distributed/Examples/Counter.hs b/src/Control/Distributed/Examples/Counter.hs deleted file mode 100644 index 1e850614..00000000 --- a/src/Control/Distributed/Examples/Counter.hs +++ /dev/null @@ -1,106 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Examples.Counter( - startCounter, - stopCounter, - getCount, - incCount, - resetCount - ) where - -import Control.Distributed.Platform.GenServer - -import Data.Binary (Binary (..), getWord8, - putWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) - --------------------------------------------------------------------------------- --- Types -- --------------------------------------------------------------------------------- - - --- Call request(s) -data CounterRequest - = IncrementCounter - | GetCount - deriving (Show, Typeable) -$(derive makeBinary ''CounterRequest) - - - --- Call response(s) -data CounterResponse - = CounterIncremented - | Count Int - deriving (Show, Typeable) -$(derive makeBinary ''CounterResponse) - - - --- Cast message(s) -data ResetCount = ResetCount deriving (Show, Typeable) -$(derive makeBinary ''ResetCount) - - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a counter server -startCounter :: Int -> Process ServerId -startCounter count = startServer count defaultServer { - msgHandlers = [ - handleCall handleCounter, - handleCast handleReset -]} - - - --- | Stop the counter server -stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid TerminateNormal - - - --- | Increment count -incCount :: ServerId -> Process () -incCount sid = do - CounterIncremented <- callServer sid NoTimeout IncrementCounter - return () - - - --- | Get the current count -getCount :: ServerId -> Process Int -getCount sid = do - Count c <- callServer sid NoTimeout GetCount - return c - - - --- | Reset the current count -resetCount :: ServerId -> Process () -resetCount sid = castServer sid ResetCount - - --------------------------------------------------------------------------------- --- IMPL -- --------------------------------------------------------------------------------- - - -handleCounter IncrementCounter = do - modifyState (+1) - count <- getState - if count > 10 - then callStop CounterIncremented "Count > 10" - else callOk CounterIncremented - -handleCounter GetCount = do - count <- getState - callOk (Count count) - - -handleReset ResetCount = do - putState 0 - castOk diff --git a/src/Control/Distributed/Examples/Kitty.hs b/src/Control/Distributed/Examples/Kitty.hs deleted file mode 100644 index 9620660b..00000000 --- a/src/Control/Distributed/Examples/Kitty.hs +++ /dev/null @@ -1,127 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - --- --- -module(kitty_server). --- -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -module Control.Distributed.Examples.Kitty - ( - startKitty, - orderCat, - returnCat, - closeShop, - Cat(..) - ) where - -import Control.Distributed.Platform.GenServer - -import Data.Binary (Binary (..), getWord8, - putWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) - --- --- % Records/Data Types --- -record(cat, {name, color=green, description}). - -type Color = String -type Description = String -type Name = String - - - -data Cat = Cat { - catName :: Name, - catColor :: Color, - catDescr :: Description } - deriving (Show, Typeable) -$( derive makeBinary ''Cat ) - - - -data CatCmd - = OrderCat String String String - | CloseShop - deriving (Show, Typeable) -$( derive makeBinary ''CatCmd ) - - - -data ReturnCat - = ReturnCat Cat - deriving (Show, Typeable) -$( derive makeBinary ''ReturnCat ) - - - -data CatEv - = CatOrdered Cat - | ShopClosed - deriving (Show, Typeable) -$( derive makeBinary ''CatEv ) - - - --- --- %% Client API --- start_link() -> spawn_link(fun init/0). --- | Start a counter server -startKitty :: [Cat] -> Process ServerId -startKitty cats = startServer cats defaultServer { - msgHandlers = [ - handleCall handleKitty, - handleCast handleReturn -]} - - --- %% Synchronous call -orderCat :: ServerId -> Name -> Color -> Description -> Process Cat -orderCat sid name color descr = do - result <- callServer sid NoTimeout (OrderCat name color descr) - case result of - CatOrdered c -> return c - _ -> error $ "Unexpected result " ++ show result - - - --- %% async call -returnCat :: ServerId -> Cat -> Process () -returnCat sid cat = castServer sid (ReturnCat cat) - - - --- %% sync call -closeShop :: ServerId -> Process () -closeShop sid = do - result <- callServer sid NoTimeout CloseShop - case result of - ShopClosed -> return () - _ -> error $ "Unexpected result " ++ show result - - - --- --- %%% Server functions - - -handleKitty (OrderCat name color descr) = do - cats <- getState - trace $ "Kitty inventory: " ++ show cats - case cats of - [] -> do - let cat = Cat name color descr - putState (cat:cats) - callOk (CatOrdered cat) - (x:xs) -> do -- TODO find cat with same features - putState xs - callOk (CatOrdered x) - -handleKitty CloseShop = do - putState [] - callOk ShopClosed - - - -handleReturn (ReturnCat cat) = do - modifyState (cat :) - castOk diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b64b736f..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer module Control.Distributed.Platform.GenServer ( @@ -12,20 +13,15 @@ module Control.Distributed.Platform.GenServer ( Timeout(..), initOk, initStop, - callOk, - callForward, - callStop, - castOk, - castForward, - castStop, - TerminateReason(..), + ok, + forward, + stop, InitHandler, + Handler, TerminateHandler, MessageDispatcher(), - handleCall, - handleCallIf, - handleCast, - handleCastIf, + handle, + handleIf, handleAny, putState, getState, @@ -33,6 +29,8 @@ module Control.Distributed.Platform.GenServer ( LocalServer(..), defaultServer, startServer, + startServerLink, + startServerMonitor, callServer, castServer, stopServer, @@ -40,40 +38,53 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Distributed.Process (AbstractMessage (forward), - Match, +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) +import qualified Control.Distributed.Process as P (forward, catch) +import Control.Distributed.Process (AbstractMessage, + Match, MonitorRef, Process, ProcessId, expect, expectTimeout, + monitor, link, + exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, send, spawnLocal) +import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (StateT, - get, lift, - modify, put, - runStateT) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- + -- | ServerId type ServerId = ProcessId -- | Server monad -type Server s = ST.StateT s Process +newtype Server s a = Server { + unServer :: ST.StateT s Process a + } + deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) -- | Initialize handler result data InitResult @@ -95,52 +106,39 @@ data TerminateReason $(derive makeBinary ''TerminateReason) -- | The result of a call -data CallResult a - = CallOk a - | CallForward ServerId - | CallStop a String +data Result a + = Ok a + | Forward ServerId + | Stop a String deriving (Show, Typeable) -callOk :: a -> Server s (CallResult a) -callOk resp = return (CallOk resp) - -callForward :: ServerId -> Server s (CallResult a) -callForward sid = return (CallForward sid) -callStop :: a -> String -> Server s (CallResult a) -callStop resp reason = return (CallStop resp reason) +ok :: (Serializable a, Show a) => a -> Server s (Result a) +ok resp = return (Ok resp) --- | The result of a cast -data CastResult - = CastOk - | CastForward ServerId - | CastStop String +forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) +forward sid = return (Forward sid) -castOk :: Server s CastResult -castOk = return CastOk +stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) +stop resp reason = return (Stop resp reason) -castForward :: ServerId -> Server s CastResult -castForward sid = return (CastForward sid) -castStop :: String -> Server s CastResult -castStop reason = return (CastStop reason) -- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type CallHandler s a b = a -> Server s (CallResult b) -type CastHandler s a = a -> Server s CastResult +type InitHandler s = Server s InitResult +type TerminateHandler s = TerminateReason -> Server s () +type Handler s a b = a -> Server s (Result b) + + -- | Adds routing metadata to the actual payload -data Message a = Message ProcessId a +data Message a = + CallMessage { msgFrom :: ProcessId, msgPayload :: a } + | CastMessage { msgFrom :: ProcessId, msgPayload :: a } deriving (Show, Typeable) $(derive makeBinary ''Message) --- | Management message --- TODO is there a std way of terminating a process from another process? -data ManageServer = TerminateServer TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''ManageServer) + -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state @@ -167,61 +165,56 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherAny d) = matchAny (d s) -- | Constructs a call message dispatcher -handleCall :: (Serializable a, Show a, Serializable b) => CallHandler s a b -> MessageDispatcher s -handleCall = handleCallIf (const True) - -handleCallIf :: (Serializable a, Show a, Serializable b) => (a -> Bool) -> CallHandler s a b -> MessageDispatcher s -handleCallIf cond handler = MessageDispatcherIf { - dispatcher = (\state m@(Message cid req) -> do - say $ "Server got CALL: [" ++ show cid ++ " / " ++ show req ++ "]" - (r, s') <- ST.runStateT (handler req) state +-- +handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s +handle = handleIf (const True) + + + +handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s +handleIf cond handler = MessageDispatcherIf { + dispatcher = (\s msg -> case msg of + CallMessage cid payload -> do + --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CallOk resp -> do + Ok resp -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Nothing) - CallForward sid -> do - send sid m + Forward sid -> do + --say $ "Server FORWARD to: " ++ show sid + send sid msg return (s', Nothing) - CallStop resp reason -> do + Stop resp reason -> do + --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - ), - dispatchIf = \_ (Message _ req) -> cond req -} - --- | Constructs a cast message dispatcher --- -handleCast :: (Serializable a, Show a) => CastHandler s a -> MessageDispatcher s -handleCast = handleCastIf (const True) - --- | -handleCastIf :: (Serializable a, Show a) => (a -> Bool) -> CastHandler s a -> MessageDispatcher s -handleCastIf cond handler = MessageDispatcherIf { - dispatcher = (\s m@(Message cid msg) -> do - say $ "Server got CAST: [" ++ show cid ++ " / " ++ show msg ++ "]" - (r, s') <- ST.runStateT (handler msg) s + CastMessage _ payload -> do + --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" + (r, s') <- runServer (handler payload) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - send sid m + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + send sid msg return (s', Nothing) ), - dispatchIf = \_ (Message _ msg) -> cond msg + dispatchIf = \_ msg -> cond (msgPayload msg) } -- | Constructs a dispatcher for any message -- Note that since we don't know the type of this message it assumes the protocol of a cast -- i.e. no reply's -handleAny :: (AbstractMessage -> Server s (CastResult)) -> MessageDispatcher s +handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s handleAny handler = MessageDispatcherAny { dispatcherAny = (\s m -> do - (r, s') <- ST.runStateT (handler m) s + (r, s') <- runServer (handler m) s case r of - CastStop reason -> return (s', Just $ TerminateReason reason) - CastOk -> return (s', Nothing) - CastForward sid -> do - (forward m) sid + Stop _ reason -> return (s', Just $ TerminateReason reason) + Ok _ -> return (s', Nothing) + Forward sid -> do + (P.forward m) sid return (s', Nothing) ) } @@ -229,7 +222,7 @@ handleAny handler = MessageDispatcherAny { -- | The server callbacks data LocalServer s = LocalServer { initHandler :: InitHandler s, -- ^ initialization handler - msgHandlers :: [MessageDispatcher s], + handlers :: [MessageDispatcher s], terminateHandler :: TerminateHandler s -- ^ termination handler } @@ -237,9 +230,9 @@ data LocalServer s = LocalServer { ---- Starting point for creating new servers defaultServer :: LocalServer s defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - msgHandlers = [], - terminateHandler = \_ -> return () + initHandler = return $ InitOk Infinity, + handlers = [], + terminateHandler = \_ -> return () } -------------------------------------------------------------------------------- @@ -248,16 +241,39 @@ defaultServer = LocalServer { -- | Start a new server and return it's id startServer :: s -> LocalServer s -> Process ServerId -startServer state handlers = spawnLocal $ do - _ <- ST.runStateT (processServer handlers) state - return () +startServer s ls = spawnLocal proc + where + proc = processServer initH terminateH hs s + initH = initHandler ls + terminateH = terminateHandler ls + hs = handlers ls + + + +-- | Spawn a process and link to it +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls + link pid + return pid --- | call a server identified by it's ServerId + + +-- | Like 'spawnServerLink', but monitor the spawned process +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls + ref <- monitor pid + return (pid, ref) + + + +-- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - say $ "Calling server " ++ show cid - send sid (Message cid rq) + --say $ "Calling server " ++ show cid + send sid (CallMessage cid rq) case timeout of Infinity -> expect Timeout time -> do @@ -270,12 +286,14 @@ callServer sid timeout rq = do castServer :: (Serializable a) => ServerId -> a -> Process () castServer sid msg = do cid <- getSelfPid - say $ "Casting server " ++ show cid - send sid (Message cid msg) + --say $ "Casting server " ++ show cid + send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: ServerId -> TerminateReason -> Process () -stopServer sid reason = castServer sid (TerminateServer reason) +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do + --say $ "Stop server " ++ show sid + exit sid reason -- | Get the server state getState :: Server s s @@ -294,30 +312,38 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -processServer :: LocalServer s -> Server s () -processServer localServer = do - ir <- processInit localServer - tr <- case ir of - InitOk to -> do - trace $ "Server ready to receive messages!" - processLoop localServer to - InitStop r -> return (TerminateReason r) - processTerminate localServer tr - --- | initialize server -processInit :: LocalServer s -> Server s InitResult -processInit localServer = do - trace $ "Server initializing ... " - ir <- initHandler localServer - return ir +processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () +processServer initH terminateH dispatchers s = do + (ir, s') <- runServer initH s + P.catch (proc ir s') (exitHandler s') + where + + proc ir s' = do + (tr, s'') <- runServer (processLoop dispatchers ir) s' + _ <- runServer (terminateH tr) s'' + return () + + exitHandler s' e = do + let tr = TerminateReason $ show (e :: SomeException) + _ <- runServer (terminateH tr) s' + return () + + -- | server loop -processLoop :: LocalServer s -> Timeout -> Server s TerminateReason -processLoop localServer t = do - mayMsg <- processReceive (msgHandlers localServer) t - case mayMsg of - Just r -> return r - Nothing -> processLoop localServer t +processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason +processLoop dispatchers ir = do + case ir of + InitOk t -> loop dispatchers t + InitStop r -> return $ TerminateReason r + where + loop ds t = do + msgM <- processReceive ds t + case msgM of + Nothing -> loop ds t + Just r -> return r + + -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) @@ -326,25 +352,33 @@ processReceive ds timeout = do let ms = map (matchMessage s) ds case timeout of Infinity -> do - (s', r) <- ST.lift $ receiveWait ms + (s', r) <- lift $ receiveWait ms putState s' return r Timeout t -> do - mayResult <- ST.lift $ receiveTimeout (intervalToMs t) ms + mayResult <- lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do putState s' return r Nothing -> do - trace "Receive timed out ..." + --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") --- | terminate server -processTerminate :: LocalServer s -> TerminateReason -> Server s () -processTerminate localServer reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler localServer) reason + -- | Log a trace message using the underlying Process's say trace :: String -> Server s () -trace msg = ST.lift . say $ msg +trace msg = lift . say $ msg + + + +-- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a +lift :: Process a -> Server s a +lift p = Server $ ST.lift p + + + +-- | +runServer :: Server s a -> s -> Process (a, s) +runServer server state = ST.runStateT (unServer server) state From 301557f6ba9fb4a76fd4149f3fa3a7a47b98996d Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0391/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..2ffff645 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 2e76bc5d684fa53d57d95882f9dcab6672280e7f Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0392/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..2ffff645 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 9b55089811d96f3107335d5fe273c9f1311890cb Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0393/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..2ffff645 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 27b7d6325b35776aa7df1e275472c6cb2a5f20ed Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0394/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..2ffff645 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s a = Server { +newtype Server s m a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 15800a0fb5837295a466345e1dbf4bbd6a571c5b Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0395/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 2ffff645..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 50e0619dd21fdbc398861b8205b9b9fdc6dd56fa Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0396/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 2ffff645..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From db36251598ff0f4a0b45357c808a477a0fb7fa4c Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0397/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 2ffff645..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From d3063f8eff4d7681e22413457cbd807c5ae5556e Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 15:12:11 -0500 Subject: [PATCH 0398/2357] cleaned up junk code --- src/Control/Distributed/Platform/GenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 2ffff645..92551615 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -81,7 +81,7 @@ import Data.Typeable (Typeable) type ServerId = ProcessId -- | Server monad -newtype Server s m a = Server { +newtype Server s a = Server { unServer :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) From 8eb1880a33481e2076ff7123f7fed02978693def Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0399/2357] Few cosmetic changes --- src/Control/Distributed/Platform/GenServer.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..cc22a671 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -164,6 +164,8 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) + + -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s From e051fc29a760d2dba849f95a4352ddb31f48462a Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0400/2357] Few cosmetic changes --- src/Control/Distributed/Platform/GenServer.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..cc22a671 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -164,6 +164,8 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) + + -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s From 74e7b2702e13c79c7b16043e18604047efb18dbc Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0401/2357] Few cosmetic changes --- src/Control/Distributed/Platform/GenServer.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..cc22a671 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -164,6 +164,8 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) + + -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s From 0ac221dbcaa819edb08a13f4cd9229f286df7b1a Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 16:27:13 -0500 Subject: [PATCH 0402/2357] Few cosmetic changes --- src/Control/Distributed/Platform/GenServer.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 92551615..cc22a671 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -164,6 +164,8 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) + + -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s From 37e92d1490491a9623683f2297fc92fa1bcd82db Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 18:45:41 -0500 Subject: [PATCH 0403/2357] rebased branch and moved main functionality to TestGenServer --- src/Control/Distributed/Platform/GenServer.hs | 33 +------ tests/TestGenServer.hs | 98 ++++++++++--------- tests/TestMain.hs | 5 + 3 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index cc22a671..cab13950 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,12 +71,10 @@ import Data.Binary (Binary (..), import Data.DeriveTH import Data.Typeable (Typeable) - -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- - -- | ServerId type ServerId = ProcessId @@ -112,7 +110,6 @@ data Result a | Stop a String deriving (Show, Typeable) - ok :: (Serializable a, Show a) => a -> Server s (Result a) ok resp = return (Ok resp) @@ -122,15 +119,11 @@ forward sid = return (Forward sid) stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) stop resp reason = return (Stop resp reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type Handler s a b = a -> Server s (Result b) - - -- | Adds routing metadata to the actual payload data Message a = CallMessage { msgFrom :: ProcessId, msgPayload :: a } @@ -138,8 +131,6 @@ data Message a = deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s = @@ -164,15 +155,11 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) - - -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s handle = handleIf (const True) - - handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s handleIf cond handler = MessageDispatcherIf { dispatcher = (\s msg -> case msg of @@ -250,8 +237,6 @@ startServer s ls = spawnLocal proc terminateH = terminateHandler ls hs = handlers ls - - -- | Spawn a process and link to it startServerLink :: s -> LocalServer s -> Process ServerId startServerLink s ls = do @@ -259,8 +244,6 @@ startServerLink s ls = do link pid return pid - - -- | Like 'spawnServerLink', but monitor the spawned process startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerMonitor s ls = do @@ -268,13 +251,11 @@ startServerMonitor s ls = do ref <- monitor pid return (pid, ref) - - -- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - --say $ "Calling server " ++ show cid + say $ "Calling server " ++ show cid send sid (CallMessage cid rq) case timeout of Infinity -> expect @@ -319,19 +300,15 @@ processServer initH terminateH dispatchers s = do (ir, s') <- runServer initH s P.catch (proc ir s') (exitHandler s') where - proc ir s' = do (tr, s'') <- runServer (processLoop dispatchers ir) s' _ <- runServer (terminateH tr) s'' return () - exitHandler s' e = do let tr = TerminateReason $ show (e :: SomeException) _ <- runServer (terminateH tr) s' return () - - -- | server loop processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason processLoop dispatchers ir = do @@ -345,8 +322,6 @@ processLoop dispatchers ir = do Nothing -> loop ds t Just r -> return r - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -367,20 +342,14 @@ processReceive ds timeout = do --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = lift . say $ msg - - -- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a lift :: Process a -> Server s a lift p = Server $ ST.lift p - - -- | runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index da8946ca..8d4e2c7c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -3,6 +3,7 @@ {-# LANGUAGE TemplateHaskell #-} module TestGenServer where +import System.IO (hPutStrLn, stderr) import Data.Binary (Binary (..), getWord8, putWord8) import Data.Typeable (Typeable) @@ -39,6 +40,7 @@ import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase) import Control.Distributed.Platform.GenServer +import Control.Distributed.Platform.Internal.Types import GenServer.Counter import GenServer.Kitty @@ -63,47 +65,53 @@ testPing transport = do pingDone <- newEmptyMVar pongDone <- newEmptyMVar terminateDone <- newEmptyMVar + serverAddr <- newEmptyMVar localNode <- newLocalNode transport initRemoteTable - runProcess localNode $ do - say "Starting ..." - sid <- startServer (0 :: Int) defaultServer { - initHandler = do - trace "Init ..." - c <- getState - liftIO $ putMVar initDone c - initOk Infinity, - terminateHandler = \reason -> do - trace "Terminate ..." - c <- getState - liftIO $ putMVar terminateDone c - return (), - handlers = [ - handle (\Ping -> do - trace "Ping ..." - modifyState (1 +) - c <- getState - --liftIO $ putMVar pingDone c - ok Pong), - handle (\Pong -> do - trace "Pong ..." - modifyState (1 +) - c <- getState - --liftIO $ putMVar pongDone c - ok ()) - ] - } - --liftIO $ takeMVar initDone - --replicateM_ 10 $ do - Pong <- callServer sid Infinity Ping - --liftIO $ takeMVar pingDone - castServer sid Ping - --liftIO $ takeMVar pongDone - --return () - exit sid () - liftIO $ takeMVar terminateDone - return () + forkIO $ runProcess localNode $ do + --say "Starting ..." + sid <- startServer (0 :: Int) defaultServer { + initHandler = do + --trace "Init ..." + c <- getState + liftIO $ putMVar initDone c + initOk Infinity, + terminateHandler = \reason -> do + --trace "Terminate ..." + c <- getState + liftIO $ putMVar terminateDone c + return (), + handlers = [ + handle (\Ping -> do + --trace "Ping ..." + modifyState (+1) + c <- getState + liftIO $ putMVar pingDone c + ok Pong), + handle (\Pong -> do + --trace "Pong ..." + modifyState (1 +) + c <- getState + liftIO $ putMVar pongDone c + ok ()) + ]} + liftIO $ putMVar serverAddr sid + return () + + forkIO $ runProcess localNode $ do + sid <- liftIO $ takeMVar serverAddr + + liftIO $ takeMVar initDone + --replicateM_ 10 $ do + Pong <- callServer sid (Timeout (TimeInterval Seconds 10)) Ping + liftIO $ takeMVar pingDone + castServer sid Pong + liftIO $ takeMVar pongDone + exit sid () + + liftIO $ takeMVar terminateDone + return () @@ -141,11 +149,11 @@ testKitty transport = do runProcess localNode $ do kPid <- startKitty [Cat "c1" "black" "a black cat"] - replicateM_ 100 $ do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 + --replicateM_ 100 $ do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 closeShop kPid stopKitty kPid liftIO $ putMVar serverDone True @@ -159,9 +167,9 @@ testKitty transport = do tests :: NT.Transport -> [Test] tests transport = [ testGroup "Basic features" [ - --testCase "Ping" (testPing transport), testCase "Counter" (testCounter transport), - testCase "Kitty" (testKitty transport) + testCase "Kitty" (testKitty transport), + testCase "Ping" (testPing transport) ] ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index c6f75c46..3128e733 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,6 +6,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where +import System.IO (hSetBuffering, BufferMode(..), stdin, stdout, stderr) import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP @@ -22,6 +23,10 @@ tests transport internals = do main :: IO () main = do + hSetBuffering stdin NoBuffering + hSetBuffering stdout NoBuffering + hSetBuffering stderr NoBuffering + Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters testData <- tests transport internals From 594c418d2fd4a54c8f55996603d7d3369f210e48 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 18:45:41 -0500 Subject: [PATCH 0404/2357] rebased branch and moved main functionality to TestGenServer --- src/Control/Distributed/Platform/GenServer.hs | 33 +------ tests/TestGenServer.hs | 98 ++++++++++--------- tests/TestMain.hs | 5 + 3 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index cc22a671..cab13950 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,12 +71,10 @@ import Data.Binary (Binary (..), import Data.DeriveTH import Data.Typeable (Typeable) - -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- - -- | ServerId type ServerId = ProcessId @@ -112,7 +110,6 @@ data Result a | Stop a String deriving (Show, Typeable) - ok :: (Serializable a, Show a) => a -> Server s (Result a) ok resp = return (Ok resp) @@ -122,15 +119,11 @@ forward sid = return (Forward sid) stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) stop resp reason = return (Stop resp reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type Handler s a b = a -> Server s (Result b) - - -- | Adds routing metadata to the actual payload data Message a = CallMessage { msgFrom :: ProcessId, msgPayload :: a } @@ -138,8 +131,6 @@ data Message a = deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s = @@ -164,15 +155,11 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) - - -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s handle = handleIf (const True) - - handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s handleIf cond handler = MessageDispatcherIf { dispatcher = (\s msg -> case msg of @@ -250,8 +237,6 @@ startServer s ls = spawnLocal proc terminateH = terminateHandler ls hs = handlers ls - - -- | Spawn a process and link to it startServerLink :: s -> LocalServer s -> Process ServerId startServerLink s ls = do @@ -259,8 +244,6 @@ startServerLink s ls = do link pid return pid - - -- | Like 'spawnServerLink', but monitor the spawned process startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerMonitor s ls = do @@ -268,13 +251,11 @@ startServerMonitor s ls = do ref <- monitor pid return (pid, ref) - - -- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - --say $ "Calling server " ++ show cid + say $ "Calling server " ++ show cid send sid (CallMessage cid rq) case timeout of Infinity -> expect @@ -319,19 +300,15 @@ processServer initH terminateH dispatchers s = do (ir, s') <- runServer initH s P.catch (proc ir s') (exitHandler s') where - proc ir s' = do (tr, s'') <- runServer (processLoop dispatchers ir) s' _ <- runServer (terminateH tr) s'' return () - exitHandler s' e = do let tr = TerminateReason $ show (e :: SomeException) _ <- runServer (terminateH tr) s' return () - - -- | server loop processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason processLoop dispatchers ir = do @@ -345,8 +322,6 @@ processLoop dispatchers ir = do Nothing -> loop ds t Just r -> return r - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -367,20 +342,14 @@ processReceive ds timeout = do --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = lift . say $ msg - - -- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a lift :: Process a -> Server s a lift p = Server $ ST.lift p - - -- | runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index da8946ca..8d4e2c7c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -3,6 +3,7 @@ {-# LANGUAGE TemplateHaskell #-} module TestGenServer where +import System.IO (hPutStrLn, stderr) import Data.Binary (Binary (..), getWord8, putWord8) import Data.Typeable (Typeable) @@ -39,6 +40,7 @@ import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase) import Control.Distributed.Platform.GenServer +import Control.Distributed.Platform.Internal.Types import GenServer.Counter import GenServer.Kitty @@ -63,47 +65,53 @@ testPing transport = do pingDone <- newEmptyMVar pongDone <- newEmptyMVar terminateDone <- newEmptyMVar + serverAddr <- newEmptyMVar localNode <- newLocalNode transport initRemoteTable - runProcess localNode $ do - say "Starting ..." - sid <- startServer (0 :: Int) defaultServer { - initHandler = do - trace "Init ..." - c <- getState - liftIO $ putMVar initDone c - initOk Infinity, - terminateHandler = \reason -> do - trace "Terminate ..." - c <- getState - liftIO $ putMVar terminateDone c - return (), - handlers = [ - handle (\Ping -> do - trace "Ping ..." - modifyState (1 +) - c <- getState - --liftIO $ putMVar pingDone c - ok Pong), - handle (\Pong -> do - trace "Pong ..." - modifyState (1 +) - c <- getState - --liftIO $ putMVar pongDone c - ok ()) - ] - } - --liftIO $ takeMVar initDone - --replicateM_ 10 $ do - Pong <- callServer sid Infinity Ping - --liftIO $ takeMVar pingDone - castServer sid Ping - --liftIO $ takeMVar pongDone - --return () - exit sid () - liftIO $ takeMVar terminateDone - return () + forkIO $ runProcess localNode $ do + --say "Starting ..." + sid <- startServer (0 :: Int) defaultServer { + initHandler = do + --trace "Init ..." + c <- getState + liftIO $ putMVar initDone c + initOk Infinity, + terminateHandler = \reason -> do + --trace "Terminate ..." + c <- getState + liftIO $ putMVar terminateDone c + return (), + handlers = [ + handle (\Ping -> do + --trace "Ping ..." + modifyState (+1) + c <- getState + liftIO $ putMVar pingDone c + ok Pong), + handle (\Pong -> do + --trace "Pong ..." + modifyState (1 +) + c <- getState + liftIO $ putMVar pongDone c + ok ()) + ]} + liftIO $ putMVar serverAddr sid + return () + + forkIO $ runProcess localNode $ do + sid <- liftIO $ takeMVar serverAddr + + liftIO $ takeMVar initDone + --replicateM_ 10 $ do + Pong <- callServer sid (Timeout (TimeInterval Seconds 10)) Ping + liftIO $ takeMVar pingDone + castServer sid Pong + liftIO $ takeMVar pongDone + exit sid () + + liftIO $ takeMVar terminateDone + return () @@ -141,11 +149,11 @@ testKitty transport = do runProcess localNode $ do kPid <- startKitty [Cat "c1" "black" "a black cat"] - replicateM_ 100 $ do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 + --replicateM_ 100 $ do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 closeShop kPid stopKitty kPid liftIO $ putMVar serverDone True @@ -159,9 +167,9 @@ testKitty transport = do tests :: NT.Transport -> [Test] tests transport = [ testGroup "Basic features" [ - --testCase "Ping" (testPing transport), testCase "Counter" (testCounter transport), - testCase "Kitty" (testKitty transport) + testCase "Kitty" (testKitty transport), + testCase "Ping" (testPing transport) ] ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index c6f75c46..3128e733 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,6 +6,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where +import System.IO (hSetBuffering, BufferMode(..), stdin, stdout, stderr) import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP @@ -22,6 +23,10 @@ tests transport internals = do main :: IO () main = do + hSetBuffering stdin NoBuffering + hSetBuffering stdout NoBuffering + hSetBuffering stderr NoBuffering + Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters testData <- tests transport internals From 78850b2aa417ab9b796cc51592857cf201d5d45f Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 18:45:41 -0500 Subject: [PATCH 0405/2357] rebased branch and moved main functionality to TestGenServer --- src/Control/Distributed/Platform/GenServer.hs | 33 +------ tests/TestGenServer.hs | 98 ++++++++++--------- tests/TestMain.hs | 5 + 3 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index cc22a671..cab13950 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,12 +71,10 @@ import Data.Binary (Binary (..), import Data.DeriveTH import Data.Typeable (Typeable) - -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- - -- | ServerId type ServerId = ProcessId @@ -112,7 +110,6 @@ data Result a | Stop a String deriving (Show, Typeable) - ok :: (Serializable a, Show a) => a -> Server s (Result a) ok resp = return (Ok resp) @@ -122,15 +119,11 @@ forward sid = return (Forward sid) stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) stop resp reason = return (Stop resp reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type Handler s a b = a -> Server s (Result b) - - -- | Adds routing metadata to the actual payload data Message a = CallMessage { msgFrom :: ProcessId, msgPayload :: a } @@ -138,8 +131,6 @@ data Message a = deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s = @@ -164,15 +155,11 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) - - -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s handle = handleIf (const True) - - handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s handleIf cond handler = MessageDispatcherIf { dispatcher = (\s msg -> case msg of @@ -250,8 +237,6 @@ startServer s ls = spawnLocal proc terminateH = terminateHandler ls hs = handlers ls - - -- | Spawn a process and link to it startServerLink :: s -> LocalServer s -> Process ServerId startServerLink s ls = do @@ -259,8 +244,6 @@ startServerLink s ls = do link pid return pid - - -- | Like 'spawnServerLink', but monitor the spawned process startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerMonitor s ls = do @@ -268,13 +251,11 @@ startServerMonitor s ls = do ref <- monitor pid return (pid, ref) - - -- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - --say $ "Calling server " ++ show cid + say $ "Calling server " ++ show cid send sid (CallMessage cid rq) case timeout of Infinity -> expect @@ -319,19 +300,15 @@ processServer initH terminateH dispatchers s = do (ir, s') <- runServer initH s P.catch (proc ir s') (exitHandler s') where - proc ir s' = do (tr, s'') <- runServer (processLoop dispatchers ir) s' _ <- runServer (terminateH tr) s'' return () - exitHandler s' e = do let tr = TerminateReason $ show (e :: SomeException) _ <- runServer (terminateH tr) s' return () - - -- | server loop processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason processLoop dispatchers ir = do @@ -345,8 +322,6 @@ processLoop dispatchers ir = do Nothing -> loop ds t Just r -> return r - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -367,20 +342,14 @@ processReceive ds timeout = do --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = lift . say $ msg - - -- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a lift :: Process a -> Server s a lift p = Server $ ST.lift p - - -- | runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index da8946ca..8d4e2c7c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -3,6 +3,7 @@ {-# LANGUAGE TemplateHaskell #-} module TestGenServer where +import System.IO (hPutStrLn, stderr) import Data.Binary (Binary (..), getWord8, putWord8) import Data.Typeable (Typeable) @@ -39,6 +40,7 @@ import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase) import Control.Distributed.Platform.GenServer +import Control.Distributed.Platform.Internal.Types import GenServer.Counter import GenServer.Kitty @@ -63,47 +65,53 @@ testPing transport = do pingDone <- newEmptyMVar pongDone <- newEmptyMVar terminateDone <- newEmptyMVar + serverAddr <- newEmptyMVar localNode <- newLocalNode transport initRemoteTable - runProcess localNode $ do - say "Starting ..." - sid <- startServer (0 :: Int) defaultServer { - initHandler = do - trace "Init ..." - c <- getState - liftIO $ putMVar initDone c - initOk Infinity, - terminateHandler = \reason -> do - trace "Terminate ..." - c <- getState - liftIO $ putMVar terminateDone c - return (), - handlers = [ - handle (\Ping -> do - trace "Ping ..." - modifyState (1 +) - c <- getState - --liftIO $ putMVar pingDone c - ok Pong), - handle (\Pong -> do - trace "Pong ..." - modifyState (1 +) - c <- getState - --liftIO $ putMVar pongDone c - ok ()) - ] - } - --liftIO $ takeMVar initDone - --replicateM_ 10 $ do - Pong <- callServer sid Infinity Ping - --liftIO $ takeMVar pingDone - castServer sid Ping - --liftIO $ takeMVar pongDone - --return () - exit sid () - liftIO $ takeMVar terminateDone - return () + forkIO $ runProcess localNode $ do + --say "Starting ..." + sid <- startServer (0 :: Int) defaultServer { + initHandler = do + --trace "Init ..." + c <- getState + liftIO $ putMVar initDone c + initOk Infinity, + terminateHandler = \reason -> do + --trace "Terminate ..." + c <- getState + liftIO $ putMVar terminateDone c + return (), + handlers = [ + handle (\Ping -> do + --trace "Ping ..." + modifyState (+1) + c <- getState + liftIO $ putMVar pingDone c + ok Pong), + handle (\Pong -> do + --trace "Pong ..." + modifyState (1 +) + c <- getState + liftIO $ putMVar pongDone c + ok ()) + ]} + liftIO $ putMVar serverAddr sid + return () + + forkIO $ runProcess localNode $ do + sid <- liftIO $ takeMVar serverAddr + + liftIO $ takeMVar initDone + --replicateM_ 10 $ do + Pong <- callServer sid (Timeout (TimeInterval Seconds 10)) Ping + liftIO $ takeMVar pingDone + castServer sid Pong + liftIO $ takeMVar pongDone + exit sid () + + liftIO $ takeMVar terminateDone + return () @@ -141,11 +149,11 @@ testKitty transport = do runProcess localNode $ do kPid <- startKitty [Cat "c1" "black" "a black cat"] - replicateM_ 100 $ do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 + --replicateM_ 100 $ do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 closeShop kPid stopKitty kPid liftIO $ putMVar serverDone True @@ -159,9 +167,9 @@ testKitty transport = do tests :: NT.Transport -> [Test] tests transport = [ testGroup "Basic features" [ - --testCase "Ping" (testPing transport), testCase "Counter" (testCounter transport), - testCase "Kitty" (testKitty transport) + testCase "Kitty" (testKitty transport), + testCase "Ping" (testPing transport) ] ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index c6f75c46..3128e733 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,6 +6,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where +import System.IO (hSetBuffering, BufferMode(..), stdin, stdout, stderr) import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP @@ -22,6 +23,10 @@ tests transport internals = do main :: IO () main = do + hSetBuffering stdin NoBuffering + hSetBuffering stdout NoBuffering + hSetBuffering stderr NoBuffering + Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters testData <- tests transport internals From edf6a23e4f36adc5b8fee2fa935c351247448aa4 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 18:45:41 -0500 Subject: [PATCH 0406/2357] rebased branch and moved main functionality to TestGenServer --- src/Control/Distributed/Platform/GenServer.hs | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index cc22a671..cab13950 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,12 +71,10 @@ import Data.Binary (Binary (..), import Data.DeriveTH import Data.Typeable (Typeable) - -------------------------------------------------------------------------------- -- Data Types -- -------------------------------------------------------------------------------- - -- | ServerId type ServerId = ProcessId @@ -112,7 +110,6 @@ data Result a | Stop a String deriving (Show, Typeable) - ok :: (Serializable a, Show a) => a -> Server s (Result a) ok resp = return (Ok resp) @@ -122,15 +119,11 @@ forward sid = return (Forward sid) stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) stop resp reason = return (Stop resp reason) - - -- | Handlers type InitHandler s = Server s InitResult type TerminateHandler s = TerminateReason -> Server s () type Handler s a b = a -> Server s (Result b) - - -- | Adds routing metadata to the actual payload data Message a = CallMessage { msgFrom :: ProcessId, msgPayload :: a } @@ -138,8 +131,6 @@ data Message a = deriving (Show, Typeable) $(derive makeBinary ''Message) - - -- | Dispatcher that knows how to dispatch messages to a handler -- s The server state data MessageDispatcher s = @@ -164,15 +155,11 @@ instance MessageMatcher MessageDispatcher where matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) matchMessage s (MessageDispatcherAny d) = matchAny (d s) - - -- | Constructs a call message dispatcher -- handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s handle = handleIf (const True) - - handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s handleIf cond handler = MessageDispatcherIf { dispatcher = (\s msg -> case msg of @@ -250,8 +237,6 @@ startServer s ls = spawnLocal proc terminateH = terminateHandler ls hs = handlers ls - - -- | Spawn a process and link to it startServerLink :: s -> LocalServer s -> Process ServerId startServerLink s ls = do @@ -259,8 +244,6 @@ startServerLink s ls = do link pid return pid - - -- | Like 'spawnServerLink', but monitor the spawned process startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) startServerMonitor s ls = do @@ -268,13 +251,11 @@ startServerMonitor s ls = do ref <- monitor pid return (pid, ref) - - -- | Call a server identified by it's ServerId callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do cid <- getSelfPid - --say $ "Calling server " ++ show cid + say $ "Calling server " ++ show cid send sid (CallMessage cid rq) case timeout of Infinity -> expect @@ -319,19 +300,15 @@ processServer initH terminateH dispatchers s = do (ir, s') <- runServer initH s P.catch (proc ir s') (exitHandler s') where - proc ir s' = do (tr, s'') <- runServer (processLoop dispatchers ir) s' _ <- runServer (terminateH tr) s'' return () - exitHandler s' e = do let tr = TerminateReason $ show (e :: SomeException) _ <- runServer (terminateH tr) s' return () - - -- | server loop processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason processLoop dispatchers ir = do @@ -345,8 +322,6 @@ processLoop dispatchers ir = do Nothing -> loop ds t Just r -> return r - - -- | processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) processReceive ds timeout = do @@ -367,20 +342,14 @@ processReceive ds timeout = do --trace "Receive timed out ..." return $ Just (TerminateReason "Receive timed out") - - -- | Log a trace message using the underlying Process's say trace :: String -> Server s () trace msg = lift . say $ msg - - -- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a lift :: Process a -> Server s a lift p = Server $ ST.lift p - - -- | runServer :: Server s a -> s -> Process (a, s) runServer server state = ST.runStateT (unServer server) state From 69f27b604b20bd806845ba42f08ab05a6ac71881 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 21:03:31 -0500 Subject: [PATCH 0407/2357] Now server calls are monitored --- .ghci | 4 +- src/Control/Distributed/Platform/GenServer.hs | 47 +++++++---- tests/GenServer/Counter.hs | 19 +++-- tests/Main.hs | 83 +++++++++++++++++++ tests/TestGenServer.hs | 2 +- 5 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 tests/Main.hs diff --git a/.ghci b/.ghci index 5c030aa9..172bd3ff 100644 --- a/.ghci +++ b/.ghci @@ -1,4 +1,4 @@ -:set -isrc -isrc +:set -isrc -itests :def hoogle \x -> return $ ":!hoogle " ++ x @@ -6,4 +6,4 @@ :set -w -fwarn-unused-binds -fwarn-unused-imports -:load src/Main.hs \ No newline at end of file +:load tests/Main.hs \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index ad6fb5b2..3a49ad6c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -43,18 +43,19 @@ import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, MonitorRef, + Match, Process, ProcessId, - expect, expectTimeout, - monitor, link, + monitor, unmonitor, + link, finally, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal) + send, spawnLocal, + ProcessMonitorNotification(..)) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -179,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do + CastMessage cid payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -252,18 +253,34 @@ startServerMonitor s ls = do return (pid, ref) -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do - cid <- getSelfPid - --say $ "Calling server " ++ show cid - send sid (CallMessage cid rq) - case timeout of - Infinity -> expect - Timeout time -> do - mayResp <- expectTimeout (intervalToMs time) + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 case mayResp of - Just msg -> return msg - Nothing -> error $ "timeout! value = " ++ show time + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 9a3effbb..bb9095df 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -31,7 +31,7 @@ $(derive makeBinary ''CounterRequest) -- Call response(s) data CounterResponse - = CounterIncremented + = CounterIncremented Int | Count Int deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) @@ -54,8 +54,9 @@ startCounter count = startServer count defaultServer { --c <- getState --trace $ "Counter init: " ++ show c initOk Infinity, - terminateHandler = const (return ()), - --trace $ "Counter terminate: " ++ show r, + terminateHandler = \r -> + --const (return ()), + trace $ "Counter terminate: " ++ show r, handlers = [ handle handleCounter, handle handleReset @@ -71,10 +72,10 @@ stopCounter sid = stopServer sid () -- | Increment count -incCount :: ServerId -> Process () +incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented <- callServer sid Infinity IncrementCounter - return () + CounterIncremented c <- callServer sid Infinity IncrementCounter + return c @@ -97,11 +98,11 @@ resetCount sid = castServer sid ResetCount handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - count <- getState modifyState (+1) + count <- getState if count > 10 - then stop CounterIncremented "Stopping because 'Count > 10'" - else ok CounterIncremented + then stop (CounterIncremented count) "Stopping because 'Count > 10'" + else ok (CounterIncremented count) handleCounter GetCount = do count <- getState ok (Count count) diff --git a/tests/Main.hs b/tests/Main.hs new file mode 100644 index 00000000..0992bc3f --- /dev/null +++ b/tests/Main.hs @@ -0,0 +1,83 @@ +module Main where + +import Prelude hiding (catch) +import GenServer.Counter +import GenServer.Kitty + +import Control.Exception (SomeException) +import Control.Distributed.Static (initRemoteTable) +import Network.Transport.TCP (createTransport, + defaultTCPParameters) +import Control.Distributed.Process (Process, catch, say) +import Control.Distributed.Process.Node (newLocalNode, runProcess) +import System.IO + +host :: String +host = "::ffff:127.0.0.1" + + + +port :: String +port = "8000" + + + +main :: IO () +main = do + hSetBuffering stdout NoBuffering + putStrLn "Starting server ... " + t <- createTransport host port defaultTCPParameters + case t of + Left ex -> error $ show ex + Right transport -> do + putStrLn "Transport created." + localNode <- newLocalNode transport initRemoteTable + putStrLn "Local node created." + runProcess localNode $ (kittyTest 10) `catch` \e -> say $ show (e :: SomeException) + runProcess localNode $ counterTest `catch` \e -> say $ show (e :: SomeException) + --putStrLn "Server started!" + getChar + return () + + + +counterTest :: Process () +counterTest = do + say "-- Starting counter test ..." + cid <- startCounter 10 + c <- getCount cid + say $ "c = " ++ show c + incCount cid + incCount cid + c <- getCount cid + say $ "c = " ++ show c + resetCount cid + c2 <- getCount cid + say $ "c2 = " ++ show c2 + stopCounter cid + return () + + + +kittyTest :: Int -> Process () +kittyTest n = do + say "-- Starting kitty test ..." + kPid <- startKitty [Cat "c1" "black" "a black cat"] + say $ "-- Ordering " ++ show n ++ " cats ..." + kittyTransactions kPid n + say "-- Closing kitty shop ..." + closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid + closeShop kPid + return () + + + +kittyTransactions kPid 0 = return () +kittyTransactions kPid n = do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + kittyTransactions kPid (n - 1) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 8d4e2c7c..394a39d4 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -70,7 +70,7 @@ testPing transport = do localNode <- newLocalNode transport initRemoteTable forkIO $ runProcess localNode $ do - --say "Starting ..." + say "Starting ..." sid <- startServer (0 :: Int) defaultServer { initHandler = do --trace "Init ..." From f9d4b556e84bd6653ca323b40515600d394694ba Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 21:03:31 -0500 Subject: [PATCH 0408/2357] Now server calls are monitored --- .ghci | 4 +- src/Control/Distributed/Platform/GenServer.hs | 47 +++++++---- tests/GenServer/Counter.hs | 19 +++-- tests/Main.hs | 83 +++++++++++++++++++ tests/TestGenServer.hs | 2 +- 5 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 tests/Main.hs diff --git a/.ghci b/.ghci index 5c030aa9..172bd3ff 100644 --- a/.ghci +++ b/.ghci @@ -1,4 +1,4 @@ -:set -isrc -isrc +:set -isrc -itests :def hoogle \x -> return $ ":!hoogle " ++ x @@ -6,4 +6,4 @@ :set -w -fwarn-unused-binds -fwarn-unused-imports -:load src/Main.hs \ No newline at end of file +:load tests/Main.hs \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index ad6fb5b2..3a49ad6c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -43,18 +43,19 @@ import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, MonitorRef, + Match, Process, ProcessId, - expect, expectTimeout, - monitor, link, + monitor, unmonitor, + link, finally, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal) + send, spawnLocal, + ProcessMonitorNotification(..)) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -179,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do + CastMessage cid payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -252,18 +253,34 @@ startServerMonitor s ls = do return (pid, ref) -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do - cid <- getSelfPid - --say $ "Calling server " ++ show cid - send sid (CallMessage cid rq) - case timeout of - Infinity -> expect - Timeout time -> do - mayResp <- expectTimeout (intervalToMs time) + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 case mayResp of - Just msg -> return msg - Nothing -> error $ "timeout! value = " ++ show time + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 9a3effbb..bb9095df 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -31,7 +31,7 @@ $(derive makeBinary ''CounterRequest) -- Call response(s) data CounterResponse - = CounterIncremented + = CounterIncremented Int | Count Int deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) @@ -54,8 +54,9 @@ startCounter count = startServer count defaultServer { --c <- getState --trace $ "Counter init: " ++ show c initOk Infinity, - terminateHandler = const (return ()), - --trace $ "Counter terminate: " ++ show r, + terminateHandler = \r -> + --const (return ()), + trace $ "Counter terminate: " ++ show r, handlers = [ handle handleCounter, handle handleReset @@ -71,10 +72,10 @@ stopCounter sid = stopServer sid () -- | Increment count -incCount :: ServerId -> Process () +incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented <- callServer sid Infinity IncrementCounter - return () + CounterIncremented c <- callServer sid Infinity IncrementCounter + return c @@ -97,11 +98,11 @@ resetCount sid = castServer sid ResetCount handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - count <- getState modifyState (+1) + count <- getState if count > 10 - then stop CounterIncremented "Stopping because 'Count > 10'" - else ok CounterIncremented + then stop (CounterIncremented count) "Stopping because 'Count > 10'" + else ok (CounterIncremented count) handleCounter GetCount = do count <- getState ok (Count count) diff --git a/tests/Main.hs b/tests/Main.hs new file mode 100644 index 00000000..0992bc3f --- /dev/null +++ b/tests/Main.hs @@ -0,0 +1,83 @@ +module Main where + +import Prelude hiding (catch) +import GenServer.Counter +import GenServer.Kitty + +import Control.Exception (SomeException) +import Control.Distributed.Static (initRemoteTable) +import Network.Transport.TCP (createTransport, + defaultTCPParameters) +import Control.Distributed.Process (Process, catch, say) +import Control.Distributed.Process.Node (newLocalNode, runProcess) +import System.IO + +host :: String +host = "::ffff:127.0.0.1" + + + +port :: String +port = "8000" + + + +main :: IO () +main = do + hSetBuffering stdout NoBuffering + putStrLn "Starting server ... " + t <- createTransport host port defaultTCPParameters + case t of + Left ex -> error $ show ex + Right transport -> do + putStrLn "Transport created." + localNode <- newLocalNode transport initRemoteTable + putStrLn "Local node created." + runProcess localNode $ (kittyTest 10) `catch` \e -> say $ show (e :: SomeException) + runProcess localNode $ counterTest `catch` \e -> say $ show (e :: SomeException) + --putStrLn "Server started!" + getChar + return () + + + +counterTest :: Process () +counterTest = do + say "-- Starting counter test ..." + cid <- startCounter 10 + c <- getCount cid + say $ "c = " ++ show c + incCount cid + incCount cid + c <- getCount cid + say $ "c = " ++ show c + resetCount cid + c2 <- getCount cid + say $ "c2 = " ++ show c2 + stopCounter cid + return () + + + +kittyTest :: Int -> Process () +kittyTest n = do + say "-- Starting kitty test ..." + kPid <- startKitty [Cat "c1" "black" "a black cat"] + say $ "-- Ordering " ++ show n ++ " cats ..." + kittyTransactions kPid n + say "-- Closing kitty shop ..." + closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid + closeShop kPid + return () + + + +kittyTransactions kPid 0 = return () +kittyTransactions kPid n = do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + kittyTransactions kPid (n - 1) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 8d4e2c7c..394a39d4 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -70,7 +70,7 @@ testPing transport = do localNode <- newLocalNode transport initRemoteTable forkIO $ runProcess localNode $ do - --say "Starting ..." + say "Starting ..." sid <- startServer (0 :: Int) defaultServer { initHandler = do --trace "Init ..." From 9a51fc1d1822b54b43ce904f67af70fa75dbee75 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 21:03:31 -0500 Subject: [PATCH 0409/2357] Now server calls are monitored --- .ghci | 4 +- src/Control/Distributed/Platform/GenServer.hs | 47 +++++++---- tests/GenServer/Counter.hs | 19 +++-- tests/Main.hs | 83 +++++++++++++++++++ tests/TestGenServer.hs | 2 +- 5 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 tests/Main.hs diff --git a/.ghci b/.ghci index 5c030aa9..172bd3ff 100644 --- a/.ghci +++ b/.ghci @@ -1,4 +1,4 @@ -:set -isrc -isrc +:set -isrc -itests :def hoogle \x -> return $ ":!hoogle " ++ x @@ -6,4 +6,4 @@ :set -w -fwarn-unused-binds -fwarn-unused-imports -:load src/Main.hs \ No newline at end of file +:load tests/Main.hs \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index ad6fb5b2..3a49ad6c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -43,18 +43,19 @@ import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, MonitorRef, + Match, Process, ProcessId, - expect, expectTimeout, - monitor, link, + monitor, unmonitor, + link, finally, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal) + send, spawnLocal, + ProcessMonitorNotification(..)) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -179,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do + CastMessage cid payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -252,18 +253,34 @@ startServerMonitor s ls = do return (pid, ref) -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do - cid <- getSelfPid - --say $ "Calling server " ++ show cid - send sid (CallMessage cid rq) - case timeout of - Infinity -> expect - Timeout time -> do - mayResp <- expectTimeout (intervalToMs time) + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 case mayResp of - Just msg -> return msg - Nothing -> error $ "timeout! value = " ++ show time + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 9a3effbb..bb9095df 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -31,7 +31,7 @@ $(derive makeBinary ''CounterRequest) -- Call response(s) data CounterResponse - = CounterIncremented + = CounterIncremented Int | Count Int deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) @@ -54,8 +54,9 @@ startCounter count = startServer count defaultServer { --c <- getState --trace $ "Counter init: " ++ show c initOk Infinity, - terminateHandler = const (return ()), - --trace $ "Counter terminate: " ++ show r, + terminateHandler = \r -> + --const (return ()), + trace $ "Counter terminate: " ++ show r, handlers = [ handle handleCounter, handle handleReset @@ -71,10 +72,10 @@ stopCounter sid = stopServer sid () -- | Increment count -incCount :: ServerId -> Process () +incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented <- callServer sid Infinity IncrementCounter - return () + CounterIncremented c <- callServer sid Infinity IncrementCounter + return c @@ -97,11 +98,11 @@ resetCount sid = castServer sid ResetCount handleCounter :: Handler Int CounterRequest CounterResponse handleCounter IncrementCounter = do - count <- getState modifyState (+1) + count <- getState if count > 10 - then stop CounterIncremented "Stopping because 'Count > 10'" - else ok CounterIncremented + then stop (CounterIncremented count) "Stopping because 'Count > 10'" + else ok (CounterIncremented count) handleCounter GetCount = do count <- getState ok (Count count) diff --git a/tests/Main.hs b/tests/Main.hs new file mode 100644 index 00000000..0992bc3f --- /dev/null +++ b/tests/Main.hs @@ -0,0 +1,83 @@ +module Main where + +import Prelude hiding (catch) +import GenServer.Counter +import GenServer.Kitty + +import Control.Exception (SomeException) +import Control.Distributed.Static (initRemoteTable) +import Network.Transport.TCP (createTransport, + defaultTCPParameters) +import Control.Distributed.Process (Process, catch, say) +import Control.Distributed.Process.Node (newLocalNode, runProcess) +import System.IO + +host :: String +host = "::ffff:127.0.0.1" + + + +port :: String +port = "8000" + + + +main :: IO () +main = do + hSetBuffering stdout NoBuffering + putStrLn "Starting server ... " + t <- createTransport host port defaultTCPParameters + case t of + Left ex -> error $ show ex + Right transport -> do + putStrLn "Transport created." + localNode <- newLocalNode transport initRemoteTable + putStrLn "Local node created." + runProcess localNode $ (kittyTest 10) `catch` \e -> say $ show (e :: SomeException) + runProcess localNode $ counterTest `catch` \e -> say $ show (e :: SomeException) + --putStrLn "Server started!" + getChar + return () + + + +counterTest :: Process () +counterTest = do + say "-- Starting counter test ..." + cid <- startCounter 10 + c <- getCount cid + say $ "c = " ++ show c + incCount cid + incCount cid + c <- getCount cid + say $ "c = " ++ show c + resetCount cid + c2 <- getCount cid + say $ "c2 = " ++ show c2 + stopCounter cid + return () + + + +kittyTest :: Int -> Process () +kittyTest n = do + say "-- Starting kitty test ..." + kPid <- startKitty [Cat "c1" "black" "a black cat"] + say $ "-- Ordering " ++ show n ++ " cats ..." + kittyTransactions kPid n + say "-- Closing kitty shop ..." + closeShop kPid + say "-- Stopping kitty shop ..." + stopKitty kPid + closeShop kPid + return () + + + +kittyTransactions kPid 0 = return () +kittyTransactions kPid n = do + cat1 <- orderCat kPid "c1" "black" "a black cat" + cat2 <- orderCat kPid "c2" "black" "a black cat" + returnCat kPid cat1 + returnCat kPid cat2 + kittyTransactions kPid (n - 1) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 8d4e2c7c..394a39d4 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -70,7 +70,7 @@ testPing transport = do localNode <- newLocalNode transport initRemoteTable forkIO $ runProcess localNode $ do - --say "Starting ..." + say "Starting ..." sid <- startServer (0 :: Int) defaultServer { initHandler = do --trace "Init ..." From 7b10f599bad97b232cb963efacd15e1677ba7962 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 21:03:31 -0500 Subject: [PATCH 0410/2357] Now server calls are monitored --- .ghci | 4 +- src/Control/Distributed/Platform/GenServer.hs | 47 +++++++++++++------ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.ghci b/.ghci index 5c030aa9..172bd3ff 100644 --- a/.ghci +++ b/.ghci @@ -1,4 +1,4 @@ -:set -isrc -isrc +:set -isrc -itests :def hoogle \x -> return $ ":!hoogle " ++ x @@ -6,4 +6,4 @@ :set -w -fwarn-unused-binds -fwarn-unused-imports -:load src/Main.hs \ No newline at end of file +:load tests/Main.hs \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index ad6fb5b2..3a49ad6c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -43,18 +43,19 @@ import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, MonitorRef, + Match, Process, ProcessId, - expect, expectTimeout, - monitor, link, + monitor, unmonitor, + link, finally, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal) + send, spawnLocal, + ProcessMonitorNotification(..)) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -179,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do + CastMessage cid payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -252,18 +253,34 @@ startServerMonitor s ls = do return (pid, ref) -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Serializable rs) => ServerId -> Timeout -> rq -> Process rs +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs callServer sid timeout rq = do - cid <- getSelfPid - --say $ "Calling server " ++ show cid - send sid (CallMessage cid rq) - case timeout of - Infinity -> expect - Timeout time -> do - mayResp <- expectTimeout (intervalToMs time) + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 case mayResp of - Just msg -> return msg - Nothing -> error $ "timeout! value = " ++ show time + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId castServer :: (Serializable a) => ServerId -> a -> Process () From 1eb25c31b2d138ada27f19e9eebd6cd1b39baea7 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 22:53:20 -0500 Subject: [PATCH 0411/2357] added support for async calls --- src/Control/Distributed/Platform/GenServer.hs | 111 +++++++++++------- tests/GenServer/Counter.hs | 38 ++---- tests/GenServer/Kitty.hs | 42 ++----- tests/Main.hs | 4 +- tests/TestGenServer.hs | 16 +-- 5 files changed, 99 insertions(+), 112 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..e9c0a0dc 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,12 +28,16 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - startServer, - startServerLink, - startServerMonitor, - callServer, - castServer, - stopServer, + start, + startLink, + startMonitor, + terminate, + cast, + Async(), + call, + callAsync, + wait, + waitTimeout, Process, trace ) where @@ -180,7 +184,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -230,8 +234,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -startServer :: s -> LocalServer s -> Process ServerId -startServer s ls = spawnLocal proc +start :: s -> LocalServer s -> Process ServerId +start s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -239,59 +243,76 @@ startServer s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startServerLink :: s -> LocalServer s -> Process ServerId -startServerLink s ls = do - pid <- startServer s ls +startLink :: s -> LocalServer s -> Process ServerId +startLink s ls = do + pid <- start s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerMonitor s ls = do - pid <- startServer s ls +startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startMonitor s ls = do + pid <- start s ls ref <- monitor pid return (pid, ref) + +data Async a = Async MonitorRef + + +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +call sid timeout rq = do + a1 <- callAsync sid rq + waitTimeout a1 timeout + -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -callServer sid timeout rq = do +callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) +callAsync sid rq = do cid <- getSelfPid - ref <- monitor sid - finally (doCall cid) (unmonitor ref) + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + async sid + +async :: ProcessId -> Process (Async a) +async pid = do + ref <- monitor pid + return $ Async ref + +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity + +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a +waitTimeout (Async ref) timeout = do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> return resp + Nothing -> error "Response-receive timeout" where - doCall cid = do - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - case timeout of - Infinity -> do - receiveWait [matchDied, matchResponse] - Timeout t -> do - mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] - case mayResp of - Just resp -> return resp - Nothing -> error $ "timeout! value = " ++ show t - - matchResponse = match (\resp -> do - --say $ "Matched: " ++ show resp - return resp) - - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do - --say $ "Matched: " ++ show n - mayResp <- expectTimeout 0 - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason) + receive to = case to of + Infinity -> do + resp <- receiveWait matches + return $ Just resp + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match (\resp -> return resp), + match (\(ProcessMonitorNotification _ _ reason) -> do + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason)] + + -- | Cast a message to a server identified by it's ServerId -castServer :: (Serializable a) => ServerId -> a -> Process () -castServer sid msg = do +cast :: (Serializable a) => ServerId -> a -> Process () +cast sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: Serializable a => ServerId -> a -> Process () -stopServer sid reason = do +terminate :: Serializable a => ServerId -> a -> Process () +terminate sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index bb9095df..f6ae921e 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -2,8 +2,9 @@ {-# LANGUAGE TemplateHaskell #-} module GenServer.Counter( startCounter, - stopCounter, + terminateCounter, getCount, + getCountAsync, incCount, resetCount ) where @@ -19,7 +20,6 @@ import Data.Typeable (Typeable) -- Types -- -------------------------------------------------------------------------------- - -- Call request(s) data CounterRequest = IncrementCounter @@ -27,8 +27,6 @@ data CounterRequest deriving (Show, Typeable) $(derive makeBinary ''CounterRequest) - - -- Call response(s) data CounterResponse = CounterIncremented Int @@ -36,61 +34,52 @@ data CounterResponse deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) - - -- Cast message(s) data ResetCount = ResetCount deriving (Show, Typeable) $(derive makeBinary ''ResetCount) - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- -- | Start a counter server startCounter :: Int -> Process ServerId -startCounter count = startServer count defaultServer { +startCounter count = start count defaultServer { initHandler = do --c <- getState --trace $ "Counter init: " ++ show c initOk Infinity, - terminateHandler = \r -> - --const (return ()), - trace $ "Counter terminate: " ++ show r, + terminateHandler = const (return ()), + -- \r -> trace $ "Counter terminate: " ++ show r, handlers = [ handle handleCounter, handle handleReset ] } - - -- | Stop the counter server -stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid () - - +terminateCounter :: ServerId -> Process () +terminateCounter sid = terminate sid () -- | Increment count incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented c <- callServer sid Infinity IncrementCounter + CounterIncremented c <- call sid Infinity IncrementCounter return c - - -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid Infinity GetCount + Count c <- call sid Infinity GetCount return c - +-- | Get the current count asynchronously +getCountAsync :: ServerId -> Process (Async Int) +getCountAsync sid = callAsync sid GetCount -- | Reset the current count resetCount :: ServerId -> Process () -resetCount sid = castServer sid ResetCount - +resetCount sid = cast sid ResetCount -------------------------------------------------------------------------------- -- IMPL -- @@ -107,7 +96,6 @@ handleCounter GetCount = do count <- getState ok (Count count) - handleReset :: Handler Int ResetCount () handleReset ResetCount = do putState 0 diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index 36205a04..ce2184de 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -7,8 +7,9 @@ module GenServer.Kitty ( startKitty, - stopKitty, + terminateKitty, orderCat, + orderCatAsync, returnCat, closeShop, Cat(..) @@ -29,8 +30,6 @@ type Color = String type Description = String type Name = String - - data Cat = Cat { catName :: Name, catColor :: Color, @@ -38,37 +37,29 @@ data Cat = Cat { deriving (Show, Typeable) $( derive makeBinary ''Cat ) - - data CatCmd = OrderCat String String String | CloseShop deriving (Show, Typeable) $( derive makeBinary ''CatCmd ) - - data ReturnCat = ReturnCat Cat deriving (Show, Typeable) $( derive makeBinary ''ReturnCat ) - - data CatEv = CatOrdered Cat | ShopClosed deriving (Show, Typeable) $( derive makeBinary ''CatEv ) - - -- -- %% Client API -- start_link() -> spawn_link(fun init/0). -- | Start a counter server startKitty :: [Cat] -> Process ServerId -startKitty cats = startServer cats defaultServer { +startKitty cats = start cats defaultServer { initHandler = do --cs <- getState --trace $ "Kitty init: " ++ show cs @@ -80,44 +71,37 @@ startKitty cats = startServer cats defaultServer { handle handleReturn ]} - - -- | Stop the kitty server -stopKitty :: ServerId -> Process () -stopKitty sid = stopServer sid () - - +terminateKitty :: ServerId -> Process () +terminateKitty sid = terminate sid () -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do - result <- callServer sid Infinity (OrderCat name color descr) + result <- call sid Infinity (OrderCat name color descr) case result of CatOrdered c -> return c _ -> error $ "Unexpected result " ++ show result +-- | Async call +orderCatAsync :: ServerId -> Name -> Color -> Description -> Process (Async Cat) +orderCatAsync sid name color descr = callAsync sid (OrderCat name color descr) - --- %% async call +-- %% cast returnCat :: ServerId -> Cat -> Process () -returnCat sid cat = castServer sid (ReturnCat cat) - - +returnCat sid cat = cast sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () closeShop sid = do - result <- callServer sid Infinity CloseShop + result <- call sid Infinity CloseShop case result of ShopClosed -> return () _ -> error $ "Unexpected result " ++ show result - - -- -- %%% Server functions - handleKitty :: Handler [Cat] CatCmd CatEv handleKitty (OrderCat name color descr) = do cats <- getState @@ -134,8 +118,6 @@ handleKitty CloseShop = do putState [] ok ShopClosed - - handleReturn :: Handler [Cat] ReturnCat () handleReturn (ReturnCat cat) = do modifyState (cat :) diff --git a/tests/Main.hs b/tests/Main.hs index 0992bc3f..21e723b0 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -54,7 +54,7 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid + terminateCounter cid return () @@ -68,7 +68,7 @@ kittyTest n = do say "-- Closing kitty shop ..." closeShop kPid say "-- Stopping kitty shop ..." - stopKitty kPid + terminateKitty kPid closeShop kPid return () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 394a39d4..e64a1c3a 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -27,11 +27,7 @@ import Network.Transport.TCP , TransportInternals(socketBetween) , defaultTCPParameters ) -import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) +import Control.Distributed.Process (say, liftIO, exit) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable (Serializable) @@ -71,7 +67,7 @@ testPing transport = do forkIO $ runProcess localNode $ do say "Starting ..." - sid <- startServer (0 :: Int) defaultServer { + sid <- start (0 :: Int) defaultServer { initHandler = do --trace "Init ..." c <- getState @@ -104,9 +100,9 @@ testPing transport = do liftIO $ takeMVar initDone --replicateM_ 10 $ do - Pong <- callServer sid (Timeout (TimeInterval Seconds 10)) Ping + Pong <- call sid (Timeout (TimeInterval Seconds 10)) Ping liftIO $ takeMVar pingDone - castServer sid Pong + cast sid Pong liftIO $ takeMVar pongDone exit sid () @@ -131,7 +127,7 @@ testCounter transport = do c <- getCount cid resetCount cid c2 <- getCount cid - stopCounter cid + terminateCounter cid liftIO $ putMVar serverDone True return () @@ -155,7 +151,7 @@ testKitty transport = do returnCat kPid cat1 returnCat kPid cat2 closeShop kPid - stopKitty kPid + terminateKitty kPid liftIO $ putMVar serverDone True return () From f8cb52b11f77172189cbb3bbcec816b04bed5cef Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 22:53:20 -0500 Subject: [PATCH 0412/2357] added support for async calls --- src/Control/Distributed/Platform/GenServer.hs | 111 +++++++++++------- tests/GenServer/Counter.hs | 38 ++---- tests/GenServer/Kitty.hs | 42 ++----- tests/Main.hs | 4 +- tests/TestGenServer.hs | 16 +-- 5 files changed, 99 insertions(+), 112 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..e9c0a0dc 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,12 +28,16 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - startServer, - startServerLink, - startServerMonitor, - callServer, - castServer, - stopServer, + start, + startLink, + startMonitor, + terminate, + cast, + Async(), + call, + callAsync, + wait, + waitTimeout, Process, trace ) where @@ -180,7 +184,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -230,8 +234,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -startServer :: s -> LocalServer s -> Process ServerId -startServer s ls = spawnLocal proc +start :: s -> LocalServer s -> Process ServerId +start s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -239,59 +243,76 @@ startServer s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startServerLink :: s -> LocalServer s -> Process ServerId -startServerLink s ls = do - pid <- startServer s ls +startLink :: s -> LocalServer s -> Process ServerId +startLink s ls = do + pid <- start s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerMonitor s ls = do - pid <- startServer s ls +startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startMonitor s ls = do + pid <- start s ls ref <- monitor pid return (pid, ref) + +data Async a = Async MonitorRef + + +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +call sid timeout rq = do + a1 <- callAsync sid rq + waitTimeout a1 timeout + -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -callServer sid timeout rq = do +callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) +callAsync sid rq = do cid <- getSelfPid - ref <- monitor sid - finally (doCall cid) (unmonitor ref) + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + async sid + +async :: ProcessId -> Process (Async a) +async pid = do + ref <- monitor pid + return $ Async ref + +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity + +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a +waitTimeout (Async ref) timeout = do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> return resp + Nothing -> error "Response-receive timeout" where - doCall cid = do - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - case timeout of - Infinity -> do - receiveWait [matchDied, matchResponse] - Timeout t -> do - mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] - case mayResp of - Just resp -> return resp - Nothing -> error $ "timeout! value = " ++ show t - - matchResponse = match (\resp -> do - --say $ "Matched: " ++ show resp - return resp) - - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do - --say $ "Matched: " ++ show n - mayResp <- expectTimeout 0 - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason) + receive to = case to of + Infinity -> do + resp <- receiveWait matches + return $ Just resp + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match (\resp -> return resp), + match (\(ProcessMonitorNotification _ _ reason) -> do + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason)] + + -- | Cast a message to a server identified by it's ServerId -castServer :: (Serializable a) => ServerId -> a -> Process () -castServer sid msg = do +cast :: (Serializable a) => ServerId -> a -> Process () +cast sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: Serializable a => ServerId -> a -> Process () -stopServer sid reason = do +terminate :: Serializable a => ServerId -> a -> Process () +terminate sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index bb9095df..f6ae921e 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -2,8 +2,9 @@ {-# LANGUAGE TemplateHaskell #-} module GenServer.Counter( startCounter, - stopCounter, + terminateCounter, getCount, + getCountAsync, incCount, resetCount ) where @@ -19,7 +20,6 @@ import Data.Typeable (Typeable) -- Types -- -------------------------------------------------------------------------------- - -- Call request(s) data CounterRequest = IncrementCounter @@ -27,8 +27,6 @@ data CounterRequest deriving (Show, Typeable) $(derive makeBinary ''CounterRequest) - - -- Call response(s) data CounterResponse = CounterIncremented Int @@ -36,61 +34,52 @@ data CounterResponse deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) - - -- Cast message(s) data ResetCount = ResetCount deriving (Show, Typeable) $(derive makeBinary ''ResetCount) - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- -- | Start a counter server startCounter :: Int -> Process ServerId -startCounter count = startServer count defaultServer { +startCounter count = start count defaultServer { initHandler = do --c <- getState --trace $ "Counter init: " ++ show c initOk Infinity, - terminateHandler = \r -> - --const (return ()), - trace $ "Counter terminate: " ++ show r, + terminateHandler = const (return ()), + -- \r -> trace $ "Counter terminate: " ++ show r, handlers = [ handle handleCounter, handle handleReset ] } - - -- | Stop the counter server -stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid () - - +terminateCounter :: ServerId -> Process () +terminateCounter sid = terminate sid () -- | Increment count incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented c <- callServer sid Infinity IncrementCounter + CounterIncremented c <- call sid Infinity IncrementCounter return c - - -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid Infinity GetCount + Count c <- call sid Infinity GetCount return c - +-- | Get the current count asynchronously +getCountAsync :: ServerId -> Process (Async Int) +getCountAsync sid = callAsync sid GetCount -- | Reset the current count resetCount :: ServerId -> Process () -resetCount sid = castServer sid ResetCount - +resetCount sid = cast sid ResetCount -------------------------------------------------------------------------------- -- IMPL -- @@ -107,7 +96,6 @@ handleCounter GetCount = do count <- getState ok (Count count) - handleReset :: Handler Int ResetCount () handleReset ResetCount = do putState 0 diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index 36205a04..ce2184de 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -7,8 +7,9 @@ module GenServer.Kitty ( startKitty, - stopKitty, + terminateKitty, orderCat, + orderCatAsync, returnCat, closeShop, Cat(..) @@ -29,8 +30,6 @@ type Color = String type Description = String type Name = String - - data Cat = Cat { catName :: Name, catColor :: Color, @@ -38,37 +37,29 @@ data Cat = Cat { deriving (Show, Typeable) $( derive makeBinary ''Cat ) - - data CatCmd = OrderCat String String String | CloseShop deriving (Show, Typeable) $( derive makeBinary ''CatCmd ) - - data ReturnCat = ReturnCat Cat deriving (Show, Typeable) $( derive makeBinary ''ReturnCat ) - - data CatEv = CatOrdered Cat | ShopClosed deriving (Show, Typeable) $( derive makeBinary ''CatEv ) - - -- -- %% Client API -- start_link() -> spawn_link(fun init/0). -- | Start a counter server startKitty :: [Cat] -> Process ServerId -startKitty cats = startServer cats defaultServer { +startKitty cats = start cats defaultServer { initHandler = do --cs <- getState --trace $ "Kitty init: " ++ show cs @@ -80,44 +71,37 @@ startKitty cats = startServer cats defaultServer { handle handleReturn ]} - - -- | Stop the kitty server -stopKitty :: ServerId -> Process () -stopKitty sid = stopServer sid () - - +terminateKitty :: ServerId -> Process () +terminateKitty sid = terminate sid () -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do - result <- callServer sid Infinity (OrderCat name color descr) + result <- call sid Infinity (OrderCat name color descr) case result of CatOrdered c -> return c _ -> error $ "Unexpected result " ++ show result +-- | Async call +orderCatAsync :: ServerId -> Name -> Color -> Description -> Process (Async Cat) +orderCatAsync sid name color descr = callAsync sid (OrderCat name color descr) - --- %% async call +-- %% cast returnCat :: ServerId -> Cat -> Process () -returnCat sid cat = castServer sid (ReturnCat cat) - - +returnCat sid cat = cast sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () closeShop sid = do - result <- callServer sid Infinity CloseShop + result <- call sid Infinity CloseShop case result of ShopClosed -> return () _ -> error $ "Unexpected result " ++ show result - - -- -- %%% Server functions - handleKitty :: Handler [Cat] CatCmd CatEv handleKitty (OrderCat name color descr) = do cats <- getState @@ -134,8 +118,6 @@ handleKitty CloseShop = do putState [] ok ShopClosed - - handleReturn :: Handler [Cat] ReturnCat () handleReturn (ReturnCat cat) = do modifyState (cat :) diff --git a/tests/Main.hs b/tests/Main.hs index 0992bc3f..21e723b0 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -54,7 +54,7 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid + terminateCounter cid return () @@ -68,7 +68,7 @@ kittyTest n = do say "-- Closing kitty shop ..." closeShop kPid say "-- Stopping kitty shop ..." - stopKitty kPid + terminateKitty kPid closeShop kPid return () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 394a39d4..e64a1c3a 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -27,11 +27,7 @@ import Network.Transport.TCP , TransportInternals(socketBetween) , defaultTCPParameters ) -import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) +import Control.Distributed.Process (say, liftIO, exit) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable (Serializable) @@ -71,7 +67,7 @@ testPing transport = do forkIO $ runProcess localNode $ do say "Starting ..." - sid <- startServer (0 :: Int) defaultServer { + sid <- start (0 :: Int) defaultServer { initHandler = do --trace "Init ..." c <- getState @@ -104,9 +100,9 @@ testPing transport = do liftIO $ takeMVar initDone --replicateM_ 10 $ do - Pong <- callServer sid (Timeout (TimeInterval Seconds 10)) Ping + Pong <- call sid (Timeout (TimeInterval Seconds 10)) Ping liftIO $ takeMVar pingDone - castServer sid Pong + cast sid Pong liftIO $ takeMVar pongDone exit sid () @@ -131,7 +127,7 @@ testCounter transport = do c <- getCount cid resetCount cid c2 <- getCount cid - stopCounter cid + terminateCounter cid liftIO $ putMVar serverDone True return () @@ -155,7 +151,7 @@ testKitty transport = do returnCat kPid cat1 returnCat kPid cat2 closeShop kPid - stopKitty kPid + terminateKitty kPid liftIO $ putMVar serverDone True return () From 5a116f4e8b34ccf9e07e374ec43fd5c427e6960a Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 22:53:20 -0500 Subject: [PATCH 0413/2357] added support for async calls --- src/Control/Distributed/Platform/GenServer.hs | 111 +++++++++++------- tests/GenServer/Counter.hs | 38 ++---- tests/GenServer/Kitty.hs | 42 ++----- tests/Main.hs | 4 +- tests/TestGenServer.hs | 16 +-- 5 files changed, 99 insertions(+), 112 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..e9c0a0dc 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,12 +28,16 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - startServer, - startServerLink, - startServerMonitor, - callServer, - castServer, - stopServer, + start, + startLink, + startMonitor, + terminate, + cast, + Async(), + call, + callAsync, + wait, + waitTimeout, Process, trace ) where @@ -180,7 +184,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -230,8 +234,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -startServer :: s -> LocalServer s -> Process ServerId -startServer s ls = spawnLocal proc +start :: s -> LocalServer s -> Process ServerId +start s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -239,59 +243,76 @@ startServer s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startServerLink :: s -> LocalServer s -> Process ServerId -startServerLink s ls = do - pid <- startServer s ls +startLink :: s -> LocalServer s -> Process ServerId +startLink s ls = do + pid <- start s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerMonitor s ls = do - pid <- startServer s ls +startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startMonitor s ls = do + pid <- start s ls ref <- monitor pid return (pid, ref) + +data Async a = Async MonitorRef + + +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +call sid timeout rq = do + a1 <- callAsync sid rq + waitTimeout a1 timeout + -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -callServer sid timeout rq = do +callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) +callAsync sid rq = do cid <- getSelfPid - ref <- monitor sid - finally (doCall cid) (unmonitor ref) + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + async sid + +async :: ProcessId -> Process (Async a) +async pid = do + ref <- monitor pid + return $ Async ref + +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity + +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a +waitTimeout (Async ref) timeout = do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> return resp + Nothing -> error "Response-receive timeout" where - doCall cid = do - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - case timeout of - Infinity -> do - receiveWait [matchDied, matchResponse] - Timeout t -> do - mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] - case mayResp of - Just resp -> return resp - Nothing -> error $ "timeout! value = " ++ show t - - matchResponse = match (\resp -> do - --say $ "Matched: " ++ show resp - return resp) - - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do - --say $ "Matched: " ++ show n - mayResp <- expectTimeout 0 - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason) + receive to = case to of + Infinity -> do + resp <- receiveWait matches + return $ Just resp + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match (\resp -> return resp), + match (\(ProcessMonitorNotification _ _ reason) -> do + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason)] + + -- | Cast a message to a server identified by it's ServerId -castServer :: (Serializable a) => ServerId -> a -> Process () -castServer sid msg = do +cast :: (Serializable a) => ServerId -> a -> Process () +cast sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: Serializable a => ServerId -> a -> Process () -stopServer sid reason = do +terminate :: Serializable a => ServerId -> a -> Process () +terminate sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index bb9095df..f6ae921e 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -2,8 +2,9 @@ {-# LANGUAGE TemplateHaskell #-} module GenServer.Counter( startCounter, - stopCounter, + terminateCounter, getCount, + getCountAsync, incCount, resetCount ) where @@ -19,7 +20,6 @@ import Data.Typeable (Typeable) -- Types -- -------------------------------------------------------------------------------- - -- Call request(s) data CounterRequest = IncrementCounter @@ -27,8 +27,6 @@ data CounterRequest deriving (Show, Typeable) $(derive makeBinary ''CounterRequest) - - -- Call response(s) data CounterResponse = CounterIncremented Int @@ -36,61 +34,52 @@ data CounterResponse deriving (Show, Typeable) $(derive makeBinary ''CounterResponse) - - -- Cast message(s) data ResetCount = ResetCount deriving (Show, Typeable) $(derive makeBinary ''ResetCount) - -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- -- | Start a counter server startCounter :: Int -> Process ServerId -startCounter count = startServer count defaultServer { +startCounter count = start count defaultServer { initHandler = do --c <- getState --trace $ "Counter init: " ++ show c initOk Infinity, - terminateHandler = \r -> - --const (return ()), - trace $ "Counter terminate: " ++ show r, + terminateHandler = const (return ()), + -- \r -> trace $ "Counter terminate: " ++ show r, handlers = [ handle handleCounter, handle handleReset ] } - - -- | Stop the counter server -stopCounter :: ServerId -> Process () -stopCounter sid = stopServer sid () - - +terminateCounter :: ServerId -> Process () +terminateCounter sid = terminate sid () -- | Increment count incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented c <- callServer sid Infinity IncrementCounter + CounterIncremented c <- call sid Infinity IncrementCounter return c - - -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- callServer sid Infinity GetCount + Count c <- call sid Infinity GetCount return c - +-- | Get the current count asynchronously +getCountAsync :: ServerId -> Process (Async Int) +getCountAsync sid = callAsync sid GetCount -- | Reset the current count resetCount :: ServerId -> Process () -resetCount sid = castServer sid ResetCount - +resetCount sid = cast sid ResetCount -------------------------------------------------------------------------------- -- IMPL -- @@ -107,7 +96,6 @@ handleCounter GetCount = do count <- getState ok (Count count) - handleReset :: Handler Int ResetCount () handleReset ResetCount = do putState 0 diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index 36205a04..ce2184de 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -7,8 +7,9 @@ module GenServer.Kitty ( startKitty, - stopKitty, + terminateKitty, orderCat, + orderCatAsync, returnCat, closeShop, Cat(..) @@ -29,8 +30,6 @@ type Color = String type Description = String type Name = String - - data Cat = Cat { catName :: Name, catColor :: Color, @@ -38,37 +37,29 @@ data Cat = Cat { deriving (Show, Typeable) $( derive makeBinary ''Cat ) - - data CatCmd = OrderCat String String String | CloseShop deriving (Show, Typeable) $( derive makeBinary ''CatCmd ) - - data ReturnCat = ReturnCat Cat deriving (Show, Typeable) $( derive makeBinary ''ReturnCat ) - - data CatEv = CatOrdered Cat | ShopClosed deriving (Show, Typeable) $( derive makeBinary ''CatEv ) - - -- -- %% Client API -- start_link() -> spawn_link(fun init/0). -- | Start a counter server startKitty :: [Cat] -> Process ServerId -startKitty cats = startServer cats defaultServer { +startKitty cats = start cats defaultServer { initHandler = do --cs <- getState --trace $ "Kitty init: " ++ show cs @@ -80,44 +71,37 @@ startKitty cats = startServer cats defaultServer { handle handleReturn ]} - - -- | Stop the kitty server -stopKitty :: ServerId -> Process () -stopKitty sid = stopServer sid () - - +terminateKitty :: ServerId -> Process () +terminateKitty sid = terminate sid () -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat orderCat sid name color descr = do - result <- callServer sid Infinity (OrderCat name color descr) + result <- call sid Infinity (OrderCat name color descr) case result of CatOrdered c -> return c _ -> error $ "Unexpected result " ++ show result +-- | Async call +orderCatAsync :: ServerId -> Name -> Color -> Description -> Process (Async Cat) +orderCatAsync sid name color descr = callAsync sid (OrderCat name color descr) - --- %% async call +-- %% cast returnCat :: ServerId -> Cat -> Process () -returnCat sid cat = castServer sid (ReturnCat cat) - - +returnCat sid cat = cast sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () closeShop sid = do - result <- callServer sid Infinity CloseShop + result <- call sid Infinity CloseShop case result of ShopClosed -> return () _ -> error $ "Unexpected result " ++ show result - - -- -- %%% Server functions - handleKitty :: Handler [Cat] CatCmd CatEv handleKitty (OrderCat name color descr) = do cats <- getState @@ -134,8 +118,6 @@ handleKitty CloseShop = do putState [] ok ShopClosed - - handleReturn :: Handler [Cat] ReturnCat () handleReturn (ReturnCat cat) = do modifyState (cat :) diff --git a/tests/Main.hs b/tests/Main.hs index 0992bc3f..21e723b0 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -54,7 +54,7 @@ counterTest = do resetCount cid c2 <- getCount cid say $ "c2 = " ++ show c2 - stopCounter cid + terminateCounter cid return () @@ -68,7 +68,7 @@ kittyTest n = do say "-- Closing kitty shop ..." closeShop kPid say "-- Stopping kitty shop ..." - stopKitty kPid + terminateKitty kPid closeShop kPid return () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 394a39d4..e64a1c3a 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -27,11 +27,7 @@ import Network.Transport.TCP , TransportInternals(socketBetween) , defaultTCPParameters ) -import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) +import Control.Distributed.Process (say, liftIO, exit) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable (Serializable) @@ -71,7 +67,7 @@ testPing transport = do forkIO $ runProcess localNode $ do say "Starting ..." - sid <- startServer (0 :: Int) defaultServer { + sid <- start (0 :: Int) defaultServer { initHandler = do --trace "Init ..." c <- getState @@ -104,9 +100,9 @@ testPing transport = do liftIO $ takeMVar initDone --replicateM_ 10 $ do - Pong <- callServer sid (Timeout (TimeInterval Seconds 10)) Ping + Pong <- call sid (Timeout (TimeInterval Seconds 10)) Ping liftIO $ takeMVar pingDone - castServer sid Pong + cast sid Pong liftIO $ takeMVar pongDone exit sid () @@ -131,7 +127,7 @@ testCounter transport = do c <- getCount cid resetCount cid c2 <- getCount cid - stopCounter cid + terminateCounter cid liftIO $ putMVar serverDone True return () @@ -155,7 +151,7 @@ testKitty transport = do returnCat kPid cat1 returnCat kPid cat2 closeShop kPid - stopKitty kPid + terminateKitty kPid liftIO $ putMVar serverDone True return () From d60cf5eb855103dc3f6719196d059e79c9f68654 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Sun, 9 Dec 2012 22:53:20 -0500 Subject: [PATCH 0414/2357] added support for async calls --- src/Control/Distributed/Platform/GenServer.hs | 111 +++++++++++------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..e9c0a0dc 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,12 +28,16 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - startServer, - startServerLink, - startServerMonitor, - callServer, - castServer, - stopServer, + start, + startLink, + startMonitor, + terminate, + cast, + Async(), + call, + callAsync, + wait, + waitTimeout, Process, trace ) where @@ -180,7 +184,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -230,8 +234,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -startServer :: s -> LocalServer s -> Process ServerId -startServer s ls = spawnLocal proc +start :: s -> LocalServer s -> Process ServerId +start s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -239,59 +243,76 @@ startServer s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startServerLink :: s -> LocalServer s -> Process ServerId -startServerLink s ls = do - pid <- startServer s ls +startLink :: s -> LocalServer s -> Process ServerId +startLink s ls = do + pid <- start s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startServerMonitor s ls = do - pid <- startServer s ls +startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startMonitor s ls = do + pid <- start s ls ref <- monitor pid return (pid, ref) + +data Async a = Async MonitorRef + + +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +call sid timeout rq = do + a1 <- callAsync sid rq + waitTimeout a1 timeout + -- | Call a server identified by it's ServerId -callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -callServer sid timeout rq = do +callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) +callAsync sid rq = do cid <- getSelfPid - ref <- monitor sid - finally (doCall cid) (unmonitor ref) + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + async sid + +async :: ProcessId -> Process (Async a) +async pid = do + ref <- monitor pid + return $ Async ref + +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity + +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a +waitTimeout (Async ref) timeout = do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> return resp + Nothing -> error "Response-receive timeout" where - doCall cid = do - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - case timeout of - Infinity -> do - receiveWait [matchDied, matchResponse] - Timeout t -> do - mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] - case mayResp of - Just resp -> return resp - Nothing -> error $ "timeout! value = " ++ show t - - matchResponse = match (\resp -> do - --say $ "Matched: " ++ show resp - return resp) - - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do - --say $ "Matched: " ++ show n - mayResp <- expectTimeout 0 - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason) + receive to = case to of + Infinity -> do + resp <- receiveWait matches + return $ Just resp + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match (\resp -> return resp), + match (\(ProcessMonitorNotification _ _ reason) -> do + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason)] + + -- | Cast a message to a server identified by it's ServerId -castServer :: (Serializable a) => ServerId -> a -> Process () -castServer sid msg = do +cast :: (Serializable a) => ServerId -> a -> Process () +cast sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -stopServer :: Serializable a => ServerId -> a -> Process () -stopServer sid reason = do +terminate :: Serializable a => ServerId -> a -> Process () +terminate sid reason = do --say $ "Stop server " ++ show sid exit sid reason From 1d69dbb10871475ddbe4be2c69bee34d170df89e Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 00:23:20 -0500 Subject: [PATCH 0415/2357] Now caching the response in an MVar --- src/Control/Distributed/Platform/GenServer.hs | 48 ++++++++++--------- tests/GenServer/Counter.hs | 5 +- tests/GenServer/Kitty.hs | 5 +- tests/Main.hs | 7 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index e9c0a0dc..c545e510 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,13 +41,13 @@ module Control.Distributed.Platform.GenServer ( Process, trace ) where - +import Control.Concurrent.MVar import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, + Match, liftIO, Process, ProcessId, expectTimeout, @@ -256,52 +256,56 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) +-- | Async data type +data Async a = Async MonitorRef (MVar a) -data Async a = Async MonitorRef - - +-- | Sync call to a server call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs call sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout --- | Call a server identified by it's ServerId +-- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) callAsync sid rq = do cid <- getSelfPid + ref <- monitor sid --say $ "Calling server " ++ show cid ++ " - " ++ show rq send sid (CallMessage cid rq) - async sid - -async :: ProcessId -> Process (Async a) -async pid = do - ref <- monitor pid - return $ Async ref + respMVar <- liftIO newEmptyMVar + return $ Async ref respMVar +-- | Wait for the call response wait :: (Serializable a, Show a) => Async a -> Process a wait a = waitTimeout a Infinity +-- | Wait for the call response given a timeout waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref) timeout = do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> return resp - Nothing -> error "Response-receive timeout" - where +waitTimeout (Async ref respMVar) timeout = + let receive to = case to of Infinity -> do resp <- receiveWait matches return $ Just resp Timeout t -> receiveTimeout (intervalToMs t) matches matches = [ - match (\resp -> return resp), + match return, match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- expectTimeout 0 + mayResp <- receiveTimeout 0 [match return] case mayResp of Just resp -> return resp Nothing -> error $ "Server died: " ++ show reason)] - - + in do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Just resp -> return resp + Nothing -> do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> do + liftIO $ putMVar respMVar resp + return resp + Nothing -> error "Response-receive timeout" -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index f6ae921e..8d08b538 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -6,7 +6,10 @@ module GenServer.Counter( getCount, getCountAsync, incCount, - resetCount + resetCount, + wait, + waitTimeout, + Timeout(..) ) where import Control.Distributed.Platform.GenServer diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index ce2184de..bd0ab61b 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -12,7 +12,10 @@ module GenServer.Kitty orderCatAsync, returnCat, closeShop, - Cat(..) + Cat(..), + wait, + waitTimeout, + Timeout(..) ) where import Control.Distributed.Platform.GenServer diff --git a/tests/Main.hs b/tests/Main.hs index 21e723b0..9134a6ed 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -3,7 +3,6 @@ module Main where import Prelude hiding (catch) import GenServer.Counter import GenServer.Kitty - import Control.Exception (SomeException) import Control.Distributed.Static (initRemoteTable) import Network.Transport.TCP (createTransport, @@ -76,8 +75,12 @@ kittyTest n = do kittyTransactions kPid 0 = return () kittyTransactions kPid n = do + say "ca1" cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" + say "a2" + a2 <- orderCatAsync kPid "c2" "black" "a black cat" + say "cat2" + cat2 <- waitTimeout a2 Infinity returnCat kPid cat1 returnCat kPid cat2 kittyTransactions kPid (n - 1) From 387a9a1b6190c7784e7f241638ca12940d2ce52a Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 00:23:20 -0500 Subject: [PATCH 0416/2357] Now caching the response in an MVar --- src/Control/Distributed/Platform/GenServer.hs | 48 ++++++++++--------- tests/GenServer/Counter.hs | 5 +- tests/GenServer/Kitty.hs | 5 +- tests/Main.hs | 7 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index e9c0a0dc..c545e510 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,13 +41,13 @@ module Control.Distributed.Platform.GenServer ( Process, trace ) where - +import Control.Concurrent.MVar import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, + Match, liftIO, Process, ProcessId, expectTimeout, @@ -256,52 +256,56 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) +-- | Async data type +data Async a = Async MonitorRef (MVar a) -data Async a = Async MonitorRef - - +-- | Sync call to a server call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs call sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout --- | Call a server identified by it's ServerId +-- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) callAsync sid rq = do cid <- getSelfPid + ref <- monitor sid --say $ "Calling server " ++ show cid ++ " - " ++ show rq send sid (CallMessage cid rq) - async sid - -async :: ProcessId -> Process (Async a) -async pid = do - ref <- monitor pid - return $ Async ref + respMVar <- liftIO newEmptyMVar + return $ Async ref respMVar +-- | Wait for the call response wait :: (Serializable a, Show a) => Async a -> Process a wait a = waitTimeout a Infinity +-- | Wait for the call response given a timeout waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref) timeout = do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> return resp - Nothing -> error "Response-receive timeout" - where +waitTimeout (Async ref respMVar) timeout = + let receive to = case to of Infinity -> do resp <- receiveWait matches return $ Just resp Timeout t -> receiveTimeout (intervalToMs t) matches matches = [ - match (\resp -> return resp), + match return, match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- expectTimeout 0 + mayResp <- receiveTimeout 0 [match return] case mayResp of Just resp -> return resp Nothing -> error $ "Server died: " ++ show reason)] - - + in do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Just resp -> return resp + Nothing -> do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> do + liftIO $ putMVar respMVar resp + return resp + Nothing -> error "Response-receive timeout" -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index f6ae921e..8d08b538 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -6,7 +6,10 @@ module GenServer.Counter( getCount, getCountAsync, incCount, - resetCount + resetCount, + wait, + waitTimeout, + Timeout(..) ) where import Control.Distributed.Platform.GenServer diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index ce2184de..bd0ab61b 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -12,7 +12,10 @@ module GenServer.Kitty orderCatAsync, returnCat, closeShop, - Cat(..) + Cat(..), + wait, + waitTimeout, + Timeout(..) ) where import Control.Distributed.Platform.GenServer diff --git a/tests/Main.hs b/tests/Main.hs index 21e723b0..9134a6ed 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -3,7 +3,6 @@ module Main where import Prelude hiding (catch) import GenServer.Counter import GenServer.Kitty - import Control.Exception (SomeException) import Control.Distributed.Static (initRemoteTable) import Network.Transport.TCP (createTransport, @@ -76,8 +75,12 @@ kittyTest n = do kittyTransactions kPid 0 = return () kittyTransactions kPid n = do + say "ca1" cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" + say "a2" + a2 <- orderCatAsync kPid "c2" "black" "a black cat" + say "cat2" + cat2 <- waitTimeout a2 Infinity returnCat kPid cat1 returnCat kPid cat2 kittyTransactions kPid (n - 1) From d06b1b3453b4854d6f3cc56c67bf1961ffed604b Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 00:23:20 -0500 Subject: [PATCH 0417/2357] Now caching the response in an MVar --- src/Control/Distributed/Platform/GenServer.hs | 48 ++++++++++--------- tests/GenServer/Counter.hs | 5 +- tests/GenServer/Kitty.hs | 5 +- tests/Main.hs | 7 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index e9c0a0dc..c545e510 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,13 +41,13 @@ module Control.Distributed.Platform.GenServer ( Process, trace ) where - +import Control.Concurrent.MVar import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, + Match, liftIO, Process, ProcessId, expectTimeout, @@ -256,52 +256,56 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) +-- | Async data type +data Async a = Async MonitorRef (MVar a) -data Async a = Async MonitorRef - - +-- | Sync call to a server call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs call sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout --- | Call a server identified by it's ServerId +-- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) callAsync sid rq = do cid <- getSelfPid + ref <- monitor sid --say $ "Calling server " ++ show cid ++ " - " ++ show rq send sid (CallMessage cid rq) - async sid - -async :: ProcessId -> Process (Async a) -async pid = do - ref <- monitor pid - return $ Async ref + respMVar <- liftIO newEmptyMVar + return $ Async ref respMVar +-- | Wait for the call response wait :: (Serializable a, Show a) => Async a -> Process a wait a = waitTimeout a Infinity +-- | Wait for the call response given a timeout waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref) timeout = do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> return resp - Nothing -> error "Response-receive timeout" - where +waitTimeout (Async ref respMVar) timeout = + let receive to = case to of Infinity -> do resp <- receiveWait matches return $ Just resp Timeout t -> receiveTimeout (intervalToMs t) matches matches = [ - match (\resp -> return resp), + match return, match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- expectTimeout 0 + mayResp <- receiveTimeout 0 [match return] case mayResp of Just resp -> return resp Nothing -> error $ "Server died: " ++ show reason)] - - + in do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Just resp -> return resp + Nothing -> do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> do + liftIO $ putMVar respMVar resp + return resp + Nothing -> error "Response-receive timeout" -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index f6ae921e..8d08b538 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -6,7 +6,10 @@ module GenServer.Counter( getCount, getCountAsync, incCount, - resetCount + resetCount, + wait, + waitTimeout, + Timeout(..) ) where import Control.Distributed.Platform.GenServer diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index ce2184de..bd0ab61b 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -12,7 +12,10 @@ module GenServer.Kitty orderCatAsync, returnCat, closeShop, - Cat(..) + Cat(..), + wait, + waitTimeout, + Timeout(..) ) where import Control.Distributed.Platform.GenServer diff --git a/tests/Main.hs b/tests/Main.hs index 21e723b0..9134a6ed 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -3,7 +3,6 @@ module Main where import Prelude hiding (catch) import GenServer.Counter import GenServer.Kitty - import Control.Exception (SomeException) import Control.Distributed.Static (initRemoteTable) import Network.Transport.TCP (createTransport, @@ -76,8 +75,12 @@ kittyTest n = do kittyTransactions kPid 0 = return () kittyTransactions kPid n = do + say "ca1" cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" + say "a2" + a2 <- orderCatAsync kPid "c2" "black" "a black cat" + say "cat2" + cat2 <- waitTimeout a2 Infinity returnCat kPid cat1 returnCat kPid cat2 kittyTransactions kPid (n - 1) From 77c6bb83db069d1832e3d928301ea4102c3e11ba Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 00:23:20 -0500 Subject: [PATCH 0418/2357] Now caching the response in an MVar --- src/Control/Distributed/Platform/GenServer.hs | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index e9c0a0dc..c545e510 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -41,13 +41,13 @@ module Control.Distributed.Platform.GenServer ( Process, trace ) where - +import Control.Concurrent.MVar import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, + Match, liftIO, Process, ProcessId, expectTimeout, @@ -256,52 +256,56 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) +-- | Async data type +data Async a = Async MonitorRef (MVar a) -data Async a = Async MonitorRef - - +-- | Sync call to a server call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs call sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout --- | Call a server identified by it's ServerId +-- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) callAsync sid rq = do cid <- getSelfPid + ref <- monitor sid --say $ "Calling server " ++ show cid ++ " - " ++ show rq send sid (CallMessage cid rq) - async sid - -async :: ProcessId -> Process (Async a) -async pid = do - ref <- monitor pid - return $ Async ref + respMVar <- liftIO newEmptyMVar + return $ Async ref respMVar +-- | Wait for the call response wait :: (Serializable a, Show a) => Async a -> Process a wait a = waitTimeout a Infinity +-- | Wait for the call response given a timeout waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref) timeout = do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> return resp - Nothing -> error "Response-receive timeout" - where +waitTimeout (Async ref respMVar) timeout = + let receive to = case to of Infinity -> do resp <- receiveWait matches return $ Just resp Timeout t -> receiveTimeout (intervalToMs t) matches matches = [ - match (\resp -> return resp), + match return, match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- expectTimeout 0 + mayResp <- receiveTimeout 0 [match return] case mayResp of Just resp -> return resp Nothing -> error $ "Server died: " ++ show reason)] - - + in do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Just resp -> return resp + Nothing -> do + respM <- finally (receive timeout) (unmonitor ref) + case respM of + Just resp -> do + liftIO $ putMVar respMVar resp + return resp + Nothing -> error "Response-receive timeout" -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () From 664225686cb777bf49d354b9aa1aa45817dbddd1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 10:25:10 +0000 Subject: [PATCH 0419/2357] back out IO buffering changes - this screws up test output completely --- tests/TestMain.hs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 3128e733..c6f75c46 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,7 +6,6 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import System.IO (hSetBuffering, BufferMode(..), stdin, stdout, stderr) import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP @@ -23,10 +22,6 @@ tests transport internals = do main :: IO () main = do - hSetBuffering stdin NoBuffering - hSetBuffering stdout NoBuffering - hSetBuffering stderr NoBuffering - Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters testData <- tests transport internals From 38f6b8cd5ecc925e743f1ac80c947c1be73fe841 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 10:25:10 +0000 Subject: [PATCH 0420/2357] back out IO buffering changes - this screws up test output completely --- tests/TestMain.hs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 3128e733..c6f75c46 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,7 +6,6 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import System.IO (hSetBuffering, BufferMode(..), stdin, stdout, stderr) import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP @@ -23,10 +22,6 @@ tests transport internals = do main :: IO () main = do - hSetBuffering stdin NoBuffering - hSetBuffering stdout NoBuffering - hSetBuffering stderr NoBuffering - Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters testData <- tests transport internals From 9236fab4e8a586cd35f87d0b1c44c2674b5e28aa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 10:25:10 +0000 Subject: [PATCH 0421/2357] back out IO buffering changes - this screws up test output completely --- tests/TestMain.hs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 3128e733..c6f75c46 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,7 +6,6 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import System.IO (hSetBuffering, BufferMode(..), stdin, stdout, stderr) import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP @@ -23,10 +22,6 @@ tests transport internals = do main :: IO () main = do - hSetBuffering stdin NoBuffering - hSetBuffering stdout NoBuffering - hSetBuffering stderr NoBuffering - Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "8080" defaultTCPParameters testData <- tests transport internals From 12a36a1825437be274f3fcd1615016133a067edb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 11:01:37 +0000 Subject: [PATCH 0422/2357] silence those pesky compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 70 +++++++++---------- tests/TestGenProcess.hs | 5 -- tests/TestGenServer.hs | 40 ++++------- 3 files changed, 47 insertions(+), 68 deletions(-) delete mode 100644 tests/TestGenProcess.hs diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..4ab7476b 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -38,39 +38,39 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) -import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, - exit, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) +import qualified Control.Distributed.Process as P (forward, catch) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) + +import Control.Distributed.Process (AbstractMessage, + Match, + Process, + ProcessId, + expectTimeout, + monitor, unmonitor, + link, finally, + exit, getSelfPid, match, + matchAny, matchIf, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + ProcessMonitorNotification(..)) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer + +import Data.Binary (Binary (..), getWord8, putWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- @@ -180,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -275,7 +275,7 @@ callServer sid timeout rq = do --say $ "Matched: " ++ show resp return resp) - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do --say $ "Matched: " ++ show n mayResp <- expectTimeout 0 case mayResp of diff --git a/tests/TestGenProcess.hs b/tests/TestGenProcess.hs deleted file mode 100644 index 7a570a72..00000000 --- a/tests/TestGenProcess.hs +++ /dev/null @@ -1,5 +0,0 @@ -module TestGenProcess where - -import Test.HUnit - -test1 = TestCase (assertEqual "1 == 2" 1 2) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 394a39d4..cfb5a4c9 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -3,40 +3,24 @@ {-# LANGUAGE TemplateHaskell #-} module TestGenServer where -import System.IO (hPutStrLn, stderr) -import Data.Binary (Binary (..), getWord8, - putWord8) +import Data.Binary (Binary (..)) import Data.Typeable (Typeable) import Data.DeriveTH -import Data.Foldable (forM_) -import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent (forkIO) import Control.Concurrent.MVar - ( MVar - , newEmptyMVar + ( newEmptyMVar , putMVar , takeMVar - , readMVar - ) -import Control.Monad (replicateM_, replicateM, forever) -import Control.Exception (SomeException, throwIO) -import qualified Control.Exception as Ex (catch) -import Control.Applicative ((<$>), (<*>), pure, (<|>)) -import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP - ( createTransportExposeInternals - , TransportInternals(socketBetween) - , defaultTCPParameters ) +import qualified Network.Transport as NT (Transport) +import Network.Transport.TCP (TransportInternals) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) +import Control.Distributed.Process.Internal.Types() import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Serializable() import Test.HUnit (Assertion) -import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import Control.Distributed.Platform.GenServer @@ -77,7 +61,7 @@ testPing transport = do c <- getState liftIO $ putMVar initDone c initOk Infinity, - terminateHandler = \reason -> do + terminateHandler = \_ -> do --trace "Terminate ..." c <- getState liftIO $ putMVar terminateDone c @@ -125,12 +109,12 @@ testCounter transport = do runProcess localNode $ do cid <- startCounter 0 - c <- getCount cid + _ <- getCount cid incCount cid incCount cid - c <- getCount cid + _ <- getCount cid resetCount cid - c2 <- getCount cid + _ <- getCount cid stopCounter cid liftIO $ putMVar serverDone True return () From 9b3dcc2464a14b5572522c6d20fde21af41435b8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 11:01:37 +0000 Subject: [PATCH 0423/2357] silence those pesky compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 70 +++++++++---------- tests/TestGenProcess.hs | 5 -- tests/TestGenServer.hs | 40 ++++------- 3 files changed, 47 insertions(+), 68 deletions(-) delete mode 100644 tests/TestGenProcess.hs diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..4ab7476b 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -38,39 +38,39 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) -import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, - exit, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) +import qualified Control.Distributed.Process as P (forward, catch) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) + +import Control.Distributed.Process (AbstractMessage, + Match, + Process, + ProcessId, + expectTimeout, + monitor, unmonitor, + link, finally, + exit, getSelfPid, match, + matchAny, matchIf, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + ProcessMonitorNotification(..)) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer + +import Data.Binary (Binary (..), getWord8, putWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- @@ -180,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -275,7 +275,7 @@ callServer sid timeout rq = do --say $ "Matched: " ++ show resp return resp) - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do --say $ "Matched: " ++ show n mayResp <- expectTimeout 0 case mayResp of diff --git a/tests/TestGenProcess.hs b/tests/TestGenProcess.hs deleted file mode 100644 index 7a570a72..00000000 --- a/tests/TestGenProcess.hs +++ /dev/null @@ -1,5 +0,0 @@ -module TestGenProcess where - -import Test.HUnit - -test1 = TestCase (assertEqual "1 == 2" 1 2) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 394a39d4..cfb5a4c9 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -3,40 +3,24 @@ {-# LANGUAGE TemplateHaskell #-} module TestGenServer where -import System.IO (hPutStrLn, stderr) -import Data.Binary (Binary (..), getWord8, - putWord8) +import Data.Binary (Binary (..)) import Data.Typeable (Typeable) import Data.DeriveTH -import Data.Foldable (forM_) -import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent (forkIO) import Control.Concurrent.MVar - ( MVar - , newEmptyMVar + ( newEmptyMVar , putMVar , takeMVar - , readMVar - ) -import Control.Monad (replicateM_, replicateM, forever) -import Control.Exception (SomeException, throwIO) -import qualified Control.Exception as Ex (catch) -import Control.Applicative ((<$>), (<*>), pure, (<|>)) -import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP - ( createTransportExposeInternals - , TransportInternals(socketBetween) - , defaultTCPParameters ) +import qualified Network.Transport as NT (Transport) +import Network.Transport.TCP (TransportInternals) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) +import Control.Distributed.Process.Internal.Types() import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Serializable() import Test.HUnit (Assertion) -import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import Control.Distributed.Platform.GenServer @@ -77,7 +61,7 @@ testPing transport = do c <- getState liftIO $ putMVar initDone c initOk Infinity, - terminateHandler = \reason -> do + terminateHandler = \_ -> do --trace "Terminate ..." c <- getState liftIO $ putMVar terminateDone c @@ -125,12 +109,12 @@ testCounter transport = do runProcess localNode $ do cid <- startCounter 0 - c <- getCount cid + _ <- getCount cid incCount cid incCount cid - c <- getCount cid + _ <- getCount cid resetCount cid - c2 <- getCount cid + _ <- getCount cid stopCounter cid liftIO $ putMVar serverDone True return () From 478295df0472eecce199bd4d2bf4619dd688837a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 11:01:37 +0000 Subject: [PATCH 0424/2357] silence those pesky compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 70 +++++++++---------- tests/TestGenProcess.hs | 5 -- tests/TestGenServer.hs | 40 ++++------- 3 files changed, 47 insertions(+), 68 deletions(-) delete mode 100644 tests/TestGenProcess.hs diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..4ab7476b 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -38,39 +38,39 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) -import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, - exit, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) +import qualified Control.Distributed.Process as P (forward, catch) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) + +import Control.Distributed.Process (AbstractMessage, + Match, + Process, + ProcessId, + expectTimeout, + monitor, unmonitor, + link, finally, + exit, getSelfPid, match, + matchAny, matchIf, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + ProcessMonitorNotification(..)) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer + +import Data.Binary (Binary (..), getWord8, putWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- @@ -180,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -275,7 +275,7 @@ callServer sid timeout rq = do --say $ "Matched: " ++ show resp return resp) - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do --say $ "Matched: " ++ show n mayResp <- expectTimeout 0 case mayResp of diff --git a/tests/TestGenProcess.hs b/tests/TestGenProcess.hs deleted file mode 100644 index 7a570a72..00000000 --- a/tests/TestGenProcess.hs +++ /dev/null @@ -1,5 +0,0 @@ -module TestGenProcess where - -import Test.HUnit - -test1 = TestCase (assertEqual "1 == 2" 1 2) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 394a39d4..cfb5a4c9 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -3,40 +3,24 @@ {-# LANGUAGE TemplateHaskell #-} module TestGenServer where -import System.IO (hPutStrLn, stderr) -import Data.Binary (Binary (..), getWord8, - putWord8) +import Data.Binary (Binary (..)) import Data.Typeable (Typeable) import Data.DeriveTH -import Data.Foldable (forM_) -import Control.Concurrent (forkIO, threadDelay, myThreadId, throwTo, ThreadId) +import Control.Concurrent (forkIO) import Control.Concurrent.MVar - ( MVar - , newEmptyMVar + ( newEmptyMVar , putMVar , takeMVar - , readMVar - ) -import Control.Monad (replicateM_, replicateM, forever) -import Control.Exception (SomeException, throwIO) -import qualified Control.Exception as Ex (catch) -import Control.Applicative ((<$>), (<*>), pure, (<|>)) -import qualified Network.Transport as NT (Transport, closeEndPoint) -import Network.Transport.TCP - ( createTransportExposeInternals - , TransportInternals(socketBetween) - , defaultTCPParameters ) +import qualified Network.Transport as NT (Transport) +import Network.Transport.TCP (TransportInternals) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( NodeId(nodeAddress) - , LocalNode(localEndPoint) - ) +import Control.Distributed.Process.Internal.Types() import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Serializable() import Test.HUnit (Assertion) -import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import Control.Distributed.Platform.GenServer @@ -77,7 +61,7 @@ testPing transport = do c <- getState liftIO $ putMVar initDone c initOk Infinity, - terminateHandler = \reason -> do + terminateHandler = \_ -> do --trace "Terminate ..." c <- getState liftIO $ putMVar terminateDone c @@ -125,12 +109,12 @@ testCounter transport = do runProcess localNode $ do cid <- startCounter 0 - c <- getCount cid + _ <- getCount cid incCount cid incCount cid - c <- getCount cid + _ <- getCount cid resetCount cid - c2 <- getCount cid + _ <- getCount cid stopCounter cid liftIO $ putMVar serverDone True return () From 338ec4c574134f9bcb36fad61c1a2fa9cdbac3cb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 11:01:37 +0000 Subject: [PATCH 0425/2357] silence those pesky compiler warnings --- src/Control/Distributed/Platform/GenServer.hs | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index 3a49ad6c..4ab7476b 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -38,39 +38,39 @@ module Control.Distributed.Platform.GenServer ( trace ) where -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) -import qualified Control.Distributed.Process as P (forward, catch) -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, - exit, - getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Data.Binary (Binary (..), - getWord8, putWord8) -import Data.DeriveTH -import Data.Typeable (Typeable) +import qualified Control.Distributed.Process as P (forward, catch) +import qualified Control.Monad.State as ST (MonadState, + MonadTrans, + StateT, get, + lift, modify, + put, + runStateT) + +import Control.Applicative (Applicative) +import Control.Exception (SomeException) +import Control.Monad.IO.Class (MonadIO) + +import Control.Distributed.Process (AbstractMessage, + Match, + Process, + ProcessId, + expectTimeout, + monitor, unmonitor, + link, finally, + exit, getSelfPid, match, + matchAny, matchIf, + receiveTimeout, + receiveWait, say, + send, spawnLocal, + ProcessMonitorNotification(..)) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer + +import Data.Binary (Binary (..), getWord8, putWord8) +import Data.DeriveTH +import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- Data Types -- @@ -180,7 +180,7 @@ handleIf cond handler = MessageDispatcherIf { --say $ "Server REPLY: " ++ show r send cid resp return (s', Just (TerminateReason reason)) - CastMessage cid payload -> do + CastMessage _ payload -> do --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" (r, s') <- runServer (handler payload) s case r of @@ -275,7 +275,7 @@ callServer sid timeout rq = do --say $ "Matched: " ++ show resp return resp) - matchDied = match (\n@(ProcessMonitorNotification _ _ reason) -> do + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do --say $ "Matched: " ++ show n mayResp <- expectTimeout 0 case mayResp of From 2155b221eacea4319444c884b28c6d032cacea2f Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:40:11 -0500 Subject: [PATCH 0426/2357] Separated Async from GenServer & added explicit Maybe result type for functions with timeouts --- src/Control/Distributed/Platform/Async.hs | 58 ++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 68 +++++-------------- tests/GenServer/Counter.hs | 4 +- tests/GenServer/Kitty.hs | 14 +--- tests/Main.hs | 10 +-- tests/TestGenServer.hs | 2 +- 6 files changed, 86 insertions(+), 70 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs new file mode 100644 index 00000000..eeaf2a35 --- /dev/null +++ b/src/Control/Distributed/Platform/Async.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Control.Distributed.Platform.Async ( + Async(), + async, + wait, + waitTimeout + ) where +import Control.Concurrent.MVar +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer +import Control.Distributed.Process (Process, + ProcessId, ProcessMonitorNotification (..), + finally, liftIO, + match, monitor, + receiveTimeout, + receiveWait, + unmonitor) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Data.Maybe (fromMaybe) + + +-- | Async data type +data Async a = Async MonitorRef (MVar a) + +-- | +async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) +async sid proc = do + ref <- monitor sid + proc + mvar <- liftIO newEmptyMVar + return $ Async ref mvar + +-- | Wait for the call response +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") + +-- | Wait for the call response given a timeout +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) +waitTimeout (Async ref respMVar) timeout = do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Nothing -> do + respM' <- finally (receive timeout) (unmonitor ref) + case respM' of + Just resp -> do + liftIO $ putMVar respMVar resp + return respM' + _ -> return respM' + _ -> return respM + where + receive to = case to of + Infinity -> receiveWait matches >>= return . Just + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match return, + match (\(ProcessMonitorNotification _ _ reason) -> + receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c545e510..2c4a8c44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -35,31 +35,30 @@ module Control.Distributed.Platform.GenServer ( cast, Async(), call, + callTimeout, callAsync, wait, waitTimeout, Process, trace ) where -import Control.Concurrent.MVar +import Data.Maybe(fromJust) import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, liftIO, + Match, Process, ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, + monitor, + link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) + send, spawnLocal) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -75,6 +74,7 @@ import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH import Data.Typeable (Typeable) +import Control.Distributed.Platform.Async -------------------------------------------------------------------------------- -- Data Types -- @@ -256,56 +256,22 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) --- | Async data type -data Async a = Async MonitorRef (MVar a) +-- | Sync call with no timeout +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs +call sid rq = callTimeout sid Infinity rq >>= return . fromJust --- | Sync call to a server -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -call sid timeout rq = do +-- | Sync call +callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) +callTimeout sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout -- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = do - cid <- getSelfPid - ref <- monitor sid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - respMVar <- liftIO newEmptyMVar - return $ Async ref respMVar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref respMVar) timeout = - let - receive to = case to of - Infinity -> do - resp <- receiveWait matches - return $ Just resp - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- receiveTimeout 0 [match return] - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason)] - in do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Just resp -> return resp - Nothing -> do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> do - liftIO $ putMVar respMVar resp - return resp - Nothing -> error "Response-receive timeout" +callAsync sid rq = async sid $ do + cid <- getSelfPid + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 8d08b538..4eb50cbc 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -67,13 +67,13 @@ terminateCounter sid = terminate sid () -- | Increment count incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented c <- call sid Infinity IncrementCounter + CounterIncremented c <- call sid IncrementCounter return c -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- call sid Infinity GetCount + Count c <- call sid GetCount return c -- | Get the current count asynchronously diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index bd0ab61b..f09e5c69 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -19,7 +19,7 @@ module GenServer.Kitty ) where import Control.Distributed.Platform.GenServer - +import Control.Monad(void) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH @@ -80,11 +80,7 @@ terminateKitty sid = terminate sid () -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat -orderCat sid name color descr = do - result <- call sid Infinity (OrderCat name color descr) - case result of - CatOrdered c -> return c - _ -> error $ "Unexpected result " ++ show result +orderCat sid name color descr = call sid (OrderCat name color descr) >>= \(CatOrdered c) -> return c -- | Async call orderCatAsync :: ServerId -> Name -> Color -> Description -> Process (Async Cat) @@ -96,11 +92,7 @@ returnCat sid cat = cast sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () -closeShop sid = do - result <- call sid Infinity CloseShop - case result of - ShopClosed -> return () - _ -> error $ "Unexpected result " ++ show result +closeShop sid = call sid CloseShop >>= \CloseShop -> return () -- -- %%% Server functions diff --git a/tests/Main.hs b/tests/Main.hs index 9134a6ed..2aca9e53 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -77,10 +77,10 @@ kittyTransactions kPid 0 = return () kittyTransactions kPid n = do say "ca1" cat1 <- orderCat kPid "c1" "black" "a black cat" - say "a2" - a2 <- orderCatAsync kPid "c2" "black" "a black cat" - say "cat2" - cat2 <- waitTimeout a2 Infinity + --say "a2" + --a2 <- orderCatAsync kPid "c2" "black" "a black cat" + --say "cat2" + --cat2 <- wait a2 returnCat kPid cat1 - returnCat kPid cat2 + --returnCat kPid cat2 kittyTransactions kPid (n - 1) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e64a1c3a..75cd4d1c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -100,7 +100,7 @@ testPing transport = do liftIO $ takeMVar initDone --replicateM_ 10 $ do - Pong <- call sid (Timeout (TimeInterval Seconds 10)) Ping + Just Pong <- callTimeout sid (Timeout (TimeInterval Seconds 10)) Ping liftIO $ takeMVar pingDone cast sid Pong liftIO $ takeMVar pongDone From 478beb0d25884b94185e29b7936b88c7930f0398 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:40:11 -0500 Subject: [PATCH 0427/2357] Separated Async from GenServer & added explicit Maybe result type for functions with timeouts --- src/Control/Distributed/Platform/Async.hs | 58 ++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 68 +++++-------------- tests/GenServer/Counter.hs | 4 +- tests/GenServer/Kitty.hs | 14 +--- tests/Main.hs | 10 +-- tests/TestGenServer.hs | 2 +- 6 files changed, 86 insertions(+), 70 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs new file mode 100644 index 00000000..eeaf2a35 --- /dev/null +++ b/src/Control/Distributed/Platform/Async.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Control.Distributed.Platform.Async ( + Async(), + async, + wait, + waitTimeout + ) where +import Control.Concurrent.MVar +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer +import Control.Distributed.Process (Process, + ProcessId, ProcessMonitorNotification (..), + finally, liftIO, + match, monitor, + receiveTimeout, + receiveWait, + unmonitor) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Data.Maybe (fromMaybe) + + +-- | Async data type +data Async a = Async MonitorRef (MVar a) + +-- | +async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) +async sid proc = do + ref <- monitor sid + proc + mvar <- liftIO newEmptyMVar + return $ Async ref mvar + +-- | Wait for the call response +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") + +-- | Wait for the call response given a timeout +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) +waitTimeout (Async ref respMVar) timeout = do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Nothing -> do + respM' <- finally (receive timeout) (unmonitor ref) + case respM' of + Just resp -> do + liftIO $ putMVar respMVar resp + return respM' + _ -> return respM' + _ -> return respM + where + receive to = case to of + Infinity -> receiveWait matches >>= return . Just + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match return, + match (\(ProcessMonitorNotification _ _ reason) -> + receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c545e510..2c4a8c44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -35,31 +35,30 @@ module Control.Distributed.Platform.GenServer ( cast, Async(), call, + callTimeout, callAsync, wait, waitTimeout, Process, trace ) where -import Control.Concurrent.MVar +import Data.Maybe(fromJust) import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, liftIO, + Match, Process, ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, + monitor, + link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) + send, spawnLocal) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -75,6 +74,7 @@ import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH import Data.Typeable (Typeable) +import Control.Distributed.Platform.Async -------------------------------------------------------------------------------- -- Data Types -- @@ -256,56 +256,22 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) --- | Async data type -data Async a = Async MonitorRef (MVar a) +-- | Sync call with no timeout +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs +call sid rq = callTimeout sid Infinity rq >>= return . fromJust --- | Sync call to a server -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -call sid timeout rq = do +-- | Sync call +callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) +callTimeout sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout -- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = do - cid <- getSelfPid - ref <- monitor sid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - respMVar <- liftIO newEmptyMVar - return $ Async ref respMVar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref respMVar) timeout = - let - receive to = case to of - Infinity -> do - resp <- receiveWait matches - return $ Just resp - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- receiveTimeout 0 [match return] - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason)] - in do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Just resp -> return resp - Nothing -> do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> do - liftIO $ putMVar respMVar resp - return resp - Nothing -> error "Response-receive timeout" +callAsync sid rq = async sid $ do + cid <- getSelfPid + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 8d08b538..4eb50cbc 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -67,13 +67,13 @@ terminateCounter sid = terminate sid () -- | Increment count incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented c <- call sid Infinity IncrementCounter + CounterIncremented c <- call sid IncrementCounter return c -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- call sid Infinity GetCount + Count c <- call sid GetCount return c -- | Get the current count asynchronously diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index bd0ab61b..f09e5c69 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -19,7 +19,7 @@ module GenServer.Kitty ) where import Control.Distributed.Platform.GenServer - +import Control.Monad(void) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH @@ -80,11 +80,7 @@ terminateKitty sid = terminate sid () -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat -orderCat sid name color descr = do - result <- call sid Infinity (OrderCat name color descr) - case result of - CatOrdered c -> return c - _ -> error $ "Unexpected result " ++ show result +orderCat sid name color descr = call sid (OrderCat name color descr) >>= \(CatOrdered c) -> return c -- | Async call orderCatAsync :: ServerId -> Name -> Color -> Description -> Process (Async Cat) @@ -96,11 +92,7 @@ returnCat sid cat = cast sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () -closeShop sid = do - result <- call sid Infinity CloseShop - case result of - ShopClosed -> return () - _ -> error $ "Unexpected result " ++ show result +closeShop sid = call sid CloseShop >>= \CloseShop -> return () -- -- %%% Server functions diff --git a/tests/Main.hs b/tests/Main.hs index 9134a6ed..2aca9e53 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -77,10 +77,10 @@ kittyTransactions kPid 0 = return () kittyTransactions kPid n = do say "ca1" cat1 <- orderCat kPid "c1" "black" "a black cat" - say "a2" - a2 <- orderCatAsync kPid "c2" "black" "a black cat" - say "cat2" - cat2 <- waitTimeout a2 Infinity + --say "a2" + --a2 <- orderCatAsync kPid "c2" "black" "a black cat" + --say "cat2" + --cat2 <- wait a2 returnCat kPid cat1 - returnCat kPid cat2 + --returnCat kPid cat2 kittyTransactions kPid (n - 1) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e64a1c3a..75cd4d1c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -100,7 +100,7 @@ testPing transport = do liftIO $ takeMVar initDone --replicateM_ 10 $ do - Pong <- call sid (Timeout (TimeInterval Seconds 10)) Ping + Just Pong <- callTimeout sid (Timeout (TimeInterval Seconds 10)) Ping liftIO $ takeMVar pingDone cast sid Pong liftIO $ takeMVar pongDone From 7d7fd5dba1f3b386898d376dcf6ac4bd91738bba Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:40:11 -0500 Subject: [PATCH 0428/2357] Separated Async from GenServer & added explicit Maybe result type for functions with timeouts --- src/Control/Distributed/Platform/Async.hs | 58 ++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 68 +++++-------------- tests/GenServer/Counter.hs | 4 +- tests/GenServer/Kitty.hs | 14 +--- tests/Main.hs | 10 +-- tests/TestGenServer.hs | 2 +- 6 files changed, 86 insertions(+), 70 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs new file mode 100644 index 00000000..eeaf2a35 --- /dev/null +++ b/src/Control/Distributed/Platform/Async.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Control.Distributed.Platform.Async ( + Async(), + async, + wait, + waitTimeout + ) where +import Control.Concurrent.MVar +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer +import Control.Distributed.Process (Process, + ProcessId, ProcessMonitorNotification (..), + finally, liftIO, + match, monitor, + receiveTimeout, + receiveWait, + unmonitor) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Data.Maybe (fromMaybe) + + +-- | Async data type +data Async a = Async MonitorRef (MVar a) + +-- | +async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) +async sid proc = do + ref <- monitor sid + proc + mvar <- liftIO newEmptyMVar + return $ Async ref mvar + +-- | Wait for the call response +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") + +-- | Wait for the call response given a timeout +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) +waitTimeout (Async ref respMVar) timeout = do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Nothing -> do + respM' <- finally (receive timeout) (unmonitor ref) + case respM' of + Just resp -> do + liftIO $ putMVar respMVar resp + return respM' + _ -> return respM' + _ -> return respM + where + receive to = case to of + Infinity -> receiveWait matches >>= return . Just + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match return, + match (\(ProcessMonitorNotification _ _ reason) -> + receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c545e510..2c4a8c44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -35,31 +35,30 @@ module Control.Distributed.Platform.GenServer ( cast, Async(), call, + callTimeout, callAsync, wait, waitTimeout, Process, trace ) where -import Control.Concurrent.MVar +import Data.Maybe(fromJust) import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, liftIO, + Match, Process, ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, + monitor, + link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) + send, spawnLocal) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -75,6 +74,7 @@ import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH import Data.Typeable (Typeable) +import Control.Distributed.Platform.Async -------------------------------------------------------------------------------- -- Data Types -- @@ -256,56 +256,22 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) --- | Async data type -data Async a = Async MonitorRef (MVar a) +-- | Sync call with no timeout +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs +call sid rq = callTimeout sid Infinity rq >>= return . fromJust --- | Sync call to a server -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -call sid timeout rq = do +-- | Sync call +callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) +callTimeout sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout -- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = do - cid <- getSelfPid - ref <- monitor sid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - respMVar <- liftIO newEmptyMVar - return $ Async ref respMVar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref respMVar) timeout = - let - receive to = case to of - Infinity -> do - resp <- receiveWait matches - return $ Just resp - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- receiveTimeout 0 [match return] - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason)] - in do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Just resp -> return resp - Nothing -> do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> do - liftIO $ putMVar respMVar resp - return resp - Nothing -> error "Response-receive timeout" +callAsync sid rq = async sid $ do + cid <- getSelfPid + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 8d08b538..4eb50cbc 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -67,13 +67,13 @@ terminateCounter sid = terminate sid () -- | Increment count incCount :: ServerId -> Process Int incCount sid = do - CounterIncremented c <- call sid Infinity IncrementCounter + CounterIncremented c <- call sid IncrementCounter return c -- | Get the current count getCount :: ServerId -> Process Int getCount sid = do - Count c <- call sid Infinity GetCount + Count c <- call sid GetCount return c -- | Get the current count asynchronously diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index bd0ab61b..f09e5c69 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -19,7 +19,7 @@ module GenServer.Kitty ) where import Control.Distributed.Platform.GenServer - +import Control.Monad(void) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH @@ -80,11 +80,7 @@ terminateKitty sid = terminate sid () -- %% Synchronous call orderCat :: ServerId -> Name -> Color -> Description -> Process Cat -orderCat sid name color descr = do - result <- call sid Infinity (OrderCat name color descr) - case result of - CatOrdered c -> return c - _ -> error $ "Unexpected result " ++ show result +orderCat sid name color descr = call sid (OrderCat name color descr) >>= \(CatOrdered c) -> return c -- | Async call orderCatAsync :: ServerId -> Name -> Color -> Description -> Process (Async Cat) @@ -96,11 +92,7 @@ returnCat sid cat = cast sid (ReturnCat cat) -- %% sync call closeShop :: ServerId -> Process () -closeShop sid = do - result <- call sid Infinity CloseShop - case result of - ShopClosed -> return () - _ -> error $ "Unexpected result " ++ show result +closeShop sid = call sid CloseShop >>= \CloseShop -> return () -- -- %%% Server functions diff --git a/tests/Main.hs b/tests/Main.hs index 9134a6ed..2aca9e53 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -77,10 +77,10 @@ kittyTransactions kPid 0 = return () kittyTransactions kPid n = do say "ca1" cat1 <- orderCat kPid "c1" "black" "a black cat" - say "a2" - a2 <- orderCatAsync kPid "c2" "black" "a black cat" - say "cat2" - cat2 <- waitTimeout a2 Infinity + --say "a2" + --a2 <- orderCatAsync kPid "c2" "black" "a black cat" + --say "cat2" + --cat2 <- wait a2 returnCat kPid cat1 - returnCat kPid cat2 + --returnCat kPid cat2 kittyTransactions kPid (n - 1) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e64a1c3a..75cd4d1c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -100,7 +100,7 @@ testPing transport = do liftIO $ takeMVar initDone --replicateM_ 10 $ do - Pong <- call sid (Timeout (TimeInterval Seconds 10)) Ping + Just Pong <- callTimeout sid (Timeout (TimeInterval Seconds 10)) Ping liftIO $ takeMVar pingDone cast sid Pong liftIO $ takeMVar pongDone From fdbd8c5258ce3c6d9b414c562cb366ed7cdc152d Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:40:11 -0500 Subject: [PATCH 0429/2357] Separated Async from GenServer & added explicit Maybe result type for functions with timeouts --- src/Control/Distributed/Platform/Async.hs | 58 ++++++++++++++++ src/Control/Distributed/Platform/GenServer.hs | 68 +++++-------------- 2 files changed, 75 insertions(+), 51 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs new file mode 100644 index 00000000..eeaf2a35 --- /dev/null +++ b/src/Control/Distributed/Platform/Async.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Control.Distributed.Platform.Async ( + Async(), + async, + wait, + waitTimeout + ) where +import Control.Concurrent.MVar +import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Timer +import Control.Distributed.Process (Process, + ProcessId, ProcessMonitorNotification (..), + finally, liftIO, + match, monitor, + receiveTimeout, + receiveWait, + unmonitor) +import Control.Distributed.Process.Internal.Types (MonitorRef) +import Control.Distributed.Process.Serializable (Serializable) +import Data.Maybe (fromMaybe) + + +-- | Async data type +data Async a = Async MonitorRef (MVar a) + +-- | +async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) +async sid proc = do + ref <- monitor sid + proc + mvar <- liftIO newEmptyMVar + return $ Async ref mvar + +-- | Wait for the call response +wait :: (Serializable a, Show a) => Async a -> Process a +wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") + +-- | Wait for the call response given a timeout +waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) +waitTimeout (Async ref respMVar) timeout = do + respM <- liftIO $ tryTakeMVar respMVar + case respM of + Nothing -> do + respM' <- finally (receive timeout) (unmonitor ref) + case respM' of + Just resp -> do + liftIO $ putMVar respMVar resp + return respM' + _ -> return respM' + _ -> return respM + where + receive to = case to of + Infinity -> receiveWait matches >>= return . Just + Timeout t -> receiveTimeout (intervalToMs t) matches + matches = [ + match return, + match (\(ProcessMonitorNotification _ _ reason) -> + receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index c545e510..2c4a8c44 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -35,31 +35,30 @@ module Control.Distributed.Platform.GenServer ( cast, Async(), call, + callTimeout, callAsync, wait, waitTimeout, Process, trace ) where -import Control.Concurrent.MVar +import Data.Maybe(fromJust) import Control.Applicative (Applicative) import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO) import qualified Control.Distributed.Process as P (forward, catch) import Control.Distributed.Process (AbstractMessage, - Match, liftIO, + Match, Process, ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, + monitor, + link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) + send, spawnLocal) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Platform.Internal.Types @@ -75,6 +74,7 @@ import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH import Data.Typeable (Typeable) +import Control.Distributed.Platform.Async -------------------------------------------------------------------------------- -- Data Types -- @@ -256,56 +256,22 @@ startMonitor s ls = do ref <- monitor pid return (pid, ref) --- | Async data type -data Async a = Async MonitorRef (MVar a) +-- | Sync call with no timeout +call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs +call sid rq = callTimeout sid Infinity rq >>= return . fromJust --- | Sync call to a server -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs -call sid timeout rq = do +-- | Sync call +callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) +callTimeout sid timeout rq = do a1 <- callAsync sid rq waitTimeout a1 timeout -- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = do - cid <- getSelfPid - ref <- monitor sid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - respMVar <- liftIO newEmptyMVar - return $ Async ref respMVar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process a -waitTimeout (Async ref respMVar) timeout = - let - receive to = case to of - Infinity -> do - resp <- receiveWait matches - return $ Just resp - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> do - mayResp <- receiveTimeout 0 [match return] - case mayResp of - Just resp -> return resp - Nothing -> error $ "Server died: " ++ show reason)] - in do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Just resp -> return resp - Nothing -> do - respM <- finally (receive timeout) (unmonitor ref) - case respM of - Just resp -> do - liftIO $ putMVar respMVar resp - return resp - Nothing -> error "Response-receive timeout" +callAsync sid rq = async sid $ do + cid <- getSelfPid + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) -- | Cast a message to a server identified by it's ServerId cast :: (Serializable a) => ServerId -> a -> Process () From 17f4e2bf9160a39621f15b037b948c8fe42c1a28 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:47:19 -0500 Subject: [PATCH 0430/2357] removed tests/Main.hs --- tests/Main.hs | 86 --------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 tests/Main.hs diff --git a/tests/Main.hs b/tests/Main.hs deleted file mode 100644 index 2aca9e53..00000000 --- a/tests/Main.hs +++ /dev/null @@ -1,86 +0,0 @@ -module Main where - -import Prelude hiding (catch) -import GenServer.Counter -import GenServer.Kitty -import Control.Exception (SomeException) -import Control.Distributed.Static (initRemoteTable) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) -import Control.Distributed.Process (Process, catch, say) -import Control.Distributed.Process.Node (newLocalNode, runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode $ (kittyTest 10) `catch` \e -> say $ show (e :: SomeException) - runProcess localNode $ counterTest `catch` \e -> say $ show (e :: SomeException) - --putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - terminateCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - say "-- Stopping kitty shop ..." - terminateKitty kPid - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - say "ca1" - cat1 <- orderCat kPid "c1" "black" "a black cat" - --say "a2" - --a2 <- orderCatAsync kPid "c2" "black" "a black cat" - --say "cat2" - --cat2 <- wait a2 - returnCat kPid cat1 - --returnCat kPid cat2 - kittyTransactions kPid (n - 1) From 7302d91aa57253b94d795d904375811b76bab6a5 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:47:19 -0500 Subject: [PATCH 0431/2357] removed tests/Main.hs --- tests/Main.hs | 86 --------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 tests/Main.hs diff --git a/tests/Main.hs b/tests/Main.hs deleted file mode 100644 index 2aca9e53..00000000 --- a/tests/Main.hs +++ /dev/null @@ -1,86 +0,0 @@ -module Main where - -import Prelude hiding (catch) -import GenServer.Counter -import GenServer.Kitty -import Control.Exception (SomeException) -import Control.Distributed.Static (initRemoteTable) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) -import Control.Distributed.Process (Process, catch, say) -import Control.Distributed.Process.Node (newLocalNode, runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode $ (kittyTest 10) `catch` \e -> say $ show (e :: SomeException) - runProcess localNode $ counterTest `catch` \e -> say $ show (e :: SomeException) - --putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - terminateCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - say "-- Stopping kitty shop ..." - terminateKitty kPid - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - say "ca1" - cat1 <- orderCat kPid "c1" "black" "a black cat" - --say "a2" - --a2 <- orderCatAsync kPid "c2" "black" "a black cat" - --say "cat2" - --cat2 <- wait a2 - returnCat kPid cat1 - --returnCat kPid cat2 - kittyTransactions kPid (n - 1) From aa923d50ff925f1adabd618748531d187b724ee5 Mon Sep 17 00:00:00 2001 From: RodLogic Date: Mon, 10 Dec 2012 07:47:19 -0500 Subject: [PATCH 0432/2357] removed tests/Main.hs --- tests/Main.hs | 86 --------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 tests/Main.hs diff --git a/tests/Main.hs b/tests/Main.hs deleted file mode 100644 index 2aca9e53..00000000 --- a/tests/Main.hs +++ /dev/null @@ -1,86 +0,0 @@ -module Main where - -import Prelude hiding (catch) -import GenServer.Counter -import GenServer.Kitty -import Control.Exception (SomeException) -import Control.Distributed.Static (initRemoteTable) -import Network.Transport.TCP (createTransport, - defaultTCPParameters) -import Control.Distributed.Process (Process, catch, say) -import Control.Distributed.Process.Node (newLocalNode, runProcess) -import System.IO - -host :: String -host = "::ffff:127.0.0.1" - - - -port :: String -port = "8000" - - - -main :: IO () -main = do - hSetBuffering stdout NoBuffering - putStrLn "Starting server ... " - t <- createTransport host port defaultTCPParameters - case t of - Left ex -> error $ show ex - Right transport -> do - putStrLn "Transport created." - localNode <- newLocalNode transport initRemoteTable - putStrLn "Local node created." - runProcess localNode $ (kittyTest 10) `catch` \e -> say $ show (e :: SomeException) - runProcess localNode $ counterTest `catch` \e -> say $ show (e :: SomeException) - --putStrLn "Server started!" - getChar - return () - - - -counterTest :: Process () -counterTest = do - say "-- Starting counter test ..." - cid <- startCounter 10 - c <- getCount cid - say $ "c = " ++ show c - incCount cid - incCount cid - c <- getCount cid - say $ "c = " ++ show c - resetCount cid - c2 <- getCount cid - say $ "c2 = " ++ show c2 - terminateCounter cid - return () - - - -kittyTest :: Int -> Process () -kittyTest n = do - say "-- Starting kitty test ..." - kPid <- startKitty [Cat "c1" "black" "a black cat"] - say $ "-- Ordering " ++ show n ++ " cats ..." - kittyTransactions kPid n - say "-- Closing kitty shop ..." - closeShop kPid - say "-- Stopping kitty shop ..." - terminateKitty kPid - closeShop kPid - return () - - - -kittyTransactions kPid 0 = return () -kittyTransactions kPid n = do - say "ca1" - cat1 <- orderCat kPid "c1" "black" "a black cat" - --say "a2" - --a2 <- orderCatAsync kPid "c2" "black" "a black cat" - --say "cat2" - --cat2 <- wait a2 - returnCat kPid cat1 - --returnCat kPid cat2 - kittyTransactions kPid (n - 1) From 961d357f9164c18a2f25dac401947a6f0b29e2e5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:01:16 +0000 Subject: [PATCH 0433/2357] rename test suite --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ff5b162..491bb316 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,7 +31,7 @@ library Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer -test-suite TestTimer +test-suite PlatformTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: From c6a340496205915e2696a4323e1428d33b81e0f8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:01:16 +0000 Subject: [PATCH 0434/2357] rename test suite --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ff5b162..491bb316 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,7 +31,7 @@ library Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer -test-suite TestTimer +test-suite PlatformTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: From 3b4bba0163eca09b6cb8b1d6aad03fd7983f9d54 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:01:16 +0000 Subject: [PATCH 0435/2357] rename test suite --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ff5b162..491bb316 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,7 +31,7 @@ library Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer -test-suite TestTimer +test-suite PlatformTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: From 35d438bd333e36f18cece42a503826810c5f06f8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:01:16 +0000 Subject: [PATCH 0436/2357] rename test suite --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ff5b162..491bb316 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,7 +31,7 @@ library Control.Distributed.Platform.GenServer other-modules: Control.Distributed.Platform.Timer -test-suite TestTimer +test-suite PlatformTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: From a51cd3fa3eb019dc3fc3eb499a7d6b6a6d6f1a02 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:02:42 +0000 Subject: [PATCH 0437/2357] take gen-server tests out of TestMain - they consistently deadlock --- tests/TestMain.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index c6f75c46..06ad8584 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -14,11 +14,11 @@ import TestTimer (timerTests) tests :: NT.Transport -> TransportInternals -> IO [Test] tests transport internals = do - gsTestGroup <- genServerTests transport internals + -- gsTestGroup <- genServerTests transport internals timerTestGroup <- timerTests transport internals return [ - testGroup "GenServer" gsTestGroup - , testGroup "Timer" timerTestGroup ] + testGroup "Timer" timerTestGroup ] + -- , testGroup "GenServer" gsTestGroup ] main :: IO () main = do From 5dec129fc96f6ac6fac55bbfaee9ed020c48d6c7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:02:42 +0000 Subject: [PATCH 0438/2357] take gen-server tests out of TestMain - they consistently deadlock --- tests/TestMain.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index c6f75c46..06ad8584 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -14,11 +14,11 @@ import TestTimer (timerTests) tests :: NT.Transport -> TransportInternals -> IO [Test] tests transport internals = do - gsTestGroup <- genServerTests transport internals + -- gsTestGroup <- genServerTests transport internals timerTestGroup <- timerTests transport internals return [ - testGroup "GenServer" gsTestGroup - , testGroup "Timer" timerTestGroup ] + testGroup "Timer" timerTestGroup ] + -- , testGroup "GenServer" gsTestGroup ] main :: IO () main = do From c6d45b3d6d9004cf5350832dd1c246eb5f5c4f1b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 15:02:42 +0000 Subject: [PATCH 0439/2357] take gen-server tests out of TestMain - they consistently deadlock --- tests/TestMain.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestMain.hs b/tests/TestMain.hs index c6f75c46..06ad8584 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -14,11 +14,11 @@ import TestTimer (timerTests) tests :: NT.Transport -> TransportInternals -> IO [Test] tests transport internals = do - gsTestGroup <- genServerTests transport internals + -- gsTestGroup <- genServerTests transport internals timerTestGroup <- timerTests transport internals return [ - testGroup "GenServer" gsTestGroup - , testGroup "Timer" timerTestGroup ] + testGroup "Timer" timerTestGroup ] + -- , testGroup "GenServer" gsTestGroup ] main :: IO () main = do From 289639540cb2c88f3dc875a13fa7ae6873eec5c6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:46:17 +0000 Subject: [PATCH 0440/2357] Primary API exports begin --- distributed-process-platform.cabal | 8 ++++++-- src/Control/Distributed/Platform.hs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 491bb316..9a4d8667 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,7 +29,9 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Platform.GenServer - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform test-suite PlatformTests type: exitcode-stdio-1.0 @@ -52,6 +54,8 @@ test-suite PlatformTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs new file mode 100644 index 00000000..faa2035d --- /dev/null +++ b/src/Control/Distributed/Platform.hs @@ -0,0 +1,11 @@ +-- | [Cloud Haskell Platform] +-- +module Control.Distributed.Platform + ( + Timeout(..) + , TimeInterval + , TimeUnit + ) where + +import Control.Distributed.Platform.Internal.Types + \ No newline at end of file From 77ab9803c84c69389a1f9bb85016531d49c91eb5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:46:17 +0000 Subject: [PATCH 0441/2357] Primary API exports begin --- distributed-process-platform.cabal | 8 ++++++-- src/Control/Distributed/Platform.hs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 491bb316..9a4d8667 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,7 +29,9 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Platform.GenServer - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform test-suite PlatformTests type: exitcode-stdio-1.0 @@ -52,6 +54,8 @@ test-suite PlatformTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs new file mode 100644 index 00000000..faa2035d --- /dev/null +++ b/src/Control/Distributed/Platform.hs @@ -0,0 +1,11 @@ +-- | [Cloud Haskell Platform] +-- +module Control.Distributed.Platform + ( + Timeout(..) + , TimeInterval + , TimeUnit + ) where + +import Control.Distributed.Platform.Internal.Types + \ No newline at end of file From 9e9fb5313024dcf2c4497aba1dadcc1575d32f48 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:46:17 +0000 Subject: [PATCH 0442/2357] Primary API exports begin --- distributed-process-platform.cabal | 8 ++++++-- src/Control/Distributed/Platform.hs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 491bb316..9a4d8667 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,7 +29,9 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Platform.GenServer - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform test-suite PlatformTests type: exitcode-stdio-1.0 @@ -52,6 +54,8 @@ test-suite PlatformTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs new file mode 100644 index 00000000..faa2035d --- /dev/null +++ b/src/Control/Distributed/Platform.hs @@ -0,0 +1,11 @@ +-- | [Cloud Haskell Platform] +-- +module Control.Distributed.Platform + ( + Timeout(..) + , TimeInterval + , TimeUnit + ) where + +import Control.Distributed.Platform.Internal.Types + \ No newline at end of file From e6d4d9fba252ed6fdae9dc0cab61c57b25c2642d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:46:17 +0000 Subject: [PATCH 0443/2357] Primary API exports begin --- distributed-process-platform.cabal | 8 ++++++-- src/Control/Distributed/Platform.hs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 491bb316..9a4d8667 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,7 +29,9 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Platform.GenServer - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform test-suite PlatformTests type: exitcode-stdio-1.0 @@ -52,6 +54,8 @@ test-suite PlatformTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: Control.Distributed.Platform.Timer + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs new file mode 100644 index 00000000..faa2035d --- /dev/null +++ b/src/Control/Distributed/Platform.hs @@ -0,0 +1,11 @@ +-- | [Cloud Haskell Platform] +-- +module Control.Distributed.Platform + ( + Timeout(..) + , TimeInterval + , TimeUnit + ) where + +import Control.Distributed.Platform.Internal.Types + \ No newline at end of file From a3519fab817441aed77a4998572a3f293036a773 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:56:57 +0000 Subject: [PATCH 0444/2357] always show test output/details --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 707f404d..c69f6e8d 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ all: build .PHONY: test test: build - cabal test + cabal test --show-details=always .PHONY: build build: configure From 0dcf378051600255bb1c3f2fc5cef7e3b216f657 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:56:57 +0000 Subject: [PATCH 0445/2357] always show test output/details --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 707f404d..c69f6e8d 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ all: build .PHONY: test test: build - cabal test + cabal test --show-details=always .PHONY: build build: configure From f5b6c6d97f689bf5fe1516850a74eb0f534bbbc5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:56:57 +0000 Subject: [PATCH 0446/2357] always show test output/details --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 707f404d..c69f6e8d 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ all: build .PHONY: test test: build - cabal test + cabal test --show-details=always .PHONY: build build: configure From eff7c03513520f3c6732664e0b32cb5ef2282128 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:57:44 +0000 Subject: [PATCH 0447/2357] restructure packages/modules; flushTimer now uses monitors --- distributed-process-platform.cabal | 10 ++-- src/Control/Distributed/Platform.hs | 11 +++- src/Control/Distributed/Platform/Async.hs | 3 +- .../Distributed/Platform/GenProcess.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 3 +- src/Control/Distributed/Platform/Timer.hs | 58 +++++-------------- src/Control/Distributed/Platform/Utils.hs | 51 ++++++++++++++++ tests/GenServer/Kitty.hs | 1 - 8 files changed, 82 insertions(+), 57 deletions(-) create mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 9a4d8667..3b12359e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,10 +28,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform.GenServer + Control.Distributed.Platform, + Control.Distributed.Platform.GenServer, + Control.Distributed.Platform.Timer other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -56,6 +57,7 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + Control.Distributed.Platform.Utils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index faa2035d..e4c90fa0 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,10 +2,17 @@ -- module Control.Distributed.Platform ( - Timeout(..) + -- time interval handling + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , Timeout(..) , TimeInterval , TimeUnit ) where import Control.Distributed.Platform.Internal.Types - \ No newline at end of file +import Control.Distributed.Platform.Utils diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index eeaf2a35..0beadb1c 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -6,8 +6,7 @@ module Control.Distributed.Platform.Async ( waitTimeout ) where import Control.Concurrent.MVar -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), finally, liftIO, diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 2d893c62..71ec2644 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -240,5 +240,5 @@ trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? -- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may hot server-code loading quite easily... +-- a remote pid? if so then we may handle hot server-code loading quite easily... diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b5dd616b..b8b91710 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,8 +71,7 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index e074e510..fab63c13 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -1,10 +1,9 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Platform.Timer ( +module Control.Distributed.Platform.Timer + ( TimerRef - , TimeInterval(..) - , TimeUnit(..) , Tick(Tick) , sleep , sendAfter @@ -15,18 +14,12 @@ module Control.Distributed.Platform.Timer ( , resetTimer , cancelTimer , flushTimer - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs ) where import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Utils import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -53,38 +46,6 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -131,14 +92,21 @@ cancelTimer = (flip send) Cancel -- | cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () flushTimer ref ignore t = do + mRef <- monitor ref cancelTimer ref -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - _ <- receiveTimeout (intervalToMs t) [ + performFlush mRef t + return () + where performFlush mRef Infinity = receiveWait $ filters mRef + performFlush mRef (Timeout i) = + receiveTimeout (intervalToMs i) (filters mRef) >> return () + filters mRef = [ matchIf (\x -> x == ignore) + (\_ -> return ()) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] - return () -- | sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs new file mode 100644 index 00000000..54362d75 --- /dev/null +++ b/src/Control/Distributed/Platform/Utils.hs @@ -0,0 +1,51 @@ +-- | General utilities for use with distributed-process-platform. +-- These entities are mainly exported via the top level @Platform@ module +-- and should be imported from there in most cases. +module Control.Distributed.Platform.Utils + ( + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Platform.Internal.Types + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + \ No newline at end of file diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index f09e5c69..de5ade6c 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -19,7 +19,6 @@ module GenServer.Kitty ) where import Control.Distributed.Platform.GenServer -import Control.Monad(void) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH From 2847be1d2301d7adf5e62d6aa59622ccca5f384f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:57:44 +0000 Subject: [PATCH 0448/2357] restructure packages/modules; flushTimer now uses monitors --- distributed-process-platform.cabal | 10 ++-- src/Control/Distributed/Platform.hs | 11 +++- src/Control/Distributed/Platform/Async.hs | 3 +- .../Distributed/Platform/GenProcess.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 3 +- src/Control/Distributed/Platform/Timer.hs | 58 +++++-------------- src/Control/Distributed/Platform/Utils.hs | 51 ++++++++++++++++ tests/GenServer/Kitty.hs | 1 - 8 files changed, 82 insertions(+), 57 deletions(-) create mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 9a4d8667..3b12359e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,10 +28,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform.GenServer + Control.Distributed.Platform, + Control.Distributed.Platform.GenServer, + Control.Distributed.Platform.Timer other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -56,6 +57,7 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + Control.Distributed.Platform.Utils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index faa2035d..e4c90fa0 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,10 +2,17 @@ -- module Control.Distributed.Platform ( - Timeout(..) + -- time interval handling + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , Timeout(..) , TimeInterval , TimeUnit ) where import Control.Distributed.Platform.Internal.Types - \ No newline at end of file +import Control.Distributed.Platform.Utils diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index eeaf2a35..0beadb1c 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -6,8 +6,7 @@ module Control.Distributed.Platform.Async ( waitTimeout ) where import Control.Concurrent.MVar -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), finally, liftIO, diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 2d893c62..71ec2644 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -240,5 +240,5 @@ trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? -- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may hot server-code loading quite easily... +-- a remote pid? if so then we may handle hot server-code loading quite easily... diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b5dd616b..b8b91710 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,8 +71,7 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index e074e510..fab63c13 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -1,10 +1,9 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Platform.Timer ( +module Control.Distributed.Platform.Timer + ( TimerRef - , TimeInterval(..) - , TimeUnit(..) , Tick(Tick) , sleep , sendAfter @@ -15,18 +14,12 @@ module Control.Distributed.Platform.Timer ( , resetTimer , cancelTimer , flushTimer - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs ) where import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Utils import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -53,38 +46,6 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -131,14 +92,21 @@ cancelTimer = (flip send) Cancel -- | cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () flushTimer ref ignore t = do + mRef <- monitor ref cancelTimer ref -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - _ <- receiveTimeout (intervalToMs t) [ + performFlush mRef t + return () + where performFlush mRef Infinity = receiveWait $ filters mRef + performFlush mRef (Timeout i) = + receiveTimeout (intervalToMs i) (filters mRef) >> return () + filters mRef = [ matchIf (\x -> x == ignore) + (\_ -> return ()) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] - return () -- | sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs new file mode 100644 index 00000000..54362d75 --- /dev/null +++ b/src/Control/Distributed/Platform/Utils.hs @@ -0,0 +1,51 @@ +-- | General utilities for use with distributed-process-platform. +-- These entities are mainly exported via the top level @Platform@ module +-- and should be imported from there in most cases. +module Control.Distributed.Platform.Utils + ( + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Platform.Internal.Types + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + \ No newline at end of file diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index f09e5c69..de5ade6c 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -19,7 +19,6 @@ module GenServer.Kitty ) where import Control.Distributed.Platform.GenServer -import Control.Monad(void) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH From eca6a606d18d644927ba6144306a60d2b512b609 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:57:44 +0000 Subject: [PATCH 0449/2357] restructure packages/modules; flushTimer now uses monitors --- distributed-process-platform.cabal | 10 ++-- src/Control/Distributed/Platform.hs | 11 +++- src/Control/Distributed/Platform/Async.hs | 3 +- .../Distributed/Platform/GenProcess.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 3 +- src/Control/Distributed/Platform/Timer.hs | 58 +++++-------------- src/Control/Distributed/Platform/Utils.hs | 51 ++++++++++++++++ tests/GenServer/Kitty.hs | 1 - 8 files changed, 82 insertions(+), 57 deletions(-) create mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 9a4d8667..3b12359e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,10 +28,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform.GenServer + Control.Distributed.Platform, + Control.Distributed.Platform.GenServer, + Control.Distributed.Platform.Timer other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -56,6 +57,7 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + Control.Distributed.Platform.Utils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index faa2035d..e4c90fa0 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,10 +2,17 @@ -- module Control.Distributed.Platform ( - Timeout(..) + -- time interval handling + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , Timeout(..) , TimeInterval , TimeUnit ) where import Control.Distributed.Platform.Internal.Types - \ No newline at end of file +import Control.Distributed.Platform.Utils diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index eeaf2a35..0beadb1c 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -6,8 +6,7 @@ module Control.Distributed.Platform.Async ( waitTimeout ) where import Control.Concurrent.MVar -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), finally, liftIO, diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 2d893c62..71ec2644 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -240,5 +240,5 @@ trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? -- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may hot server-code loading quite easily... +-- a remote pid? if so then we may handle hot server-code loading quite easily... diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b5dd616b..b8b91710 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,8 +71,7 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index e074e510..fab63c13 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -1,10 +1,9 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Platform.Timer ( +module Control.Distributed.Platform.Timer + ( TimerRef - , TimeInterval(..) - , TimeUnit(..) , Tick(Tick) , sleep , sendAfter @@ -15,18 +14,12 @@ module Control.Distributed.Platform.Timer ( , resetTimer , cancelTimer , flushTimer - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs ) where import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Utils import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -53,38 +46,6 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -131,14 +92,21 @@ cancelTimer = (flip send) Cancel -- | cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () flushTimer ref ignore t = do + mRef <- monitor ref cancelTimer ref -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - _ <- receiveTimeout (intervalToMs t) [ + performFlush mRef t + return () + where performFlush mRef Infinity = receiveWait $ filters mRef + performFlush mRef (Timeout i) = + receiveTimeout (intervalToMs i) (filters mRef) >> return () + filters mRef = [ matchIf (\x -> x == ignore) + (\_ -> return ()) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] - return () -- | sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs new file mode 100644 index 00000000..54362d75 --- /dev/null +++ b/src/Control/Distributed/Platform/Utils.hs @@ -0,0 +1,51 @@ +-- | General utilities for use with distributed-process-platform. +-- These entities are mainly exported via the top level @Platform@ module +-- and should be imported from there in most cases. +module Control.Distributed.Platform.Utils + ( + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Platform.Internal.Types + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + \ No newline at end of file diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index f09e5c69..de5ade6c 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -19,7 +19,6 @@ module GenServer.Kitty ) where import Control.Distributed.Platform.GenServer -import Control.Monad(void) import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH From 1ae34a0b8f37be07c4096173ea2d5725de1a5e01 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 16:57:44 +0000 Subject: [PATCH 0450/2357] restructure packages/modules; flushTimer now uses monitors --- distributed-process-platform.cabal | 10 ++-- src/Control/Distributed/Platform.hs | 11 +++- src/Control/Distributed/Platform/Async.hs | 3 +- .../Distributed/Platform/GenProcess.hs | 2 +- src/Control/Distributed/Platform/GenServer.hs | 3 +- src/Control/Distributed/Platform/Timer.hs | 58 +++++-------------- src/Control/Distributed/Platform/Utils.hs | 51 ++++++++++++++++ 7 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 9a4d8667..3b12359e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -28,10 +28,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform.GenServer + Control.Distributed.Platform, + Control.Distributed.Platform.GenServer, + Control.Distributed.Platform.Timer other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -56,6 +57,7 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + Control.Distributed.Platform.Utils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index faa2035d..e4c90fa0 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,10 +2,17 @@ -- module Control.Distributed.Platform ( - Timeout(..) + -- time interval handling + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , Timeout(..) , TimeInterval , TimeUnit ) where import Control.Distributed.Platform.Internal.Types - \ No newline at end of file +import Control.Distributed.Platform.Utils diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index eeaf2a35..0beadb1c 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -6,8 +6,7 @@ module Control.Distributed.Platform.Async ( waitTimeout ) where import Control.Concurrent.MVar -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), finally, liftIO, diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 2d893c62..71ec2644 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -240,5 +240,5 @@ trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? -- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may hot server-code loading quite easily... +-- a remote pid? if so then we may handle hot server-code loading quite easily... diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b5dd616b..b8b91710 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -71,8 +71,7 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer +import Control.Distributed.Platform import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index e074e510..fab63c13 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -1,10 +1,9 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Platform.Timer ( +module Control.Distributed.Platform.Timer + ( TimerRef - , TimeInterval(..) - , TimeUnit(..) , Tick(Tick) , sleep , sendAfter @@ -15,18 +14,12 @@ module Control.Distributed.Platform.Timer ( , resetTimer , cancelTimer , flushTimer - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs ) where import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Utils import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -53,38 +46,6 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -131,14 +92,21 @@ cancelTimer = (flip send) Cancel -- | cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> TimeInterval -> Process () +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () flushTimer ref ignore t = do + mRef <- monitor ref cancelTimer ref -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - _ <- receiveTimeout (intervalToMs t) [ + performFlush mRef t + return () + where performFlush mRef Infinity = receiveWait $ filters mRef + performFlush mRef (Timeout i) = + receiveTimeout (intervalToMs i) (filters mRef) >> return () + filters mRef = [ matchIf (\x -> x == ignore) + (\_ -> return ()) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] - return () -- | sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs new file mode 100644 index 00000000..54362d75 --- /dev/null +++ b/src/Control/Distributed/Platform/Utils.hs @@ -0,0 +1,51 @@ +-- | General utilities for use with distributed-process-platform. +-- These entities are mainly exported via the top level @Platform@ module +-- and should be imported from there in most cases. +module Control.Distributed.Platform.Utils + ( + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + ) where + +import Control.Distributed.Platform.Internal.Types + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied TimeInterval to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a `TimeInterval' of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a `TimeInterval' of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a `TimeInterval' of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a `TimeInterval' of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied TimeUnit to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + \ No newline at end of file From 3c1f0e5e4bf15b17f46cb5e18657de55a3c63262 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:09:21 +0000 Subject: [PATCH 0451/2357] oops --- src/Control/Distributed/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 71ec2644..79845f90 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -20,7 +20,7 @@ import qualified Control.Monad.State as ST (StateT, get, import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer (intervalToMs) +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) From 3b3560fe8d4b87f9e3b3ddc72aaf473c41b849f7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:09:21 +0000 Subject: [PATCH 0452/2357] oops --- src/Control/Distributed/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 71ec2644..79845f90 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -20,7 +20,7 @@ import qualified Control.Monad.State as ST (StateT, get, import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer (intervalToMs) +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) From 78d75f81db2f9913c69c3aa235dedf1641c5c85d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:09:21 +0000 Subject: [PATCH 0453/2357] oops --- src/Control/Distributed/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 71ec2644..79845f90 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -20,7 +20,7 @@ import qualified Control.Monad.State as ST (StateT, get, import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer (intervalToMs) +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) From 9720cad3b4917d225526666fed38bbf9c5acc81e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:09:21 +0000 Subject: [PATCH 0454/2357] oops --- src/Control/Distributed/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs index 71ec2644..79845f90 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Platform/GenProcess.hs @@ -20,7 +20,7 @@ import qualified Control.Monad.State as ST (StateT, get, import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Timer (intervalToMs) +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) From 0ad829c1f20a2220465eda94a775766610a1d083 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:12:08 +0000 Subject: [PATCH 0455/2357] Utils/* => Platform/API; doc tidy --- distributed-process-platform.cabal | 5 +-- src/Control/Distributed/Platform.hs | 40 +++++++++++++++++- src/Control/Distributed/Platform/Timer.hs | 2 +- src/Control/Distributed/Platform/Utils.hs | 51 ----------------------- 4 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3b12359e..bdf58e3c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,8 +31,6 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer - other-modules: - Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -57,7 +55,6 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform, - Control.Distributed.Platform.Utils + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index e4c90fa0..3dcfffd2 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,7 +2,7 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- time interval handling milliseconds , seconds , minutes @@ -15,4 +15,40 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index fab63c13..036a582c 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -19,7 +19,7 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs deleted file mode 100644 index 54362d75..00000000 --- a/src/Control/Distributed/Platform/Utils.hs +++ /dev/null @@ -1,51 +0,0 @@ --- | General utilities for use with distributed-process-platform. --- These entities are mainly exported via the top level @Platform@ module --- and should be imported from there in most cases. -module Control.Distributed.Platform.Utils - ( - milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - ) where - -import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - \ No newline at end of file From f082646f5eeb02ff5f1a807a3c6080350c1692c8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:12:08 +0000 Subject: [PATCH 0456/2357] Utils/* => Platform/API; doc tidy --- distributed-process-platform.cabal | 5 +-- src/Control/Distributed/Platform.hs | 40 +++++++++++++++++- src/Control/Distributed/Platform/Timer.hs | 2 +- src/Control/Distributed/Platform/Utils.hs | 51 ----------------------- 4 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3b12359e..bdf58e3c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,8 +31,6 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer - other-modules: - Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -57,7 +55,6 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform, - Control.Distributed.Platform.Utils + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index e4c90fa0..3dcfffd2 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,7 +2,7 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- time interval handling milliseconds , seconds , minutes @@ -15,4 +15,40 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index fab63c13..036a582c 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -19,7 +19,7 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs deleted file mode 100644 index 54362d75..00000000 --- a/src/Control/Distributed/Platform/Utils.hs +++ /dev/null @@ -1,51 +0,0 @@ --- | General utilities for use with distributed-process-platform. --- These entities are mainly exported via the top level @Platform@ module --- and should be imported from there in most cases. -module Control.Distributed.Platform.Utils - ( - milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - ) where - -import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - \ No newline at end of file From e85f80733982f287897565e366b1dce1bcff3d85 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:12:08 +0000 Subject: [PATCH 0457/2357] Utils/* => Platform/API; doc tidy --- distributed-process-platform.cabal | 5 +-- src/Control/Distributed/Platform.hs | 40 +++++++++++++++++- src/Control/Distributed/Platform/Timer.hs | 2 +- src/Control/Distributed/Platform/Utils.hs | 51 ----------------------- 4 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3b12359e..bdf58e3c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,8 +31,6 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer - other-modules: - Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -57,7 +55,6 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform, - Control.Distributed.Platform.Utils + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index e4c90fa0..3dcfffd2 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,7 +2,7 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- time interval handling milliseconds , seconds , minutes @@ -15,4 +15,40 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index fab63c13..036a582c 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -19,7 +19,7 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs deleted file mode 100644 index 54362d75..00000000 --- a/src/Control/Distributed/Platform/Utils.hs +++ /dev/null @@ -1,51 +0,0 @@ --- | General utilities for use with distributed-process-platform. --- These entities are mainly exported via the top level @Platform@ module --- and should be imported from there in most cases. -module Control.Distributed.Platform.Utils - ( - milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - ) where - -import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - \ No newline at end of file From c8106e6556e5cff5182136c0568672f4dfb0cbc4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 10 Dec 2012 17:12:08 +0000 Subject: [PATCH 0458/2357] Utils/* => Platform/API; doc tidy --- distributed-process-platform.cabal | 5 +-- src/Control/Distributed/Platform.hs | 40 +++++++++++++++++- src/Control/Distributed/Platform/Timer.hs | 2 +- src/Control/Distributed/Platform/Utils.hs | 51 ----------------------- 4 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 src/Control/Distributed/Platform/Utils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3b12359e..bdf58e3c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -31,8 +31,6 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer - other-modules: - Control.Distributed.Platform.Utils test-suite PlatformTests type: exitcode-stdio-1.0 @@ -57,7 +55,6 @@ test-suite PlatformTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform, - Control.Distributed.Platform.Utils + Control.Distributed.Platform extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index e4c90fa0..3dcfffd2 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,7 +2,7 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- time interval handling milliseconds , seconds , minutes @@ -15,4 +15,40 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index fab63c13..036a582c 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -19,7 +19,7 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Utils +import Control.Distributed.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) diff --git a/src/Control/Distributed/Platform/Utils.hs b/src/Control/Distributed/Platform/Utils.hs deleted file mode 100644 index 54362d75..00000000 --- a/src/Control/Distributed/Platform/Utils.hs +++ /dev/null @@ -1,51 +0,0 @@ --- | General utilities for use with distributed-process-platform. --- These entities are mainly exported via the top level @Platform@ module --- and should be imported from there in most cases. -module Control.Distributed.Platform.Utils - ( - milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - ) where - -import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied TimeInterval to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a `TimeInterval' of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a `TimeInterval' of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a `TimeInterval' of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a `TimeInterval' of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied TimeUnit to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - \ No newline at end of file From bf1a9f0c01631d338b87d81929c5ef8f50c946f1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 11 Dec 2012 20:50:16 +0000 Subject: [PATCH 0459/2357] async actions use proxy to obtain results; monitoring is performed only in the proxy; no stray message are ever delivered to the caller's process; async actions are described by a rich result type describing the various observable states they may be in --- distributed-process-platform.cabal | 8 +- src/Control/Distributed/Platform.hs | 61 ++--- src/Control/Distributed/Platform/Async.hs | 229 +++++++++++++----- src/Control/Distributed/Platform/GenServer.hs | 90 +++---- .../Distributed/Platform/Internal/Types.hs | 7 +- src/Control/Distributed/Platform/Timer.hs | 64 ++++- tests/TestAsync.hs | 58 +++++ tests/TestMain.hs | 2 +- 8 files changed, 367 insertions(+), 152 deletions(-) create mode 100644 tests/TestAsync.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..f76cc51a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -30,7 +30,8 @@ library exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async test-suite PlatformTests type: exitcode-stdio-1.0 @@ -54,7 +55,10 @@ test-suite PlatformTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: + Control.Distributed.Platform.Async, Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + TestAsync, + TestUtils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..6b95b2cf 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,53 +2,38 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- exported time interval handling milliseconds , seconds , minutes , hours , intervalToMs , timeToMs + -- timeouts and time interval types , Timeout(..) , TimeInterval , TimeUnit + , TimerRef + -- exported timer operations + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer ) where import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - +import Control.Distributed.Platform.Timer + ( milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer + , TimerRef + ) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0beadb1c..0379b92d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,57 +1,174 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE EmptyDataDecls #-} + +module Control.Distributed.Platform.Async where + +import Control.Concurrent.MVar +import Control.Distributed.Platform + ( sendAfter + , TimerRef + , TimeInterval() + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +-- | A reference to an asynchronous action +type AsyncRef = ProcessId + +-- | A reference to an asynchronous worker +type AsyncWorkerId = AsyncRef + +-- | A reference to an asynchronous "gatherer" +type AsyncGathererId = AsyncRef + +-- | A function that takes an @AsyncGathererId@ (to which replies should be +-- sent) and spawns an asynchronous (user defined) action, returning the +-- spawned actions @AsyncWorkerId@ in the @Process@ monad. +type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId + +type AsyncData a = MVar (AsyncResult a) + +-- | An asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +data Async a = Async AsyncRef AsyncRef (AsyncData a) + +-- | Represents the result of an asynchronous action, which can be in several +-- states at any given time. +data AsyncResult a = + AsyncDone a -- | a completed action and its result + | AsyncFailed DiedReason -- | a failed action and the failure reason + | AsyncCancelled -- | a cancelled action + | AsyncPending -- | a pending action (that is still running) + +-- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- operation in the @Process@ monad. +type AsyncCancel = AsyncRef -> Process () + +-- TODO: Document me please! +async :: (Serializable a) => SpawnAsync -> Process (Async a) +async spawnF = do + mv <- liftIO $ newEmptyMVar + (wpid, gpid) <- spawnWorkers spawnF mv + return (Async wpid gpid mv) where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] + spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers sp ad = do + root <- getSelfPid + + -- listener/response proxy + gpid <- spawnLocal $ do + proxy <- getSelfPid + worker <- sp proxy + + send root worker + + monRef <- monitor worker + finally (pollUntilExit worker monRef ad) (unmonitor monRef) + + wpid <- expect + return (wpid, gpid) + + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit pid ref ad = do + r <- receiveWait [ + matchIf + (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid == pid') + (\(ProcessMonitorNotification _ _ r) -> return (Right r)) + , match (\x -> return (Left x)) + ] + case r of + Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] + Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left a -> liftIO $ putMVar ad (AsyncDone a) + +-- note [recursion] +-- We recurse *just once* if we've seen a normal exit from worker. We're +-- absolutely sure about this, because once we've seen DiedNormal for the +-- monitored process, it's not possible to see another monitor signal for it. +-- Based on this, the only other kinds of message that can arrive are the +-- return value from the worker or a cancellation from the coordinating process. + +-- | Check whether an @Async@ has completed yet. The status of the asynchronous +-- action is encoded in the returned @AsyncResult@. If not, the result is +-- @AsyncPending@, or one of the other constructors otherwise. +-- See @Async@. +poll :: (Serializable a) => Async a -> Process (AsyncResult a) +poll (Async _ _ d) = do + mv <- liftIO $ tryTakeMVar d + case mv of + Nothing -> return AsyncPending + Just v -> return v + +-- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if no result is available within the specified delay. +waitTimeout :: (Serializable a) => TimeInterval -> + Async a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = do + self <- getSelfPid + ar <- poll hAsync + case ar of + AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync + _ -> return (Just ar) + where + waitOnMailBox :: (Serializable a) => TimeInterval -> + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitOnMailBox t' a ref = do + m <- receiveTimeout 0 [ + match (\CancelWait -> return AsyncPending) + ] + -- TODO: this is pretty disgusting - sprinkle with applicative or some such + case m of + Nothing -> do + r <- check a + case r of + -- this isn't tail recursive, so we're likely to overflow fast + Nothing -> waitOnMailBox t' a ref + Just _ -> return r + Just _ -> + return m + +-- | Cancel an asynchronous operation. The cancellation method to be used +-- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or +-- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the +-- same way that message passing is asynchronous, whilst the former will block +-- until a @ProcessMonitorNotification@ is received for all participants in the +-- @Async@ action. +cancel :: Async a -> AsyncCancel -> Process () +cancel (Async w g d) asyncCancel = do + asyncCancel w + asyncCancel g + liftIO $ tryPutMVar d AsyncCancelled >> return () + +-- | Given an @AsyncRef@, will kill the associated process. This call returns +-- immediately. +cancelAsync :: AsyncCancel +cancelAsync = (flip kill) "cancelled" + +-- | Given an @AsyncRef@, will kill the associated process and block until +-- a @ProcessMonitorNotification@ is received, confirming that the process has +-- indeed died. Passing an @AsyncRef@ for a process that has already died is +-- not an error and will not block, so long as the monitor implementation +-- continues to support this. +cancelWait :: AsyncCancel +cancelWait pid = do + ref <- monitor pid + cancelAsync pid + receiveWait [ + matchIf (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid' == pid) + (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () + \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b8b91710..5271e42c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,17 +28,12 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - Async(), - call, - callTimeout, - callAsync, - wait, - waitTimeout, + startServer, + startServerLink, + startServerMonitor, + callServer, + castServer, + stopServer, Process, trace ) where @@ -68,14 +63,12 @@ import Control.Distributed.Process (AbstractMessage, receiveWait, say, send, spawnLocal, ProcessMonitorNotification(..)) - import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform -import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) import Data.DeriveTH import Data.Typeable (Typeable) @@ -237,8 +230,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc +startServer :: s -> LocalServer s -> Process ServerId +startServer s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -246,46 +239,59 @@ start s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls ref <- monitor pid return (pid, ref) --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do - a1 <- callAsync sid rq - waitTimeout a1 timeout - --- | Async call to a server -callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = async sid $ do - cid <- getSelfPid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) +-- | Call a server identified by it's ServerId +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +callServer sid timeout rq = do + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do +castServer :: (Serializable a) => ServerId -> a -> Process () +castServer sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs index a0721940..4df92c9e 100644 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -14,7 +14,8 @@ module Control.Distributed.Platform.Internal.Types ( TimeUnit(..) , TimeInterval(..) - , Timeout(..) + , Timeout(..) + , CancelWait(..) ) where import Data.Binary @@ -36,3 +37,7 @@ $(derive makeBinary ''TimeInterval) data Timeout = Timeout TimeInterval | Infinity deriving (Typeable, Show) $(derive makeBinary ''Timeout) + +data CancelWait = CancelWait + deriving (Typeable) +$(derive makeBinary ''CancelWait) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 036a582c..1105eeee 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -5,6 +5,12 @@ module Control.Distributed.Platform.Timer ( TimerRef , Tick(Tick) + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs , sleep , sendAfter , runAfter @@ -19,11 +25,10 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) +import Data.Typeable (Typeable) +import Prelude hiding (init) -- | an opaque reference to a timer type TimerRef = ProcessId @@ -46,6 +51,40 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- process implementations + -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -58,16 +97,17 @@ sleep t = do (\_ -> return ())] return () --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. +-- | Starts a timer which sends the supplied message to the destination process +-- after the specified time interval. The message is sent only once, after +-- which the timer exits normally. sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef sendAfter t pid msg = runAfter t (mkSender pid msg) --- | runs the supplied process action(s) after `t' has elapsed +-- | Runs the supplied process action(s) after `t' has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef runAfter t p = spawnLocal $ runTimer t p True --- | starts a timer that repeatedly sends the supplied message to the destination +-- | Starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from -- being sent in future, cancelTimer can be called. startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef @@ -77,7 +117,7 @@ startTimer t pid msg = periodically t (mkSender pid msg) periodically :: TimeInterval -> Process () -> Process TimerRef periodically t p = spawnLocal $ runTimer t p False --- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- | Resets a running timer. Note: Cancelling a timer does not guarantee that -- a timer's messages are prevented from being delivered to the target process. -- Also note that resetting an ongoing timer (started using the `startTimer' or -- `periodically' functions) will only cause the current elapsed period to time @@ -89,7 +129,7 @@ resetTimer = (flip send) Reset cancelTimer :: TimerRef -> Process () cancelTimer = (flip send) Cancel --- | cancels a running timer and flushes any viable timer messages from the +-- | Cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () @@ -108,7 +148,7 @@ flushTimer ref ignore t = do , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +-- | Sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef ticker t pid = startTimer t pid Tick @@ -116,7 +156,7 @@ ticker t pid = startTimer t pid Tick -- Implementation -- -------------------------------------------------------------------------------- --- runs the timer process +-- Runs the timer process runTimer :: TimeInterval -> Process () -> Bool -> Process () runTimer t proc cancelOnReset = do cancel <- expectTimeout (intervalToMs t) @@ -129,7 +169,7 @@ runTimer t proc cancelOnReset = do where runProc True = proc runProc False = proc >> runTimer t proc cancelOnReset --- create a 'sender' action for dispatching `msg' to `pid' +-- Create a 'sender' action for dispatching `msg' to `pid' mkSender :: (Serializable a) => ProcessId -> a -> Process () mkSender pid msg = do -- say "sending\n" diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs new file mode 100644 index 00000000..a6810bed --- /dev/null +++ b/tests/TestAsync.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module TestAsync where + +import Prelude hiding (catch) +import Data.Binary (Binary(..)) +import Data.Typeable (Typeable) +import Data.DeriveTH +import Control.Monad (forever) +import Data.Maybe (fromMaybe) +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + , withMVar + , tryTakeMVar + ) +-- import Control.Applicative ((<$>), (<*>), pure, (<|>)) +import qualified Network.Transport as NT (Transport) +import Network.Transport.TCP (TransportInternals) +import Control.Distributed.Process +import Control.Distributed.Platform +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable(Serializable) +import Control.Distributed.Platform.Async +import Control.Distributed.Platform + +import Test.HUnit (Assertion) +import Test.Framework (Test, testGroup) +import Test.Framework.Providers.HUnit (testCase) +import Test.HUnit.Base (assertBool) + +import TestUtils + +testAsyncPoll :: TestResult Bool -> Process () +testAsyncPoll = do + self <- getSelfPid + async $ do + return () + +tests :: LocalNode -> [Test] +tests localNode = [ + testGroup "Async Tests" [ + testCase "testAsyncPoll" + (delayedAssertion + "expected poll to return something useful" + localNode True testAsyncPoll) + ] + ] + +asyncTests :: NT.Transport -> TransportInternals -> IO [Test] +asyncTests transport _ = do + localNode <- newLocalNode transport initRemoteTable + let testData = tests localNode + return testData + diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 06ad8584..25fe40ea 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -9,7 +9,7 @@ module Main where import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP -import TestGenServer (genServerTests) +-- import TestGenServer (genServerTests) import TestTimer (timerTests) tests :: NT.Transport -> TransportInternals -> IO [Test] From 8ef0b3a2419aa7db34ba35ebaf7eacf27242d44e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 11 Dec 2012 20:50:16 +0000 Subject: [PATCH 0460/2357] async actions use proxy to obtain results; monitoring is performed only in the proxy; no stray message are ever delivered to the caller's process; async actions are described by a rich result type describing the various observable states they may be in --- distributed-process-platform.cabal | 8 +- src/Control/Distributed/Platform.hs | 61 ++--- src/Control/Distributed/Platform/Async.hs | 229 +++++++++++++----- src/Control/Distributed/Platform/GenServer.hs | 90 +++---- .../Distributed/Platform/Internal/Types.hs | 7 +- src/Control/Distributed/Platform/Timer.hs | 64 ++++- tests/TestMain.hs | 2 +- tests/TestUtils.hs | 78 ++++++ 8 files changed, 387 insertions(+), 152 deletions(-) create mode 100644 tests/TestUtils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..f76cc51a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -30,7 +30,8 @@ library exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async test-suite PlatformTests type: exitcode-stdio-1.0 @@ -54,7 +55,10 @@ test-suite PlatformTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: + Control.Distributed.Platform.Async, Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + TestAsync, + TestUtils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..6b95b2cf 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,53 +2,38 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- exported time interval handling milliseconds , seconds , minutes , hours , intervalToMs , timeToMs + -- timeouts and time interval types , Timeout(..) , TimeInterval , TimeUnit + , TimerRef + -- exported timer operations + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer ) where import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - +import Control.Distributed.Platform.Timer + ( milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer + , TimerRef + ) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0beadb1c..0379b92d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,57 +1,174 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE EmptyDataDecls #-} + +module Control.Distributed.Platform.Async where + +import Control.Concurrent.MVar +import Control.Distributed.Platform + ( sendAfter + , TimerRef + , TimeInterval() + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +-- | A reference to an asynchronous action +type AsyncRef = ProcessId + +-- | A reference to an asynchronous worker +type AsyncWorkerId = AsyncRef + +-- | A reference to an asynchronous "gatherer" +type AsyncGathererId = AsyncRef + +-- | A function that takes an @AsyncGathererId@ (to which replies should be +-- sent) and spawns an asynchronous (user defined) action, returning the +-- spawned actions @AsyncWorkerId@ in the @Process@ monad. +type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId + +type AsyncData a = MVar (AsyncResult a) + +-- | An asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +data Async a = Async AsyncRef AsyncRef (AsyncData a) + +-- | Represents the result of an asynchronous action, which can be in several +-- states at any given time. +data AsyncResult a = + AsyncDone a -- | a completed action and its result + | AsyncFailed DiedReason -- | a failed action and the failure reason + | AsyncCancelled -- | a cancelled action + | AsyncPending -- | a pending action (that is still running) + +-- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- operation in the @Process@ monad. +type AsyncCancel = AsyncRef -> Process () + +-- TODO: Document me please! +async :: (Serializable a) => SpawnAsync -> Process (Async a) +async spawnF = do + mv <- liftIO $ newEmptyMVar + (wpid, gpid) <- spawnWorkers spawnF mv + return (Async wpid gpid mv) where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] + spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers sp ad = do + root <- getSelfPid + + -- listener/response proxy + gpid <- spawnLocal $ do + proxy <- getSelfPid + worker <- sp proxy + + send root worker + + monRef <- monitor worker + finally (pollUntilExit worker monRef ad) (unmonitor monRef) + + wpid <- expect + return (wpid, gpid) + + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit pid ref ad = do + r <- receiveWait [ + matchIf + (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid == pid') + (\(ProcessMonitorNotification _ _ r) -> return (Right r)) + , match (\x -> return (Left x)) + ] + case r of + Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] + Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left a -> liftIO $ putMVar ad (AsyncDone a) + +-- note [recursion] +-- We recurse *just once* if we've seen a normal exit from worker. We're +-- absolutely sure about this, because once we've seen DiedNormal for the +-- monitored process, it's not possible to see another monitor signal for it. +-- Based on this, the only other kinds of message that can arrive are the +-- return value from the worker or a cancellation from the coordinating process. + +-- | Check whether an @Async@ has completed yet. The status of the asynchronous +-- action is encoded in the returned @AsyncResult@. If not, the result is +-- @AsyncPending@, or one of the other constructors otherwise. +-- See @Async@. +poll :: (Serializable a) => Async a -> Process (AsyncResult a) +poll (Async _ _ d) = do + mv <- liftIO $ tryTakeMVar d + case mv of + Nothing -> return AsyncPending + Just v -> return v + +-- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if no result is available within the specified delay. +waitTimeout :: (Serializable a) => TimeInterval -> + Async a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = do + self <- getSelfPid + ar <- poll hAsync + case ar of + AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync + _ -> return (Just ar) + where + waitOnMailBox :: (Serializable a) => TimeInterval -> + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitOnMailBox t' a ref = do + m <- receiveTimeout 0 [ + match (\CancelWait -> return AsyncPending) + ] + -- TODO: this is pretty disgusting - sprinkle with applicative or some such + case m of + Nothing -> do + r <- check a + case r of + -- this isn't tail recursive, so we're likely to overflow fast + Nothing -> waitOnMailBox t' a ref + Just _ -> return r + Just _ -> + return m + +-- | Cancel an asynchronous operation. The cancellation method to be used +-- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or +-- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the +-- same way that message passing is asynchronous, whilst the former will block +-- until a @ProcessMonitorNotification@ is received for all participants in the +-- @Async@ action. +cancel :: Async a -> AsyncCancel -> Process () +cancel (Async w g d) asyncCancel = do + asyncCancel w + asyncCancel g + liftIO $ tryPutMVar d AsyncCancelled >> return () + +-- | Given an @AsyncRef@, will kill the associated process. This call returns +-- immediately. +cancelAsync :: AsyncCancel +cancelAsync = (flip kill) "cancelled" + +-- | Given an @AsyncRef@, will kill the associated process and block until +-- a @ProcessMonitorNotification@ is received, confirming that the process has +-- indeed died. Passing an @AsyncRef@ for a process that has already died is +-- not an error and will not block, so long as the monitor implementation +-- continues to support this. +cancelWait :: AsyncCancel +cancelWait pid = do + ref <- monitor pid + cancelAsync pid + receiveWait [ + matchIf (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid' == pid) + (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () + \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b8b91710..5271e42c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,17 +28,12 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - Async(), - call, - callTimeout, - callAsync, - wait, - waitTimeout, + startServer, + startServerLink, + startServerMonitor, + callServer, + castServer, + stopServer, Process, trace ) where @@ -68,14 +63,12 @@ import Control.Distributed.Process (AbstractMessage, receiveWait, say, send, spawnLocal, ProcessMonitorNotification(..)) - import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform -import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) import Data.DeriveTH import Data.Typeable (Typeable) @@ -237,8 +230,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc +startServer :: s -> LocalServer s -> Process ServerId +startServer s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -246,46 +239,59 @@ start s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls ref <- monitor pid return (pid, ref) --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do - a1 <- callAsync sid rq - waitTimeout a1 timeout - --- | Async call to a server -callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = async sid $ do - cid <- getSelfPid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) +-- | Call a server identified by it's ServerId +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +callServer sid timeout rq = do + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do +castServer :: (Serializable a) => ServerId -> a -> Process () +castServer sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs index a0721940..4df92c9e 100644 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -14,7 +14,8 @@ module Control.Distributed.Platform.Internal.Types ( TimeUnit(..) , TimeInterval(..) - , Timeout(..) + , Timeout(..) + , CancelWait(..) ) where import Data.Binary @@ -36,3 +37,7 @@ $(derive makeBinary ''TimeInterval) data Timeout = Timeout TimeInterval | Infinity deriving (Typeable, Show) $(derive makeBinary ''Timeout) + +data CancelWait = CancelWait + deriving (Typeable) +$(derive makeBinary ''CancelWait) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 036a582c..1105eeee 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -5,6 +5,12 @@ module Control.Distributed.Platform.Timer ( TimerRef , Tick(Tick) + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs , sleep , sendAfter , runAfter @@ -19,11 +25,10 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) +import Data.Typeable (Typeable) +import Prelude hiding (init) -- | an opaque reference to a timer type TimerRef = ProcessId @@ -46,6 +51,40 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- process implementations + -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -58,16 +97,17 @@ sleep t = do (\_ -> return ())] return () --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. +-- | Starts a timer which sends the supplied message to the destination process +-- after the specified time interval. The message is sent only once, after +-- which the timer exits normally. sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef sendAfter t pid msg = runAfter t (mkSender pid msg) --- | runs the supplied process action(s) after `t' has elapsed +-- | Runs the supplied process action(s) after `t' has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef runAfter t p = spawnLocal $ runTimer t p True --- | starts a timer that repeatedly sends the supplied message to the destination +-- | Starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from -- being sent in future, cancelTimer can be called. startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef @@ -77,7 +117,7 @@ startTimer t pid msg = periodically t (mkSender pid msg) periodically :: TimeInterval -> Process () -> Process TimerRef periodically t p = spawnLocal $ runTimer t p False --- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- | Resets a running timer. Note: Cancelling a timer does not guarantee that -- a timer's messages are prevented from being delivered to the target process. -- Also note that resetting an ongoing timer (started using the `startTimer' or -- `periodically' functions) will only cause the current elapsed period to time @@ -89,7 +129,7 @@ resetTimer = (flip send) Reset cancelTimer :: TimerRef -> Process () cancelTimer = (flip send) Cancel --- | cancels a running timer and flushes any viable timer messages from the +-- | Cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () @@ -108,7 +148,7 @@ flushTimer ref ignore t = do , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +-- | Sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef ticker t pid = startTimer t pid Tick @@ -116,7 +156,7 @@ ticker t pid = startTimer t pid Tick -- Implementation -- -------------------------------------------------------------------------------- --- runs the timer process +-- Runs the timer process runTimer :: TimeInterval -> Process () -> Bool -> Process () runTimer t proc cancelOnReset = do cancel <- expectTimeout (intervalToMs t) @@ -129,7 +169,7 @@ runTimer t proc cancelOnReset = do where runProc True = proc runProc False = proc >> runTimer t proc cancelOnReset --- create a 'sender' action for dispatching `msg' to `pid' +-- Create a 'sender' action for dispatching `msg' to `pid' mkSender :: (Serializable a) => ProcessId -> a -> Process () mkSender pid msg = do -- say "sending\n" diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 06ad8584..25fe40ea 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -9,7 +9,7 @@ module Main where import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP -import TestGenServer (genServerTests) +-- import TestGenServer (genServerTests) import TestTimer (timerTests) tests :: NT.Transport -> TransportInternals -> IO [Test] diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs new file mode 100644 index 00000000..209fe8e9 --- /dev/null +++ b/tests/TestUtils.hs @@ -0,0 +1,78 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module TestUtils + ( TestResult + , TestProcessControl + , Ping(Ping) + , startTestProcess + , delayedAssertion + , assertComplete + , noop + , stash + ) where + +import Prelude hiding (catch) +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + ) +import Control.Distributed.Process +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() + +import Control.Monad (forever) + +import Test.HUnit (Assertion) +import Test.HUnit.Base (assertBool) + +type TestResult a = MVar a + +data Ping = Ping + deriving (Typeable) +$(derive makeBinary ''Ping) + +data TestProcessControl = Stop | Go | Report ProcessId + deriving (Typeable) +$(derive makeBinary ''TestProcessControl) + +startTestProcess :: Process () -> Process ProcessId +startTestProcess proc = spawnLocal $ testProcess proc + +testProcess :: Process () -> Process () +testProcess proc = forever $ do + ctl <- expect + case ctl of + Stop -> terminate + Go -> proc + Report p -> acquireAndRespond p + where acquireAndRespond :: ProcessId -> Process () + acquireAndRespond p = do + _ <- receiveWait [ + matchAny (\m -> forward m p) + ] + return () + +delayedAssertion :: (Eq a) => String -> LocalNode -> a -> + (TestResult a -> Process ()) -> Assertion +delayedAssertion note localNode expected testProc = do + result <- newEmptyMVar + _ <- forkProcess localNode $ testProc result + assertComplete note result expected + +assertComplete :: (Eq a) => String -> MVar a -> a -> IO () +assertComplete msg mv a = do + b <- takeMVar mv + assertBool msg (a == b) + +noop :: Process () +noop = return () + +stash :: TestResult a -> a -> Process () +stash mvar x = liftIO $ putMVar mvar x + From 6e0938337a276fc2b7f5d8fb9418342ffa5c4b67 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 11 Dec 2012 20:50:16 +0000 Subject: [PATCH 0461/2357] async actions use proxy to obtain results; monitoring is performed only in the proxy; no stray message are ever delivered to the caller's process; async actions are described by a rich result type describing the various observable states they may be in --- distributed-process-platform.cabal | 8 +- src/Control/Distributed/Platform.hs | 61 ++--- src/Control/Distributed/Platform/Async.hs | 229 +++++++++++++----- src/Control/Distributed/Platform/GenServer.hs | 90 +++---- .../Distributed/Platform/Internal/Types.hs | 7 +- src/Control/Distributed/Platform/Timer.hs | 64 ++++- tests/TestMain.hs | 2 +- tests/TestUtils.hs | 78 ++++++ 8 files changed, 387 insertions(+), 152 deletions(-) create mode 100644 tests/TestUtils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..f76cc51a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -30,7 +30,8 @@ library exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async test-suite PlatformTests type: exitcode-stdio-1.0 @@ -54,7 +55,10 @@ test-suite PlatformTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: + Control.Distributed.Platform.Async, Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + TestAsync, + TestUtils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..6b95b2cf 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,53 +2,38 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- exported time interval handling milliseconds , seconds , minutes , hours , intervalToMs , timeToMs + -- timeouts and time interval types , Timeout(..) , TimeInterval , TimeUnit + , TimerRef + -- exported timer operations + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer ) where import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - +import Control.Distributed.Platform.Timer + ( milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer + , TimerRef + ) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0beadb1c..0379b92d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,57 +1,174 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE EmptyDataDecls #-} + +module Control.Distributed.Platform.Async where + +import Control.Concurrent.MVar +import Control.Distributed.Platform + ( sendAfter + , TimerRef + , TimeInterval() + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +-- | A reference to an asynchronous action +type AsyncRef = ProcessId + +-- | A reference to an asynchronous worker +type AsyncWorkerId = AsyncRef + +-- | A reference to an asynchronous "gatherer" +type AsyncGathererId = AsyncRef + +-- | A function that takes an @AsyncGathererId@ (to which replies should be +-- sent) and spawns an asynchronous (user defined) action, returning the +-- spawned actions @AsyncWorkerId@ in the @Process@ monad. +type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId + +type AsyncData a = MVar (AsyncResult a) + +-- | An asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +data Async a = Async AsyncRef AsyncRef (AsyncData a) + +-- | Represents the result of an asynchronous action, which can be in several +-- states at any given time. +data AsyncResult a = + AsyncDone a -- | a completed action and its result + | AsyncFailed DiedReason -- | a failed action and the failure reason + | AsyncCancelled -- | a cancelled action + | AsyncPending -- | a pending action (that is still running) + +-- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- operation in the @Process@ monad. +type AsyncCancel = AsyncRef -> Process () + +-- TODO: Document me please! +async :: (Serializable a) => SpawnAsync -> Process (Async a) +async spawnF = do + mv <- liftIO $ newEmptyMVar + (wpid, gpid) <- spawnWorkers spawnF mv + return (Async wpid gpid mv) where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] + spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers sp ad = do + root <- getSelfPid + + -- listener/response proxy + gpid <- spawnLocal $ do + proxy <- getSelfPid + worker <- sp proxy + + send root worker + + monRef <- monitor worker + finally (pollUntilExit worker monRef ad) (unmonitor monRef) + + wpid <- expect + return (wpid, gpid) + + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit pid ref ad = do + r <- receiveWait [ + matchIf + (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid == pid') + (\(ProcessMonitorNotification _ _ r) -> return (Right r)) + , match (\x -> return (Left x)) + ] + case r of + Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] + Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left a -> liftIO $ putMVar ad (AsyncDone a) + +-- note [recursion] +-- We recurse *just once* if we've seen a normal exit from worker. We're +-- absolutely sure about this, because once we've seen DiedNormal for the +-- monitored process, it's not possible to see another monitor signal for it. +-- Based on this, the only other kinds of message that can arrive are the +-- return value from the worker or a cancellation from the coordinating process. + +-- | Check whether an @Async@ has completed yet. The status of the asynchronous +-- action is encoded in the returned @AsyncResult@. If not, the result is +-- @AsyncPending@, or one of the other constructors otherwise. +-- See @Async@. +poll :: (Serializable a) => Async a -> Process (AsyncResult a) +poll (Async _ _ d) = do + mv <- liftIO $ tryTakeMVar d + case mv of + Nothing -> return AsyncPending + Just v -> return v + +-- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if no result is available within the specified delay. +waitTimeout :: (Serializable a) => TimeInterval -> + Async a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = do + self <- getSelfPid + ar <- poll hAsync + case ar of + AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync + _ -> return (Just ar) + where + waitOnMailBox :: (Serializable a) => TimeInterval -> + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitOnMailBox t' a ref = do + m <- receiveTimeout 0 [ + match (\CancelWait -> return AsyncPending) + ] + -- TODO: this is pretty disgusting - sprinkle with applicative or some such + case m of + Nothing -> do + r <- check a + case r of + -- this isn't tail recursive, so we're likely to overflow fast + Nothing -> waitOnMailBox t' a ref + Just _ -> return r + Just _ -> + return m + +-- | Cancel an asynchronous operation. The cancellation method to be used +-- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or +-- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the +-- same way that message passing is asynchronous, whilst the former will block +-- until a @ProcessMonitorNotification@ is received for all participants in the +-- @Async@ action. +cancel :: Async a -> AsyncCancel -> Process () +cancel (Async w g d) asyncCancel = do + asyncCancel w + asyncCancel g + liftIO $ tryPutMVar d AsyncCancelled >> return () + +-- | Given an @AsyncRef@, will kill the associated process. This call returns +-- immediately. +cancelAsync :: AsyncCancel +cancelAsync = (flip kill) "cancelled" + +-- | Given an @AsyncRef@, will kill the associated process and block until +-- a @ProcessMonitorNotification@ is received, confirming that the process has +-- indeed died. Passing an @AsyncRef@ for a process that has already died is +-- not an error and will not block, so long as the monitor implementation +-- continues to support this. +cancelWait :: AsyncCancel +cancelWait pid = do + ref <- monitor pid + cancelAsync pid + receiveWait [ + matchIf (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid' == pid) + (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () + \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b8b91710..5271e42c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,17 +28,12 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - Async(), - call, - callTimeout, - callAsync, - wait, - waitTimeout, + startServer, + startServerLink, + startServerMonitor, + callServer, + castServer, + stopServer, Process, trace ) where @@ -68,14 +63,12 @@ import Control.Distributed.Process (AbstractMessage, receiveWait, say, send, spawnLocal, ProcessMonitorNotification(..)) - import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform -import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) import Data.DeriveTH import Data.Typeable (Typeable) @@ -237,8 +230,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc +startServer :: s -> LocalServer s -> Process ServerId +startServer s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -246,46 +239,59 @@ start s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls ref <- monitor pid return (pid, ref) --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do - a1 <- callAsync sid rq - waitTimeout a1 timeout - --- | Async call to a server -callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = async sid $ do - cid <- getSelfPid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) +-- | Call a server identified by it's ServerId +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +callServer sid timeout rq = do + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do +castServer :: (Serializable a) => ServerId -> a -> Process () +castServer sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs index a0721940..4df92c9e 100644 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -14,7 +14,8 @@ module Control.Distributed.Platform.Internal.Types ( TimeUnit(..) , TimeInterval(..) - , Timeout(..) + , Timeout(..) + , CancelWait(..) ) where import Data.Binary @@ -36,3 +37,7 @@ $(derive makeBinary ''TimeInterval) data Timeout = Timeout TimeInterval | Infinity deriving (Typeable, Show) $(derive makeBinary ''Timeout) + +data CancelWait = CancelWait + deriving (Typeable) +$(derive makeBinary ''CancelWait) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 036a582c..1105eeee 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -5,6 +5,12 @@ module Control.Distributed.Platform.Timer ( TimerRef , Tick(Tick) + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs , sleep , sendAfter , runAfter @@ -19,11 +25,10 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) +import Data.Typeable (Typeable) +import Prelude hiding (init) -- | an opaque reference to a timer type TimerRef = ProcessId @@ -46,6 +51,40 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- process implementations + -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -58,16 +97,17 @@ sleep t = do (\_ -> return ())] return () --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. +-- | Starts a timer which sends the supplied message to the destination process +-- after the specified time interval. The message is sent only once, after +-- which the timer exits normally. sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef sendAfter t pid msg = runAfter t (mkSender pid msg) --- | runs the supplied process action(s) after `t' has elapsed +-- | Runs the supplied process action(s) after `t' has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef runAfter t p = spawnLocal $ runTimer t p True --- | starts a timer that repeatedly sends the supplied message to the destination +-- | Starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from -- being sent in future, cancelTimer can be called. startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef @@ -77,7 +117,7 @@ startTimer t pid msg = periodically t (mkSender pid msg) periodically :: TimeInterval -> Process () -> Process TimerRef periodically t p = spawnLocal $ runTimer t p False --- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- | Resets a running timer. Note: Cancelling a timer does not guarantee that -- a timer's messages are prevented from being delivered to the target process. -- Also note that resetting an ongoing timer (started using the `startTimer' or -- `periodically' functions) will only cause the current elapsed period to time @@ -89,7 +129,7 @@ resetTimer = (flip send) Reset cancelTimer :: TimerRef -> Process () cancelTimer = (flip send) Cancel --- | cancels a running timer and flushes any viable timer messages from the +-- | Cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () @@ -108,7 +148,7 @@ flushTimer ref ignore t = do , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +-- | Sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef ticker t pid = startTimer t pid Tick @@ -116,7 +156,7 @@ ticker t pid = startTimer t pid Tick -- Implementation -- -------------------------------------------------------------------------------- --- runs the timer process +-- Runs the timer process runTimer :: TimeInterval -> Process () -> Bool -> Process () runTimer t proc cancelOnReset = do cancel <- expectTimeout (intervalToMs t) @@ -129,7 +169,7 @@ runTimer t proc cancelOnReset = do where runProc True = proc runProc False = proc >> runTimer t proc cancelOnReset --- create a 'sender' action for dispatching `msg' to `pid' +-- Create a 'sender' action for dispatching `msg' to `pid' mkSender :: (Serializable a) => ProcessId -> a -> Process () mkSender pid msg = do -- say "sending\n" diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 06ad8584..25fe40ea 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -9,7 +9,7 @@ module Main where import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT import Network.Transport.TCP -import TestGenServer (genServerTests) +-- import TestGenServer (genServerTests) import TestTimer (timerTests) tests :: NT.Transport -> TransportInternals -> IO [Test] diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs new file mode 100644 index 00000000..209fe8e9 --- /dev/null +++ b/tests/TestUtils.hs @@ -0,0 +1,78 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module TestUtils + ( TestResult + , TestProcessControl + , Ping(Ping) + , startTestProcess + , delayedAssertion + , assertComplete + , noop + , stash + ) where + +import Prelude hiding (catch) +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + ) +import Control.Distributed.Process +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() + +import Control.Monad (forever) + +import Test.HUnit (Assertion) +import Test.HUnit.Base (assertBool) + +type TestResult a = MVar a + +data Ping = Ping + deriving (Typeable) +$(derive makeBinary ''Ping) + +data TestProcessControl = Stop | Go | Report ProcessId + deriving (Typeable) +$(derive makeBinary ''TestProcessControl) + +startTestProcess :: Process () -> Process ProcessId +startTestProcess proc = spawnLocal $ testProcess proc + +testProcess :: Process () -> Process () +testProcess proc = forever $ do + ctl <- expect + case ctl of + Stop -> terminate + Go -> proc + Report p -> acquireAndRespond p + where acquireAndRespond :: ProcessId -> Process () + acquireAndRespond p = do + _ <- receiveWait [ + matchAny (\m -> forward m p) + ] + return () + +delayedAssertion :: (Eq a) => String -> LocalNode -> a -> + (TestResult a -> Process ()) -> Assertion +delayedAssertion note localNode expected testProc = do + result <- newEmptyMVar + _ <- forkProcess localNode $ testProc result + assertComplete note result expected + +assertComplete :: (Eq a) => String -> MVar a -> a -> IO () +assertComplete msg mv a = do + b <- takeMVar mv + assertBool msg (a == b) + +noop :: Process () +noop = return () + +stash :: TestResult a -> a -> Process () +stash mvar x = liftIO $ putMVar mvar x + From 7e367fcae659d9965bff48cd7233473bf980bdae Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 11 Dec 2012 20:50:16 +0000 Subject: [PATCH 0462/2357] async actions use proxy to obtain results; monitoring is performed only in the proxy; no stray message are ever delivered to the caller's process; async actions are described by a rich result type describing the various observable states they may be in --- distributed-process-platform.cabal | 8 +- src/Control/Distributed/Platform.hs | 61 ++--- src/Control/Distributed/Platform/Async.hs | 229 +++++++++++++----- src/Control/Distributed/Platform/GenServer.hs | 90 +++---- .../Distributed/Platform/Internal/Types.hs | 7 +- src/Control/Distributed/Platform/Timer.hs | 64 ++++- 6 files changed, 308 insertions(+), 151 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..f76cc51a 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -30,7 +30,8 @@ library exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async test-suite PlatformTests type: exitcode-stdio-1.0 @@ -54,7 +55,10 @@ test-suite PlatformTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: + Control.Distributed.Platform.Async, Control.Distributed.Platform.Timer, - Control.Distributed.Platform + Control.Distributed.Platform, + TestAsync, + TestUtils extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..6b95b2cf 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,53 +2,38 @@ -- module Control.Distributed.Platform ( - -- time interval handling + -- exported time interval handling milliseconds , seconds , minutes , hours , intervalToMs , timeToMs + -- timeouts and time interval types , Timeout(..) , TimeInterval , TimeUnit + , TimerRef + -- exported timer operations + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer ) where import Control.Distributed.Platform.Internal.Types - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - +import Control.Distributed.Platform.Timer + ( milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , sleep + , sendAfter + , startTimer + , resetTimer + , cancelTimer + , TimerRef + ) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0beadb1c..0379b92d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,57 +1,174 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE EmptyDataDecls #-} + +module Control.Distributed.Platform.Async where + +import Control.Concurrent.MVar +import Control.Distributed.Platform + ( sendAfter + , TimerRef + , TimeInterval() + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +-- | A reference to an asynchronous action +type AsyncRef = ProcessId + +-- | A reference to an asynchronous worker +type AsyncWorkerId = AsyncRef + +-- | A reference to an asynchronous "gatherer" +type AsyncGathererId = AsyncRef + +-- | A function that takes an @AsyncGathererId@ (to which replies should be +-- sent) and spawns an asynchronous (user defined) action, returning the +-- spawned actions @AsyncWorkerId@ in the @Process@ monad. +type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId + +type AsyncData a = MVar (AsyncResult a) + +-- | An asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +data Async a = Async AsyncRef AsyncRef (AsyncData a) + +-- | Represents the result of an asynchronous action, which can be in several +-- states at any given time. +data AsyncResult a = + AsyncDone a -- | a completed action and its result + | AsyncFailed DiedReason -- | a failed action and the failure reason + | AsyncCancelled -- | a cancelled action + | AsyncPending -- | a pending action (that is still running) + +-- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- operation in the @Process@ monad. +type AsyncCancel = AsyncRef -> Process () + +-- TODO: Document me please! +async :: (Serializable a) => SpawnAsync -> Process (Async a) +async spawnF = do + mv <- liftIO $ newEmptyMVar + (wpid, gpid) <- spawnWorkers spawnF mv + return (Async wpid gpid mv) where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] + spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers sp ad = do + root <- getSelfPid + + -- listener/response proxy + gpid <- spawnLocal $ do + proxy <- getSelfPid + worker <- sp proxy + + send root worker + + monRef <- monitor worker + finally (pollUntilExit worker monRef ad) (unmonitor monRef) + + wpid <- expect + return (wpid, gpid) + + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit pid ref ad = do + r <- receiveWait [ + matchIf + (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid == pid') + (\(ProcessMonitorNotification _ _ r) -> return (Right r)) + , match (\x -> return (Left x)) + ] + case r of + Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] + Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left a -> liftIO $ putMVar ad (AsyncDone a) + +-- note [recursion] +-- We recurse *just once* if we've seen a normal exit from worker. We're +-- absolutely sure about this, because once we've seen DiedNormal for the +-- monitored process, it's not possible to see another monitor signal for it. +-- Based on this, the only other kinds of message that can arrive are the +-- return value from the worker or a cancellation from the coordinating process. + +-- | Check whether an @Async@ has completed yet. The status of the asynchronous +-- action is encoded in the returned @AsyncResult@. If not, the result is +-- @AsyncPending@, or one of the other constructors otherwise. +-- See @Async@. +poll :: (Serializable a) => Async a -> Process (AsyncResult a) +poll (Async _ _ d) = do + mv <- liftIO $ tryTakeMVar d + case mv of + Nothing -> return AsyncPending + Just v -> return v + +-- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if no result is available within the specified delay. +waitTimeout :: (Serializable a) => TimeInterval -> + Async a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = do + self <- getSelfPid + ar <- poll hAsync + case ar of + AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync + _ -> return (Just ar) + where + waitOnMailBox :: (Serializable a) => TimeInterval -> + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitOnMailBox t' a ref = do + m <- receiveTimeout 0 [ + match (\CancelWait -> return AsyncPending) + ] + -- TODO: this is pretty disgusting - sprinkle with applicative or some such + case m of + Nothing -> do + r <- check a + case r of + -- this isn't tail recursive, so we're likely to overflow fast + Nothing -> waitOnMailBox t' a ref + Just _ -> return r + Just _ -> + return m + +-- | Cancel an asynchronous operation. The cancellation method to be used +-- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or +-- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the +-- same way that message passing is asynchronous, whilst the former will block +-- until a @ProcessMonitorNotification@ is received for all participants in the +-- @Async@ action. +cancel :: Async a -> AsyncCancel -> Process () +cancel (Async w g d) asyncCancel = do + asyncCancel w + asyncCancel g + liftIO $ tryPutMVar d AsyncCancelled >> return () + +-- | Given an @AsyncRef@, will kill the associated process. This call returns +-- immediately. +cancelAsync :: AsyncCancel +cancelAsync = (flip kill) "cancelled" + +-- | Given an @AsyncRef@, will kill the associated process and block until +-- a @ProcessMonitorNotification@ is received, confirming that the process has +-- indeed died. Passing an @AsyncRef@ for a process that has already died is +-- not an error and will not block, so long as the monitor implementation +-- continues to support this. +cancelWait :: AsyncCancel +cancelWait pid = do + ref <- monitor pid + cancelAsync pid + receiveWait [ + matchIf (\(ProcessMonitorNotification ref' pid' _) -> + ref' == ref && pid' == pid) + (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () + \ No newline at end of file diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs index b8b91710..5271e42c 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Platform/GenServer.hs @@ -28,17 +28,12 @@ module Control.Distributed.Platform.GenServer ( modifyState, LocalServer(..), defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - Async(), - call, - callTimeout, - callAsync, - wait, - waitTimeout, + startServer, + startServerLink, + startServerMonitor, + callServer, + castServer, + stopServer, Process, trace ) where @@ -68,14 +63,12 @@ import Control.Distributed.Process (AbstractMessage, receiveWait, say, send, spawnLocal, ProcessMonitorNotification(..)) - import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Platform.Internal.Types import Control.Distributed.Platform -import Control.Distributed.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) import Data.DeriveTH import Data.Typeable (Typeable) @@ -237,8 +230,8 @@ defaultServer = LocalServer { -------------------------------------------------------------------------------- -- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc +startServer :: s -> LocalServer s -> Process ServerId +startServer s ls = spawnLocal proc where proc = processServer initH terminateH hs s initH = initHandler ls @@ -246,46 +239,59 @@ start s ls = spawnLocal proc hs = handlers ls -- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls +startServerLink :: s -> LocalServer s -> Process ServerId +startServerLink s ls = do + pid <- startServer s ls link pid return pid -- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls +startServerMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) +startServerMonitor s ls = do + pid <- startServer s ls ref <- monitor pid return (pid, ref) --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do - a1 <- callAsync sid rq - waitTimeout a1 timeout - --- | Async call to a server -callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = async sid $ do - cid <- getSelfPid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) +-- | Call a server identified by it's ServerId +callServer :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process rs +callServer sid timeout rq = do + cid <- getSelfPid + ref <- monitor sid + finally (doCall cid) (unmonitor ref) + where + doCall cid = do + --say $ "Calling server " ++ show cid ++ " - " ++ show rq + send sid (CallMessage cid rq) + case timeout of + Infinity -> do + receiveWait [matchDied, matchResponse] + Timeout t -> do + mayResp <- receiveTimeout (intervalToMs t) [matchDied, matchResponse] + case mayResp of + Just resp -> return resp + Nothing -> error $ "timeout! value = " ++ show t + + matchResponse = match (\resp -> do + --say $ "Matched: " ++ show resp + return resp) + + matchDied = match (\(ProcessMonitorNotification _ _ reason) -> do + --say $ "Matched: " ++ show n + mayResp <- expectTimeout 0 + case mayResp of + Just resp -> return resp + Nothing -> error $ "Server died: " ++ show reason) -- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do +castServer :: (Serializable a) => ServerId -> a -> Process () +castServer sid msg = do cid <- getSelfPid --say $ "Casting server " ++ show cid send sid (CastMessage cid msg) -- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do +stopServer :: Serializable a => ServerId -> a -> Process () +stopServer sid reason = do --say $ "Stop server " ++ show sid exit sid reason diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs index a0721940..4df92c9e 100644 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Platform/Internal/Types.hs @@ -14,7 +14,8 @@ module Control.Distributed.Platform.Internal.Types ( TimeUnit(..) , TimeInterval(..) - , Timeout(..) + , Timeout(..) + , CancelWait(..) ) where import Data.Binary @@ -36,3 +37,7 @@ $(derive makeBinary ''TimeInterval) data Timeout = Timeout TimeInterval | Infinity deriving (Typeable, Show) $(derive makeBinary ''Timeout) + +data CancelWait = CancelWait + deriving (Typeable) +$(derive makeBinary ''CancelWait) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 036a582c..1105eeee 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -5,6 +5,12 @@ module Control.Distributed.Platform.Timer ( TimerRef , Tick(Tick) + , milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs , sleep , sendAfter , runAfter @@ -19,11 +25,10 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) +import Data.Typeable (Typeable) +import Prelude hiding (init) -- | an opaque reference to a timer type TimerRef = ProcessId @@ -46,6 +51,40 @@ $(derive makeBinary ''SleepingPill) -- API -- -------------------------------------------------------------------------------- +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we need to scale it up to +-- deal with days, months, years, etc + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 + +-- process implementations + -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards @@ -58,16 +97,17 @@ sleep t = do (\_ -> return ())] return () --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. +-- | Starts a timer which sends the supplied message to the destination process +-- after the specified time interval. The message is sent only once, after +-- which the timer exits normally. sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef sendAfter t pid msg = runAfter t (mkSender pid msg) --- | runs the supplied process action(s) after `t' has elapsed +-- | Runs the supplied process action(s) after `t' has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef runAfter t p = spawnLocal $ runTimer t p True --- | starts a timer that repeatedly sends the supplied message to the destination +-- | Starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from -- being sent in future, cancelTimer can be called. startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef @@ -77,7 +117,7 @@ startTimer t pid msg = periodically t (mkSender pid msg) periodically :: TimeInterval -> Process () -> Process TimerRef periodically t p = spawnLocal $ runTimer t p False --- | resets a running timer. Note: Cancelling a timer does not guarantee that +-- | Resets a running timer. Note: Cancelling a timer does not guarantee that -- a timer's messages are prevented from being delivered to the target process. -- Also note that resetting an ongoing timer (started using the `startTimer' or -- `periodically' functions) will only cause the current elapsed period to time @@ -89,7 +129,7 @@ resetTimer = (flip send) Reset cancelTimer :: TimerRef -> Process () cancelTimer = (flip send) Cancel --- | cancels a running timer and flushes any viable timer messages from the +-- | Cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () @@ -108,7 +148,7 @@ flushTimer ref ignore t = do , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +-- | Sets up a timer that sends `Tick' repeatedly at intervals of `t' ticker :: TimeInterval -> ProcessId -> Process TimerRef ticker t pid = startTimer t pid Tick @@ -116,7 +156,7 @@ ticker t pid = startTimer t pid Tick -- Implementation -- -------------------------------------------------------------------------------- --- runs the timer process +-- Runs the timer process runTimer :: TimeInterval -> Process () -> Bool -> Process () runTimer t proc cancelOnReset = do cancel <- expectTimeout (intervalToMs t) @@ -129,7 +169,7 @@ runTimer t proc cancelOnReset = do where runProc True = proc runProc False = proc >> runTimer t proc cancelOnReset --- create a 'sender' action for dispatching `msg' to `pid' +-- Create a 'sender' action for dispatching `msg' to `pid' mkSender :: (Serializable a) => ProcessId -> a -> Process () mkSender pid msg = do -- say "sending\n" From 897873c46f00ccbec1130c2d248a566eeac01052 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:25 +0000 Subject: [PATCH 0463/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 27 +++++++++++++++-------- src/Control/Distributed/Platform/Timer.hs | 15 +++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0379b92d..b3c00086 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -16,6 +16,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + -- | A reference to an asynchronous action type AsyncRef = ProcessId @@ -25,9 +29,9 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an @AsyncGathererId@ (to which replies should be +-- | A function that takes an 'AsyncGathererId' (to which replies should be -- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions @AsyncWorkerId@ in the @Process@ monad. +-- spawned actions 'AsyncWorkerId' in the @Process@ monad. type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId type AsyncData a = MVar (AsyncResult a) @@ -46,11 +50,16 @@ data AsyncResult a = | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) --- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. type AsyncCancel = AsyncRef -> Process () --- TODO: Document me please! +-- | An asynchronous action spawned by 'async' or 'withAsync'. +-- Asynchronous actions are executed in a separate @Process@, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- There is currently a contract between async workers and async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar @@ -96,10 +105,10 @@ async spawnF = do -- Based on this, the only other kinds of message that can arrive are the -- return value from the worker or a cancellation from the coordinating process. --- | Check whether an @Async@ has completed yet. The status of the asynchronous --- action is encoded in the returned @AsyncResult@. If not, the result is --- @AsyncPending@, or one of the other constructors otherwise. --- See @Async@. +-- | Check whether an 'Async' has completed yet. The status of the asynchronous +-- action is encoded in the returned 'AsyncResult', If not, the result is +-- 'AsyncPending', or one of the other constructors otherwise. +-- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do mv <- liftIO $ tryTakeMVar d @@ -107,7 +116,7 @@ poll (Async _ _ d) = do Nothing -> return AsyncPending Just v -> return v --- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 1105eeee..513ee531 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -30,7 +30,11 @@ import Data.DeriveTH import Data.Typeable (Typeable) import Prelude hiding (init) --- | an opaque reference to a timer +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | An opaque reference to a timer. type TimerRef = ProcessId -- | cancellation message sent to timers @@ -38,19 +42,16 @@ data TimerConfig = Reset | Cancel deriving (Typeable, Show) $(derive makeBinary ''TimerConfig) --- | represents a 'tick' event that timers can generate +-- | Represents a 'tick' event that timers can generate data Tick = Tick deriving (Typeable, Eq) $(derive makeBinary ''Tick) +-- | Private data type used to guarantee that 'sleep' blocks on receive. data SleepingPill = SleepingPill deriving (Typeable) $(derive makeBinary ''SleepingPill) --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - -- time interval/unit handling -- | converts the supplied @TimeInterval@ to milliseconds @@ -85,7 +86,7 @@ timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 -- process implementations --- | blocks the calling Process for the specified TimeInterval. Note that this +-- | Blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards -- scheduling) to differ from threadDelay and/or operating system specific From 929ad94958329ee026b6bbdd4c14672da8a3cf67 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:25 +0000 Subject: [PATCH 0464/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 27 +++++++++++++++-------- src/Control/Distributed/Platform/Timer.hs | 15 +++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0379b92d..b3c00086 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -16,6 +16,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + -- | A reference to an asynchronous action type AsyncRef = ProcessId @@ -25,9 +29,9 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an @AsyncGathererId@ (to which replies should be +-- | A function that takes an 'AsyncGathererId' (to which replies should be -- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions @AsyncWorkerId@ in the @Process@ monad. +-- spawned actions 'AsyncWorkerId' in the @Process@ monad. type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId type AsyncData a = MVar (AsyncResult a) @@ -46,11 +50,16 @@ data AsyncResult a = | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) --- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. type AsyncCancel = AsyncRef -> Process () --- TODO: Document me please! +-- | An asynchronous action spawned by 'async' or 'withAsync'. +-- Asynchronous actions are executed in a separate @Process@, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- There is currently a contract between async workers and async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar @@ -96,10 +105,10 @@ async spawnF = do -- Based on this, the only other kinds of message that can arrive are the -- return value from the worker or a cancellation from the coordinating process. --- | Check whether an @Async@ has completed yet. The status of the asynchronous --- action is encoded in the returned @AsyncResult@. If not, the result is --- @AsyncPending@, or one of the other constructors otherwise. --- See @Async@. +-- | Check whether an 'Async' has completed yet. The status of the asynchronous +-- action is encoded in the returned 'AsyncResult', If not, the result is +-- 'AsyncPending', or one of the other constructors otherwise. +-- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do mv <- liftIO $ tryTakeMVar d @@ -107,7 +116,7 @@ poll (Async _ _ d) = do Nothing -> return AsyncPending Just v -> return v --- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 1105eeee..513ee531 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -30,7 +30,11 @@ import Data.DeriveTH import Data.Typeable (Typeable) import Prelude hiding (init) --- | an opaque reference to a timer +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | An opaque reference to a timer. type TimerRef = ProcessId -- | cancellation message sent to timers @@ -38,19 +42,16 @@ data TimerConfig = Reset | Cancel deriving (Typeable, Show) $(derive makeBinary ''TimerConfig) --- | represents a 'tick' event that timers can generate +-- | Represents a 'tick' event that timers can generate data Tick = Tick deriving (Typeable, Eq) $(derive makeBinary ''Tick) +-- | Private data type used to guarantee that 'sleep' blocks on receive. data SleepingPill = SleepingPill deriving (Typeable) $(derive makeBinary ''SleepingPill) --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - -- time interval/unit handling -- | converts the supplied @TimeInterval@ to milliseconds @@ -85,7 +86,7 @@ timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 -- process implementations --- | blocks the calling Process for the specified TimeInterval. Note that this +-- | Blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards -- scheduling) to differ from threadDelay and/or operating system specific From 98781f57c67cdcbccc5cb77cb5c7aecb678b78b9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:25 +0000 Subject: [PATCH 0465/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 27 +++++++++++++++-------- src/Control/Distributed/Platform/Timer.hs | 15 +++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0379b92d..b3c00086 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -16,6 +16,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + -- | A reference to an asynchronous action type AsyncRef = ProcessId @@ -25,9 +29,9 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an @AsyncGathererId@ (to which replies should be +-- | A function that takes an 'AsyncGathererId' (to which replies should be -- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions @AsyncWorkerId@ in the @Process@ monad. +-- spawned actions 'AsyncWorkerId' in the @Process@ monad. type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId type AsyncData a = MVar (AsyncResult a) @@ -46,11 +50,16 @@ data AsyncResult a = | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) --- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. type AsyncCancel = AsyncRef -> Process () --- TODO: Document me please! +-- | An asynchronous action spawned by 'async' or 'withAsync'. +-- Asynchronous actions are executed in a separate @Process@, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- There is currently a contract between async workers and async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar @@ -96,10 +105,10 @@ async spawnF = do -- Based on this, the only other kinds of message that can arrive are the -- return value from the worker or a cancellation from the coordinating process. --- | Check whether an @Async@ has completed yet. The status of the asynchronous --- action is encoded in the returned @AsyncResult@. If not, the result is --- @AsyncPending@, or one of the other constructors otherwise. --- See @Async@. +-- | Check whether an 'Async' has completed yet. The status of the asynchronous +-- action is encoded in the returned 'AsyncResult', If not, the result is +-- 'AsyncPending', or one of the other constructors otherwise. +-- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do mv <- liftIO $ tryTakeMVar d @@ -107,7 +116,7 @@ poll (Async _ _ d) = do Nothing -> return AsyncPending Just v -> return v --- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 1105eeee..513ee531 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -30,7 +30,11 @@ import Data.DeriveTH import Data.Typeable (Typeable) import Prelude hiding (init) --- | an opaque reference to a timer +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | An opaque reference to a timer. type TimerRef = ProcessId -- | cancellation message sent to timers @@ -38,19 +42,16 @@ data TimerConfig = Reset | Cancel deriving (Typeable, Show) $(derive makeBinary ''TimerConfig) --- | represents a 'tick' event that timers can generate +-- | Represents a 'tick' event that timers can generate data Tick = Tick deriving (Typeable, Eq) $(derive makeBinary ''Tick) +-- | Private data type used to guarantee that 'sleep' blocks on receive. data SleepingPill = SleepingPill deriving (Typeable) $(derive makeBinary ''SleepingPill) --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - -- time interval/unit handling -- | converts the supplied @TimeInterval@ to milliseconds @@ -85,7 +86,7 @@ timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 -- process implementations --- | blocks the calling Process for the specified TimeInterval. Note that this +-- | Blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards -- scheduling) to differ from threadDelay and/or operating system specific From ddf65325fb649dd83e7dd6685c0d881c6ba5e48c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:25 +0000 Subject: [PATCH 0466/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 27 +++++++++++++++-------- src/Control/Distributed/Platform/Timer.hs | 15 +++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 0379b92d..b3c00086 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -16,6 +16,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + -- | A reference to an asynchronous action type AsyncRef = ProcessId @@ -25,9 +29,9 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an @AsyncGathererId@ (to which replies should be +-- | A function that takes an 'AsyncGathererId' (to which replies should be -- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions @AsyncWorkerId@ in the @Process@ monad. +-- spawned actions 'AsyncWorkerId' in the @Process@ monad. type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId type AsyncData a = MVar (AsyncResult a) @@ -46,11 +50,16 @@ data AsyncResult a = | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) --- | An async cancellation takes an @AsyncRef@ and does some cancellation +-- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. type AsyncCancel = AsyncRef -> Process () --- TODO: Document me please! +-- | An asynchronous action spawned by 'async' or 'withAsync'. +-- Asynchronous actions are executed in a separate @Process@, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- There is currently a contract between async workers and async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar @@ -96,10 +105,10 @@ async spawnF = do -- Based on this, the only other kinds of message that can arrive are the -- return value from the worker or a cancellation from the coordinating process. --- | Check whether an @Async@ has completed yet. The status of the asynchronous --- action is encoded in the returned @AsyncResult@. If not, the result is --- @AsyncPending@, or one of the other constructors otherwise. --- See @Async@. +-- | Check whether an 'Async' has completed yet. The status of the asynchronous +-- action is encoded in the returned 'AsyncResult', If not, the result is +-- 'AsyncPending', or one of the other constructors otherwise. +-- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do mv <- liftIO $ tryTakeMVar d @@ -107,7 +116,7 @@ poll (Async _ _ d) = do Nothing -> return AsyncPending Just v -> return v --- | Like @poll@ but returns @Nothing@ if @poll@ returns @AsyncPending@. +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs index 1105eeee..513ee531 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Platform/Timer.hs @@ -30,7 +30,11 @@ import Data.DeriveTH import Data.Typeable (Typeable) import Prelude hiding (init) --- | an opaque reference to a timer +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | An opaque reference to a timer. type TimerRef = ProcessId -- | cancellation message sent to timers @@ -38,19 +42,16 @@ data TimerConfig = Reset | Cancel deriving (Typeable, Show) $(derive makeBinary ''TimerConfig) --- | represents a 'tick' event that timers can generate +-- | Represents a 'tick' event that timers can generate data Tick = Tick deriving (Typeable, Eq) $(derive makeBinary ''Tick) +-- | Private data type used to guarantee that 'sleep' blocks on receive. data SleepingPill = SleepingPill deriving (Typeable) $(derive makeBinary ''SleepingPill) --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - -- time interval/unit handling -- | converts the supplied @TimeInterval@ to milliseconds @@ -85,7 +86,7 @@ timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 -- process implementations --- | blocks the calling Process for the specified TimeInterval. Note that this +-- | Blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards -- scheduling) to differ from threadDelay and/or operating system specific From 0ce473eaf69d3091a7231b6e36c1384ffd1445b8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:53 +0000 Subject: [PATCH 0467/2357] export an API --- src/Control/Distributed/Platform/Async.hs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3c00086..8ced43e7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -2,7 +2,23 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE EmptyDataDecls #-} -module Control.Distributed.Platform.Async where +module Control.Distributed.Platform.Async + ( AsyncRef + , AsyncWorkerId + , AsyncGathererId + , SpawnAsync + , AsyncCancel + , AsyncData + , Async() + , AsyncResult(..) + , async + , poll + , check + , waitTimeout + , cancel + , cancelAsync + , cancelWait + ) where import Control.Concurrent.MVar import Control.Distributed.Platform From ca405482f711e0f42492acaa4049a6539c5a167d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:53 +0000 Subject: [PATCH 0468/2357] export an API --- src/Control/Distributed/Platform/Async.hs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3c00086..8ced43e7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -2,7 +2,23 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE EmptyDataDecls #-} -module Control.Distributed.Platform.Async where +module Control.Distributed.Platform.Async + ( AsyncRef + , AsyncWorkerId + , AsyncGathererId + , SpawnAsync + , AsyncCancel + , AsyncData + , Async() + , AsyncResult(..) + , async + , poll + , check + , waitTimeout + , cancel + , cancelAsync + , cancelWait + ) where import Control.Concurrent.MVar import Control.Distributed.Platform From ab2f525120ff481dffe5e55ec6eb9e5f97f1b59d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:53 +0000 Subject: [PATCH 0469/2357] export an API --- src/Control/Distributed/Platform/Async.hs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3c00086..8ced43e7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -2,7 +2,23 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE EmptyDataDecls #-} -module Control.Distributed.Platform.Async where +module Control.Distributed.Platform.Async + ( AsyncRef + , AsyncWorkerId + , AsyncGathererId + , SpawnAsync + , AsyncCancel + , AsyncData + , Async() + , AsyncResult(..) + , async + , poll + , check + , waitTimeout + , cancel + , cancelAsync + , cancelWait + ) where import Control.Concurrent.MVar import Control.Distributed.Platform From 545d93bbb461452a9fed5ee9b8e8d4c1cc72383a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:24:53 +0000 Subject: [PATCH 0470/2357] export an API --- src/Control/Distributed/Platform/Async.hs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3c00086..8ced43e7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -2,7 +2,23 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE EmptyDataDecls #-} -module Control.Distributed.Platform.Async where +module Control.Distributed.Platform.Async + ( AsyncRef + , AsyncWorkerId + , AsyncGathererId + , SpawnAsync + , AsyncCancel + , AsyncData + , Async() + , AsyncResult(..) + , async + , poll + , check + , waitTimeout + , cancel + , cancelAsync + , cancelWait + ) where import Control.Concurrent.MVar import Control.Distributed.Platform From e6338f128fce17a390bc82f08e17436994bb125e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:26:38 +0000 Subject: [PATCH 0471/2357] cosmetic --- src/Control/Distributed/Platform/Async.hs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ced43e7..8ac24320 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -82,7 +82,8 @@ async spawnF = do (wpid, gpid) <- spawnWorkers spawnF mv return (Async wpid gpid mv) where - spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers :: (Serializable a) => + SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) spawnWorkers sp ad = do root <- getSelfPid @@ -100,7 +101,8 @@ async spawnF = do return (wpid, gpid) -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit :: (Serializable a) => + ProcessId -> MonitorRef -> AsyncData a -> Process () pollUntilExit pid ref ad = do r <- receiveWait [ matchIf @@ -140,8 +142,8 @@ check hAsync = poll hAsync >>= \r -> case r of -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if no result is available within the specified delay. -waitTimeout :: (Serializable a) => TimeInterval -> - Async a -> Process (Maybe (AsyncResult a)) +waitTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do self <- getSelfPid ar <- poll hAsync From 10b98b0e3247754bb55574db16aec92086eab022 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:26:38 +0000 Subject: [PATCH 0472/2357] cosmetic --- src/Control/Distributed/Platform/Async.hs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ced43e7..8ac24320 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -82,7 +82,8 @@ async spawnF = do (wpid, gpid) <- spawnWorkers spawnF mv return (Async wpid gpid mv) where - spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers :: (Serializable a) => + SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) spawnWorkers sp ad = do root <- getSelfPid @@ -100,7 +101,8 @@ async spawnF = do return (wpid, gpid) -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit :: (Serializable a) => + ProcessId -> MonitorRef -> AsyncData a -> Process () pollUntilExit pid ref ad = do r <- receiveWait [ matchIf @@ -140,8 +142,8 @@ check hAsync = poll hAsync >>= \r -> case r of -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if no result is available within the specified delay. -waitTimeout :: (Serializable a) => TimeInterval -> - Async a -> Process (Maybe (AsyncResult a)) +waitTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do self <- getSelfPid ar <- poll hAsync From 30c6603ac0f9493ce4a238e556700aa8d1e9b643 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:26:38 +0000 Subject: [PATCH 0473/2357] cosmetic --- src/Control/Distributed/Platform/Async.hs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ced43e7..8ac24320 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -82,7 +82,8 @@ async spawnF = do (wpid, gpid) <- spawnWorkers spawnF mv return (Async wpid gpid mv) where - spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers :: (Serializable a) => + SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) spawnWorkers sp ad = do root <- getSelfPid @@ -100,7 +101,8 @@ async spawnF = do return (wpid, gpid) -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit :: (Serializable a) => + ProcessId -> MonitorRef -> AsyncData a -> Process () pollUntilExit pid ref ad = do r <- receiveWait [ matchIf @@ -140,8 +142,8 @@ check hAsync = poll hAsync >>= \r -> case r of -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if no result is available within the specified delay. -waitTimeout :: (Serializable a) => TimeInterval -> - Async a -> Process (Maybe (AsyncResult a)) +waitTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do self <- getSelfPid ar <- poll hAsync From 3e2e83b2479a4f6b58e70a831ff5d3ccb247423c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:26:38 +0000 Subject: [PATCH 0474/2357] cosmetic --- src/Control/Distributed/Platform/Async.hs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ced43e7..8ac24320 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -82,7 +82,8 @@ async spawnF = do (wpid, gpid) <- spawnWorkers spawnF mv return (Async wpid gpid mv) where - spawnWorkers :: (Serializable a) => SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) + spawnWorkers :: (Serializable a) => + SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) spawnWorkers sp ad = do root <- getSelfPid @@ -100,7 +101,8 @@ async spawnF = do return (wpid, gpid) -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> AsyncData a -> Process () + pollUntilExit :: (Serializable a) => + ProcessId -> MonitorRef -> AsyncData a -> Process () pollUntilExit pid ref ad = do r <- receiveWait [ matchIf @@ -140,8 +142,8 @@ check hAsync = poll hAsync >>= \r -> case r of -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if no result is available within the specified delay. -waitTimeout :: (Serializable a) => TimeInterval -> - Async a -> Process (Maybe (AsyncResult a)) +waitTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do self <- getSelfPid ar <- poll hAsync From c6fef8746446dc24cd970a506db16e5ea83c9a9c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:27:08 +0000 Subject: [PATCH 0475/2357] implement waitTimeout sanely --- src/Control/Distributed/Platform/Async.hs | 57 +++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ac24320..55098aba 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -23,6 +23,7 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform ( sendAfter + , cancelTimer , TimerRef , TimeInterval() ) @@ -112,12 +113,12 @@ async spawnF = do , match (\x -> return (Left x)) ] case r of + Left a -> liftIO $ putMVar ad (AsyncDone a) Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] Right d -> liftIO $ putMVar ad (AsyncFailed d) - Left a -> liftIO $ putMVar ad (AsyncDone a) - + -- note [recursion] --- We recurse *just once* if we've seen a normal exit from worker. We're +-- We recurse /just once/ if we've seen a normal exit from our worker. We're -- absolutely sure about this, because once we've seen DiedNormal for the -- monitored process, it's not possible to see another monitor signal for it. -- Based on this, the only other kinds of message that can arrive are the @@ -145,28 +146,46 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do + -- TODO: this implementation is just nonsense - we should spawn a worker to + -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync - _ -> return (Just ar) + AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync + _ -> return (Just ar) where - waitOnMailBox :: (Serializable a) => TimeInterval -> - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitOnMailBox t' a ref = do + waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a ref pid = spawnLocal $ waitLoop a ref pid + + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle ref wAuxPid = do + m <- waitOne asyncHandle -- note [wait loop] + case m of + Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Nothing -> waitLoop asyncHandle ref wAuxPid + + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a = do m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending) - ] - -- TODO: this is pretty disgusting - sprinkle with applicative or some such + match (\CancelWait -> return AsyncPending)] case m of - Nothing -> do - r <- check a - case r of - -- this isn't tail recursive, so we're likely to overflow fast - Nothing -> waitOnMailBox t' a ref - Just _ -> return r - Just _ -> - return m + Nothing -> check a -- if we timed out, check the result again + Just _ -> return m -- (Just CancelWait) means we're done here + +-- note [wait loop] +-- This logic is a bit spaghetti-like so a little explanation: +-- Firstly, we spawn a /waiter/ process so that timing out is simple. +-- Doing this by creating a loop in the caller's process is just a mess, +-- as should be obvious from the necessary type signature once you try it. +-- Instead, the /waiter/ queries its mailbox continually and defers to 'check' +-- to see if the AsyncResult is ready yet. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From 74fe49eacd587ee627ef0d7bfe529b9eff5b26ee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:27:08 +0000 Subject: [PATCH 0476/2357] implement waitTimeout sanely --- src/Control/Distributed/Platform/Async.hs | 57 +++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ac24320..55098aba 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -23,6 +23,7 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform ( sendAfter + , cancelTimer , TimerRef , TimeInterval() ) @@ -112,12 +113,12 @@ async spawnF = do , match (\x -> return (Left x)) ] case r of + Left a -> liftIO $ putMVar ad (AsyncDone a) Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] Right d -> liftIO $ putMVar ad (AsyncFailed d) - Left a -> liftIO $ putMVar ad (AsyncDone a) - + -- note [recursion] --- We recurse *just once* if we've seen a normal exit from worker. We're +-- We recurse /just once/ if we've seen a normal exit from our worker. We're -- absolutely sure about this, because once we've seen DiedNormal for the -- monitored process, it's not possible to see another monitor signal for it. -- Based on this, the only other kinds of message that can arrive are the @@ -145,28 +146,46 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do + -- TODO: this implementation is just nonsense - we should spawn a worker to + -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync - _ -> return (Just ar) + AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync + _ -> return (Just ar) where - waitOnMailBox :: (Serializable a) => TimeInterval -> - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitOnMailBox t' a ref = do + waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a ref pid = spawnLocal $ waitLoop a ref pid + + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle ref wAuxPid = do + m <- waitOne asyncHandle -- note [wait loop] + case m of + Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Nothing -> waitLoop asyncHandle ref wAuxPid + + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a = do m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending) - ] - -- TODO: this is pretty disgusting - sprinkle with applicative or some such + match (\CancelWait -> return AsyncPending)] case m of - Nothing -> do - r <- check a - case r of - -- this isn't tail recursive, so we're likely to overflow fast - Nothing -> waitOnMailBox t' a ref - Just _ -> return r - Just _ -> - return m + Nothing -> check a -- if we timed out, check the result again + Just _ -> return m -- (Just CancelWait) means we're done here + +-- note [wait loop] +-- This logic is a bit spaghetti-like so a little explanation: +-- Firstly, we spawn a /waiter/ process so that timing out is simple. +-- Doing this by creating a loop in the caller's process is just a mess, +-- as should be obvious from the necessary type signature once you try it. +-- Instead, the /waiter/ queries its mailbox continually and defers to 'check' +-- to see if the AsyncResult is ready yet. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From 3ab10329bfc0eb1dde102f85919cab8fa45234de Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:27:08 +0000 Subject: [PATCH 0477/2357] implement waitTimeout sanely --- src/Control/Distributed/Platform/Async.hs | 57 +++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ac24320..55098aba 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -23,6 +23,7 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform ( sendAfter + , cancelTimer , TimerRef , TimeInterval() ) @@ -112,12 +113,12 @@ async spawnF = do , match (\x -> return (Left x)) ] case r of + Left a -> liftIO $ putMVar ad (AsyncDone a) Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] Right d -> liftIO $ putMVar ad (AsyncFailed d) - Left a -> liftIO $ putMVar ad (AsyncDone a) - + -- note [recursion] --- We recurse *just once* if we've seen a normal exit from worker. We're +-- We recurse /just once/ if we've seen a normal exit from our worker. We're -- absolutely sure about this, because once we've seen DiedNormal for the -- monitored process, it's not possible to see another monitor signal for it. -- Based on this, the only other kinds of message that can arrive are the @@ -145,28 +146,46 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do + -- TODO: this implementation is just nonsense - we should spawn a worker to + -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync - _ -> return (Just ar) + AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync + _ -> return (Just ar) where - waitOnMailBox :: (Serializable a) => TimeInterval -> - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitOnMailBox t' a ref = do + waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a ref pid = spawnLocal $ waitLoop a ref pid + + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle ref wAuxPid = do + m <- waitOne asyncHandle -- note [wait loop] + case m of + Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Nothing -> waitLoop asyncHandle ref wAuxPid + + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a = do m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending) - ] - -- TODO: this is pretty disgusting - sprinkle with applicative or some such + match (\CancelWait -> return AsyncPending)] case m of - Nothing -> do - r <- check a - case r of - -- this isn't tail recursive, so we're likely to overflow fast - Nothing -> waitOnMailBox t' a ref - Just _ -> return r - Just _ -> - return m + Nothing -> check a -- if we timed out, check the result again + Just _ -> return m -- (Just CancelWait) means we're done here + +-- note [wait loop] +-- This logic is a bit spaghetti-like so a little explanation: +-- Firstly, we spawn a /waiter/ process so that timing out is simple. +-- Doing this by creating a loop in the caller's process is just a mess, +-- as should be obvious from the necessary type signature once you try it. +-- Instead, the /waiter/ queries its mailbox continually and defers to 'check' +-- to see if the AsyncResult is ready yet. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From a729f19bde0d3316b38e9273941519b14d5a2abf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:27:08 +0000 Subject: [PATCH 0478/2357] implement waitTimeout sanely --- src/Control/Distributed/Platform/Async.hs | 57 +++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 8ac24320..55098aba 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -23,6 +23,7 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform ( sendAfter + , cancelTimer , TimerRef , TimeInterval() ) @@ -112,12 +113,12 @@ async spawnF = do , match (\x -> return (Left x)) ] case r of + Left a -> liftIO $ putMVar ad (AsyncDone a) Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] Right d -> liftIO $ putMVar ad (AsyncFailed d) - Left a -> liftIO $ putMVar ad (AsyncDone a) - + -- note [recursion] --- We recurse *just once* if we've seen a normal exit from worker. We're +-- We recurse /just once/ if we've seen a normal exit from our worker. We're -- absolutely sure about this, because once we've seen DiedNormal for the -- monitored process, it's not possible to see another monitor signal for it. -- Based on this, the only other kinds of message that can arrive are the @@ -145,28 +146,46 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do + -- TODO: this implementation is just nonsense - we should spawn a worker to + -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitOnMailBox t hAsync - _ -> return (Just ar) + AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync + _ -> return (Just ar) where - waitOnMailBox :: (Serializable a) => TimeInterval -> - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitOnMailBox t' a ref = do + waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) + waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a ref pid = spawnLocal $ waitLoop a ref pid + + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle ref wAuxPid = do + m <- waitOne asyncHandle -- note [wait loop] + case m of + Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Nothing -> waitLoop asyncHandle ref wAuxPid + + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a = do m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending) - ] - -- TODO: this is pretty disgusting - sprinkle with applicative or some such + match (\CancelWait -> return AsyncPending)] case m of - Nothing -> do - r <- check a - case r of - -- this isn't tail recursive, so we're likely to overflow fast - Nothing -> waitOnMailBox t' a ref - Just _ -> return r - Just _ -> - return m + Nothing -> check a -- if we timed out, check the result again + Just _ -> return m -- (Just CancelWait) means we're done here + +-- note [wait loop] +-- This logic is a bit spaghetti-like so a little explanation: +-- Firstly, we spawn a /waiter/ process so that timing out is simple. +-- Doing this by creating a loop in the caller's process is just a mess, +-- as should be obvious from the necessary type signature once you try it. +-- Instead, the /waiter/ queries its mailbox continually and defers to 'check' +-- to see if the AsyncResult is ready yet. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From 7352136a40e9afb584a525993d3958b4e646989d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:02 +0000 Subject: [PATCH 0479/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 55098aba..62c793b9 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -125,8 +125,10 @@ async spawnF = do -- return value from the worker or a cancellation from the coordinating process. -- | Check whether an 'Async' has completed yet. The status of the asynchronous --- action is encoded in the returned 'AsyncResult', If not, the result is --- 'AsyncPending', or one of the other constructors otherwise. +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do @@ -136,13 +138,17 @@ poll (Async _ _ d) = do Just v -> return v -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if no result is available within the specified delay. +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'waitCheck' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do From 11de65fa5e745aea0ae92477ce52f349cbcdf87a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:02 +0000 Subject: [PATCH 0480/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 55098aba..62c793b9 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -125,8 +125,10 @@ async spawnF = do -- return value from the worker or a cancellation from the coordinating process. -- | Check whether an 'Async' has completed yet. The status of the asynchronous --- action is encoded in the returned 'AsyncResult', If not, the result is --- 'AsyncPending', or one of the other constructors otherwise. +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do @@ -136,13 +138,17 @@ poll (Async _ _ d) = do Just v -> return v -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if no result is available within the specified delay. +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'waitCheck' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do From e7872f83cda62c7e6b32a5d631eebfe64f63080b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:02 +0000 Subject: [PATCH 0481/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 55098aba..62c793b9 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -125,8 +125,10 @@ async spawnF = do -- return value from the worker or a cancellation from the coordinating process. -- | Check whether an 'Async' has completed yet. The status of the asynchronous --- action is encoded in the returned 'AsyncResult', If not, the result is --- 'AsyncPending', or one of the other constructors otherwise. +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do @@ -136,13 +138,17 @@ poll (Async _ _ d) = do Just v -> return v -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if no result is available within the specified delay. +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'waitCheck' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do From f2272a77bf2a7426a71270ab2961b9a4d18e253d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:02 +0000 Subject: [PATCH 0482/2357] documentation --- src/Control/Distributed/Platform/Async.hs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 55098aba..62c793b9 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -125,8 +125,10 @@ async spawnF = do -- return value from the worker or a cancellation from the coordinating process. -- | Check whether an 'Async' has completed yet. The status of the asynchronous --- action is encoded in the returned 'AsyncResult', If not, the result is --- 'AsyncPending', or one of the other constructors otherwise. +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) poll (Async _ _ d) = do @@ -136,13 +138,17 @@ poll (Async _ _ d) = do Just v -> return v -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if no result is available within the specified delay. +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'waitCheck' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do From f0bbdda4424084bef14fea8d0e24f533bfc4863c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:34 +0000 Subject: [PATCH 0483/2357] TODO => done; strip it out --- src/Control/Distributed/Platform/Async.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 62c793b9..69370cc7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -152,8 +152,6 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do - -- TODO: this implementation is just nonsense - we should spawn a worker to - -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of From 24801a0f979d8860f8ef58fcbae2e2b7ff4ae44e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:34 +0000 Subject: [PATCH 0484/2357] TODO => done; strip it out --- src/Control/Distributed/Platform/Async.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 62c793b9..69370cc7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -152,8 +152,6 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do - -- TODO: this implementation is just nonsense - we should spawn a worker to - -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of From 0ab757b49a7cc5b933f09c7b876b32a25f4f4362 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:34 +0000 Subject: [PATCH 0485/2357] TODO => done; strip it out --- src/Control/Distributed/Platform/Async.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 62c793b9..69370cc7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -152,8 +152,6 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do - -- TODO: this implementation is just nonsense - we should spawn a worker to - -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of From 1073c3055b0b37b0f4f12eb81c110ca633cc2c45 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:35:34 +0000 Subject: [PATCH 0486/2357] TODO => done; strip it out --- src/Control/Distributed/Platform/Async.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 62c793b9..69370cc7 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -152,8 +152,6 @@ check hAsync = poll hAsync >>= \r -> case r of waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do - -- TODO: this implementation is just nonsense - we should spawn a worker to - -- handle the loop and simply timeout if it doesn't return quickly self <- getSelfPid ar <- poll hAsync case ar of From 8acd3104479629840e81481d6f295a817162e071 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:30 +0000 Subject: [PATCH 0487/2357] refactor --- src/Control/Distributed/Platform/Async.hs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 69370cc7..c4dfbb35 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -131,11 +131,8 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = do - mv <- liftIO $ tryTakeMVar d - case mv of - Nothing -> return AsyncPending - Just v -> return v +poll (Async _ _ d) = + liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. From afcb30e3431f4e688368467a74643a055a5e6fe2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:30 +0000 Subject: [PATCH 0488/2357] refactor --- src/Control/Distributed/Platform/Async.hs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 69370cc7..c4dfbb35 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -131,11 +131,8 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = do - mv <- liftIO $ tryTakeMVar d - case mv of - Nothing -> return AsyncPending - Just v -> return v +poll (Async _ _ d) = + liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. From 35f6205b4674b06addbf5d11b6ba1617601c6b77 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:30 +0000 Subject: [PATCH 0489/2357] refactor --- src/Control/Distributed/Platform/Async.hs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 69370cc7..c4dfbb35 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -131,11 +131,8 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = do - mv <- liftIO $ tryTakeMVar d - case mv of - Nothing -> return AsyncPending - Just v -> return v +poll (Async _ _ d) = + liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. From 73c2237ae89371b37f396274a219f6cc627a52f1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:30 +0000 Subject: [PATCH 0490/2357] refactor --- src/Control/Distributed/Platform/Async.hs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 69370cc7..c4dfbb35 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -131,11 +131,8 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = do - mv <- liftIO $ tryTakeMVar d - case mv of - Nothing -> return AsyncPending - Just v -> return v +poll (Async _ _ d) = + liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. From b500c5c2b9932af24361393c1d0c8941db74f4b6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:52 +0000 Subject: [PATCH 0491/2357] document the contract for async workers --- src/Control/Distributed/Platform/Async.hs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index c4dfbb35..5a26070f 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -76,7 +76,16 @@ type AsyncCancel = AsyncRef -> Process () -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and +-- There is currently a contract between async workers and their coordinating +-- processes (including the caller to functions such as 'wait'). Given the +-- process identifier of a gatherer, a worker that wishes to publish some +-- results should send these to the gatherer process when it is finished. +-- Workers that do not return anything should simply exit normally (i.e., they +-- should not call @exit selfPid reason@ not @terminate@ in the base Cloud +-- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ +-- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual +-- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar From 392ec8b242d46a74679b3a7b5c07d6e3d27d9a96 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:52 +0000 Subject: [PATCH 0492/2357] document the contract for async workers --- src/Control/Distributed/Platform/Async.hs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index c4dfbb35..5a26070f 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -76,7 +76,16 @@ type AsyncCancel = AsyncRef -> Process () -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and +-- There is currently a contract between async workers and their coordinating +-- processes (including the caller to functions such as 'wait'). Given the +-- process identifier of a gatherer, a worker that wishes to publish some +-- results should send these to the gatherer process when it is finished. +-- Workers that do not return anything should simply exit normally (i.e., they +-- should not call @exit selfPid reason@ not @terminate@ in the base Cloud +-- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ +-- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual +-- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar From 25a53ccb7758fe69cc5fdc819f5e713f8efad460 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:52 +0000 Subject: [PATCH 0493/2357] document the contract for async workers --- src/Control/Distributed/Platform/Async.hs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index c4dfbb35..5a26070f 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -76,7 +76,16 @@ type AsyncCancel = AsyncRef -> Process () -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and +-- There is currently a contract between async workers and their coordinating +-- processes (including the caller to functions such as 'wait'). Given the +-- process identifier of a gatherer, a worker that wishes to publish some +-- results should send these to the gatherer process when it is finished. +-- Workers that do not return anything should simply exit normally (i.e., they +-- should not call @exit selfPid reason@ not @terminate@ in the base Cloud +-- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ +-- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual +-- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar From 0076c21444c96c46acda504fcd979924e24edd45 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:49:52 +0000 Subject: [PATCH 0494/2357] document the contract for async workers --- src/Control/Distributed/Platform/Async.hs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index c4dfbb35..5a26070f 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -76,7 +76,16 @@ type AsyncCancel = AsyncRef -> Process () -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and +-- There is currently a contract between async workers and their coordinating +-- processes (including the caller to functions such as 'wait'). Given the +-- process identifier of a gatherer, a worker that wishes to publish some +-- results should send these to the gatherer process when it is finished. +-- Workers that do not return anything should simply exit normally (i.e., they +-- should not call @exit selfPid reason@ not @terminate@ in the base Cloud +-- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ +-- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual +-- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- async :: (Serializable a) => SpawnAsync -> Process (Async a) async spawnF = do mv <- liftIO $ newEmptyMVar From 39a053628a1b51b223152238e5f487e686fbf875 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:50:48 +0000 Subject: [PATCH 0495/2357] implement waitCheckTimeout --- src/Control/Distributed/Platform/Async.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 5a26070f..72b757e5 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -18,6 +18,7 @@ module Control.Distributed.Platform.Async , cancel , cancelAsync , cancelWait + , waitCheckTimeout ) where import Control.Concurrent.MVar @@ -33,6 +34,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +import Data.Maybe + ( fromMaybe + ) + -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- -------------------------------------------------------------------------------- @@ -150,6 +155,11 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +waitCheckTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want From d6ea5616f5bb480ad7e16bcc423f641dcca21964 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:50:48 +0000 Subject: [PATCH 0496/2357] implement waitCheckTimeout --- src/Control/Distributed/Platform/Async.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 5a26070f..72b757e5 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -18,6 +18,7 @@ module Control.Distributed.Platform.Async , cancel , cancelAsync , cancelWait + , waitCheckTimeout ) where import Control.Concurrent.MVar @@ -33,6 +34,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +import Data.Maybe + ( fromMaybe + ) + -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- -------------------------------------------------------------------------------- @@ -150,6 +155,11 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +waitCheckTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want From 8557baa8111cec84d60b4f6fdc9992319f91bf9c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:50:48 +0000 Subject: [PATCH 0497/2357] implement waitCheckTimeout --- src/Control/Distributed/Platform/Async.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 5a26070f..72b757e5 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -18,6 +18,7 @@ module Control.Distributed.Platform.Async , cancel , cancelAsync , cancelWait + , waitCheckTimeout ) where import Control.Concurrent.MVar @@ -33,6 +34,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +import Data.Maybe + ( fromMaybe + ) + -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- -------------------------------------------------------------------------------- @@ -150,6 +155,11 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +waitCheckTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want From 2260c1296073e9f41a8e774bfa0d782aa41a0202 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:50:48 +0000 Subject: [PATCH 0498/2357] implement waitCheckTimeout --- src/Control/Distributed/Platform/Async.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 5a26070f..72b757e5 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -18,6 +18,7 @@ module Control.Distributed.Platform.Async , cancel , cancelAsync , cancelWait + , waitCheckTimeout ) where import Control.Concurrent.MVar @@ -33,6 +34,10 @@ import Control.Distributed.Platform.Internal.Types import Control.Distributed.Process import Control.Distributed.Process.Serializable +import Data.Maybe + ( fromMaybe + ) + -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- -------------------------------------------------------------------------------- @@ -150,6 +155,11 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +waitCheckTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want From a4bc9906de5f9815fbefffca93b27969573210ce Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:51:49 +0000 Subject: [PATCH 0499/2357] cosmetic (ish) --- src/Control/Distributed/Platform/Async.hs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 72b757e5..7180ab11 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -3,7 +3,8 @@ {-# LANGUAGE EmptyDataDecls #-} module Control.Distributed.Platform.Async - ( AsyncRef + ( -- types/data + AsyncRef , AsyncWorkerId , AsyncGathererId , SpawnAsync @@ -11,13 +12,16 @@ module Control.Distributed.Platform.Async , AsyncData , Async() , AsyncResult(..) + -- functions for starting/spawning , async - , poll - , check - , waitTimeout + -- and stopping/killing , cancel , cancelAsync , cancelWait + -- functions to query an async-result + , poll + , check + , waitTimeout , waitCheckTimeout ) where @@ -107,7 +111,7 @@ async spawnF = do proxy <- getSelfPid worker <- sp proxy - send root worker + send root worker -- let the parent process know the worker pid monRef <- monitor worker finally (pollUntilExit worker monRef ad) (unmonitor monRef) From 02833cb59e36dd939755781e23080a980761f28d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:51:49 +0000 Subject: [PATCH 0500/2357] cosmetic (ish) --- src/Control/Distributed/Platform/Async.hs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 72b757e5..7180ab11 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -3,7 +3,8 @@ {-# LANGUAGE EmptyDataDecls #-} module Control.Distributed.Platform.Async - ( AsyncRef + ( -- types/data + AsyncRef , AsyncWorkerId , AsyncGathererId , SpawnAsync @@ -11,13 +12,16 @@ module Control.Distributed.Platform.Async , AsyncData , Async() , AsyncResult(..) + -- functions for starting/spawning , async - , poll - , check - , waitTimeout + -- and stopping/killing , cancel , cancelAsync , cancelWait + -- functions to query an async-result + , poll + , check + , waitTimeout , waitCheckTimeout ) where @@ -107,7 +111,7 @@ async spawnF = do proxy <- getSelfPid worker <- sp proxy - send root worker + send root worker -- let the parent process know the worker pid monRef <- monitor worker finally (pollUntilExit worker monRef ad) (unmonitor monRef) From 359f576e66b56e3ee82cf8f8a75de160532a58b5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:51:49 +0000 Subject: [PATCH 0501/2357] cosmetic (ish) --- src/Control/Distributed/Platform/Async.hs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 72b757e5..7180ab11 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -3,7 +3,8 @@ {-# LANGUAGE EmptyDataDecls #-} module Control.Distributed.Platform.Async - ( AsyncRef + ( -- types/data + AsyncRef , AsyncWorkerId , AsyncGathererId , SpawnAsync @@ -11,13 +12,16 @@ module Control.Distributed.Platform.Async , AsyncData , Async() , AsyncResult(..) + -- functions for starting/spawning , async - , poll - , check - , waitTimeout + -- and stopping/killing , cancel , cancelAsync , cancelWait + -- functions to query an async-result + , poll + , check + , waitTimeout , waitCheckTimeout ) where @@ -107,7 +111,7 @@ async spawnF = do proxy <- getSelfPid worker <- sp proxy - send root worker + send root worker -- let the parent process know the worker pid monRef <- monitor worker finally (pollUntilExit worker monRef ad) (unmonitor monRef) From 124878abcdb10a8b41aa35e066cbe3c450ad4f28 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 10:51:49 +0000 Subject: [PATCH 0502/2357] cosmetic (ish) --- src/Control/Distributed/Platform/Async.hs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 72b757e5..7180ab11 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -3,7 +3,8 @@ {-# LANGUAGE EmptyDataDecls #-} module Control.Distributed.Platform.Async - ( AsyncRef + ( -- types/data + AsyncRef , AsyncWorkerId , AsyncGathererId , SpawnAsync @@ -11,13 +12,16 @@ module Control.Distributed.Platform.Async , AsyncData , Async() , AsyncResult(..) + -- functions for starting/spawning , async - , poll - , check - , waitTimeout + -- and stopping/killing , cancel , cancelAsync , cancelWait + -- functions to query an async-result + , poll + , check + , waitTimeout , waitCheckTimeout ) where @@ -107,7 +111,7 @@ async spawnF = do proxy <- getSelfPid worker <- sp proxy - send root worker + send root worker -- let the parent process know the worker pid monRef <- monitor worker finally (pollUntilExit worker monRef ad) (unmonitor monRef) From 60ff9424fbd92f1d6a22a4c771c8529d0ed9c54f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 11:58:40 +0000 Subject: [PATCH 0503/2357] add blocking wait call - document things a bit more --- src/Control/Distributed/Platform/Async.hs | 67 ++++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 7180ab11..d7588b79 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -21,6 +21,7 @@ module Control.Distributed.Platform.Async -- functions to query an async-result , poll , check + , wait , waitTimeout , waitCheckTimeout ) where @@ -36,6 +37,9 @@ import Control.Distributed.Platform.Internal.Types ( CancelWait(..) ) import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( nullProcessId + ) import Control.Distributed.Process.Serializable import Data.Maybe @@ -159,16 +163,30 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. waitCheckTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (AsyncResult a) waitCheckTimeout t hAsync = waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => Async a -> Process (AsyncResult a) +wait hAsync = + nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) + where nullTimerRef :: Process TimerRef + nullTimerRef = do + nid <- getSelfNode + return (nullProcessId nid) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'waitCheck' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do @@ -177,30 +195,31 @@ waitTimeout t hAsync = do case ar of AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync _ -> return (Just ar) - where - waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a ref pid = spawnLocal $ waitLoop a ref pid - - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle ref wAuxPid = do + +-- | Private /wait/ implementation. +waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) +waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + where + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid + ------------------- + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle tRef wAuxPid = do m <- waitOne asyncHandle -- note [wait loop] case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) Nothing -> waitLoop asyncHandle ref wAuxPid - - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a = do + ------------------- + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a' = do m <- receiveTimeout 0 [ match (\CancelWait -> return AsyncPending)] case m of - Nothing -> check a -- if we timed out, check the result again + Nothing -> check a' -- if we timed out, check the result again Just _ -> return m -- (Just CancelWait) means we're done here -- note [wait loop] @@ -208,8 +227,16 @@ waitTimeout t hAsync = do -- Firstly, we spawn a /waiter/ process so that timing out is simple. -- Doing this by creating a loop in the caller's process is just a mess, -- as should be obvious from the necessary type signature once you try it. +-- -- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. +-- to see if the AsyncResult is ready yet. Once we finally get a result back +-- from 'check', we signal the caller's process with "finished" after which +-- they can pull the results without delay by calling 'check' again. +-- +-- This logic can easily be made to block indefinitely by simply not setting up +-- a timer to signal us with @CancelWait@ - remember to pass an /empty/ +-- process identifier (i.e., nullProcessId) in that case however, otherwise +-- the call to 'cancelTimer' might crash. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From 80bb3343f3000927f7579b993bf097628788394a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 11:58:40 +0000 Subject: [PATCH 0504/2357] add blocking wait call - document things a bit more --- src/Control/Distributed/Platform/Async.hs | 67 ++++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 7180ab11..d7588b79 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -21,6 +21,7 @@ module Control.Distributed.Platform.Async -- functions to query an async-result , poll , check + , wait , waitTimeout , waitCheckTimeout ) where @@ -36,6 +37,9 @@ import Control.Distributed.Platform.Internal.Types ( CancelWait(..) ) import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( nullProcessId + ) import Control.Distributed.Process.Serializable import Data.Maybe @@ -159,16 +163,30 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. waitCheckTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (AsyncResult a) waitCheckTimeout t hAsync = waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => Async a -> Process (AsyncResult a) +wait hAsync = + nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) + where nullTimerRef :: Process TimerRef + nullTimerRef = do + nid <- getSelfNode + return (nullProcessId nid) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'waitCheck' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do @@ -177,30 +195,31 @@ waitTimeout t hAsync = do case ar of AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync _ -> return (Just ar) - where - waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a ref pid = spawnLocal $ waitLoop a ref pid - - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle ref wAuxPid = do + +-- | Private /wait/ implementation. +waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) +waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + where + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid + ------------------- + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle tRef wAuxPid = do m <- waitOne asyncHandle -- note [wait loop] case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) Nothing -> waitLoop asyncHandle ref wAuxPid - - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a = do + ------------------- + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a' = do m <- receiveTimeout 0 [ match (\CancelWait -> return AsyncPending)] case m of - Nothing -> check a -- if we timed out, check the result again + Nothing -> check a' -- if we timed out, check the result again Just _ -> return m -- (Just CancelWait) means we're done here -- note [wait loop] @@ -208,8 +227,16 @@ waitTimeout t hAsync = do -- Firstly, we spawn a /waiter/ process so that timing out is simple. -- Doing this by creating a loop in the caller's process is just a mess, -- as should be obvious from the necessary type signature once you try it. +-- -- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. +-- to see if the AsyncResult is ready yet. Once we finally get a result back +-- from 'check', we signal the caller's process with "finished" after which +-- they can pull the results without delay by calling 'check' again. +-- +-- This logic can easily be made to block indefinitely by simply not setting up +-- a timer to signal us with @CancelWait@ - remember to pass an /empty/ +-- process identifier (i.e., nullProcessId) in that case however, otherwise +-- the call to 'cancelTimer' might crash. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From 19eee4f077fd058f8a73ec4650dd2ceffb6b9ce2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 11:58:40 +0000 Subject: [PATCH 0505/2357] add blocking wait call - document things a bit more --- src/Control/Distributed/Platform/Async.hs | 67 ++++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 7180ab11..d7588b79 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -21,6 +21,7 @@ module Control.Distributed.Platform.Async -- functions to query an async-result , poll , check + , wait , waitTimeout , waitCheckTimeout ) where @@ -36,6 +37,9 @@ import Control.Distributed.Platform.Internal.Types ( CancelWait(..) ) import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( nullProcessId + ) import Control.Distributed.Process.Serializable import Data.Maybe @@ -159,16 +163,30 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. waitCheckTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (AsyncResult a) waitCheckTimeout t hAsync = waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => Async a -> Process (AsyncResult a) +wait hAsync = + nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) + where nullTimerRef :: Process TimerRef + nullTimerRef = do + nid <- getSelfNode + return (nullProcessId nid) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'waitCheck' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do @@ -177,30 +195,31 @@ waitTimeout t hAsync = do case ar of AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync _ -> return (Just ar) - where - waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a ref pid = spawnLocal $ waitLoop a ref pid - - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle ref wAuxPid = do + +-- | Private /wait/ implementation. +waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) +waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + where + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid + ------------------- + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle tRef wAuxPid = do m <- waitOne asyncHandle -- note [wait loop] case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) Nothing -> waitLoop asyncHandle ref wAuxPid - - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a = do + ------------------- + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a' = do m <- receiveTimeout 0 [ match (\CancelWait -> return AsyncPending)] case m of - Nothing -> check a -- if we timed out, check the result again + Nothing -> check a' -- if we timed out, check the result again Just _ -> return m -- (Just CancelWait) means we're done here -- note [wait loop] @@ -208,8 +227,16 @@ waitTimeout t hAsync = do -- Firstly, we spawn a /waiter/ process so that timing out is simple. -- Doing this by creating a loop in the caller's process is just a mess, -- as should be obvious from the necessary type signature once you try it. +-- -- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. +-- to see if the AsyncResult is ready yet. Once we finally get a result back +-- from 'check', we signal the caller's process with "finished" after which +-- they can pull the results without delay by calling 'check' again. +-- +-- This logic can easily be made to block indefinitely by simply not setting up +-- a timer to signal us with @CancelWait@ - remember to pass an /empty/ +-- process identifier (i.e., nullProcessId) in that case however, otherwise +-- the call to 'cancelTimer' might crash. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From c4ef938d5eb043f8a576a542436574b7684c237a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 11:58:40 +0000 Subject: [PATCH 0506/2357] add blocking wait call - document things a bit more --- src/Control/Distributed/Platform/Async.hs | 67 ++++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 7180ab11..d7588b79 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -21,6 +21,7 @@ module Control.Distributed.Platform.Async -- functions to query an async-result , poll , check + , wait , waitTimeout , waitCheckTimeout ) where @@ -36,6 +37,9 @@ import Control.Distributed.Platform.Internal.Types ( CancelWait(..) ) import Control.Distributed.Process +import Control.Distributed.Process.Internal.Types + ( nullProcessId + ) import Control.Distributed.Process.Serializable import Data.Maybe @@ -159,16 +163,30 @@ check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. waitCheckTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (AsyncResult a) waitCheckTimeout t hAsync = waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => Async a -> Process (AsyncResult a) +wait hAsync = + nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) + where nullTimerRef :: Process TimerRef + nullTimerRef = do + nid <- getSelfNode + return (nullProcessId nid) + -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'waitCheck' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = do @@ -177,30 +195,31 @@ waitTimeout t hAsync = do case ar of AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync _ -> return (Just ar) - where - waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) - waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a ref pid = spawnLocal $ waitLoop a ref pid - - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle ref wAuxPid = do + +-- | Private /wait/ implementation. +waitAux :: (Serializable a) => + Async a -> TimerRef -> Process (Maybe (AsyncResult a)) +waitAux a ref = + getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) + where + spawnWait :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process (ProcessId) + spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid + ------------------- + waitLoop :: (Serializable a) => + Async a -> TimerRef -> ProcessId -> Process () + waitLoop asyncHandle tRef wAuxPid = do m <- waitOne asyncHandle -- note [wait loop] case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer ref) + Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) Nothing -> waitLoop asyncHandle ref wAuxPid - - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a = do + ------------------- + waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) + waitOne a' = do m <- receiveTimeout 0 [ match (\CancelWait -> return AsyncPending)] case m of - Nothing -> check a -- if we timed out, check the result again + Nothing -> check a' -- if we timed out, check the result again Just _ -> return m -- (Just CancelWait) means we're done here -- note [wait loop] @@ -208,8 +227,16 @@ waitTimeout t hAsync = do -- Firstly, we spawn a /waiter/ process so that timing out is simple. -- Doing this by creating a loop in the caller's process is just a mess, -- as should be obvious from the necessary type signature once you try it. +-- -- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. +-- to see if the AsyncResult is ready yet. Once we finally get a result back +-- from 'check', we signal the caller's process with "finished" after which +-- they can pull the results without delay by calling 'check' again. +-- +-- This logic can easily be made to block indefinitely by simply not setting up +-- a timer to signal us with @CancelWait@ - remember to pass an /empty/ +-- process identifier (i.e., nullProcessId) in that case however, otherwise +-- the call to 'cancelTimer' might crash. -- | Cancel an asynchronous operation. The cancellation method to be used -- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or From 59ac6f323c74c5b1e59bf31e8fae6d339d8c32c4 Mon Sep 17 00:00:00 2001 From: Simon Marlow Date: Wed, 12 Dec 2012 13:40:11 +0000 Subject: [PATCH 0507/2357] Improvements to the redirection of logging Now we do this by communicating with the SlaveController rather than using reregisterRemoteAsync. It now works even when the master is killed and restarted, which it didn't before (because the reregister would fail). I also improved startup time by removing one call to findSlaves, improved the exception safety, and generally did some tidying up. --- .../Process/Backend/SimpleLocalnet.hs | 110 +++++++++++++----- 1 file changed, 78 insertions(+), 32 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index ff6e8a1e..3409eda5 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -104,7 +104,7 @@ import Data.Foldable (forM_) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Exception (throw) -import Control.Monad (forever, forM, replicateM) +import Control.Monad (forever, forM, replicateM, when, replicateM_) import Control.Monad.IO.Class (liftIO) import Control.Concurrent (forkIO, threadDelay, ThreadId) import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_) @@ -112,26 +112,36 @@ import Control.Distributed.Process ( RemoteTable , NodeId , Process + , ProcessId , WhereIsReply(..) , whereis , whereisRemoteAsync + , registerRemoteAsync , reregisterRemoteAsync , getSelfPid , register + , reregister , expect , nsendRemote , receiveWait + , receiveTimeout , match , matchIf , processNodeId , monitorNode + , monitor , unmonitor , NodeMonitorNotification(..) + , ProcessRegistrationException , finally , newChan , receiveChan , nsend , SendPort + , RegisterReply(..) + , bracket + , try + , send ) import qualified Control.Distributed.Process.Node as Node ( LocalNode @@ -157,7 +167,7 @@ data Backend = Backend { , findPeers :: Int -> IO [NodeId] -- | Make sure that all log messages are printed by the logger on the -- current node - , redirectLogsHere :: Process () + , redirectLogsHere :: [ProcessId] -> Process () } data BackendState = BackendState { @@ -244,12 +254,27 @@ peerDiscoveryDaemon backendState recv send = forever go -------------------------------------------------------------------------------- -- | Make sure that all log messages are printed by the logger on this node -apiRedirectLogsHere :: Backend -> Process () -apiRedirectLogsHere backend = do +apiRedirectLogsHere :: Backend -> [ProcessId] -> Process () +apiRedirectLogsHere _backend slavecontrollers = do mLogger <- whereis "logger" + myPid <- getSelfPid + forM_ mLogger $ \logger -> do - nids <- liftIO $ findPeers backend 1000000 - forM_ nids $ \nid -> reregisterRemoteAsync nid "logger" logger -- ignore async response + + bracket + (mapM monitor slavecontrollers) + (mapM unmonitor) + $ \_ -> do + + -- fire off redirect requests + forM_ slavecontrollers $ \pid -> send pid (RedirectLogsTo logger myPid) + + -- Wait for the replies + replicateM_ (length slavecontrollers) $ do + receiveWait + [ match (\(RedirectLogsReply from ok) -> return ()) + , match (\m@(NodeMonitorNotification {}) -> return ()) + ] -------------------------------------------------------------------------------- -- Slaves -- @@ -259,18 +284,31 @@ apiRedirectLogsHere backend = do -- -- This datatype is not exposed; instead, we expose primitives for dealing -- with slaves. -data SlaveControllerMsg = - SlaveTerminate +data SlaveControllerMsg + = SlaveTerminate + | RedirectLogsTo ProcessId ProcessId deriving (Typeable, Show) instance Binary SlaveControllerMsg where put SlaveTerminate = putWord8 0 + put (RedirectLogsTo a b) = do putWord8 1; put (a,b) get = do header <- getWord8 case header of 0 -> return SlaveTerminate + 1 -> do (a,b) <- get; return (RedirectLogsTo a b) _ -> fail "SlaveControllerMsg.get: invalid" +data RedirectLogsReply + = RedirectLogsReply ProcessId Bool + deriving (Typeable, Show) + +instance Binary RedirectLogsReply where + put (RedirectLogsReply from ok) = put (from,ok) + get = do + (from,ok) <- get + return (RedirectLogsReply from ok) + -- | Calling 'slave' sets up a new local node and then waits. You start -- processes on the slave by calling 'spawn' from other nodes. -- @@ -292,41 +330,49 @@ slaveController = do msg <- expect case msg of SlaveTerminate -> return () + RedirectLogsTo loggerPid from -> do + r <- try (reregister "logger" loggerPid) + ok <- case (r :: Either ProcessRegistrationException ()) of + Right _ -> return True + Left _ -> do + r <- try (register "logger" loggerPid) + case (r :: Either ProcessRegistrationException ()) of + Right _ -> return True + Left _ -> return False + pid <- getSelfPid + send from (RedirectLogsReply pid ok) + go -- | Terminate the slave at the given node ID terminateSlave :: NodeId -> Process () terminateSlave nid = nsendRemote nid "slaveController" SlaveTerminate -- | Find slave nodes -findSlaves :: Backend -> Process [NodeId] +findSlaves :: Backend -> Process [ProcessId] findSlaves backend = do nodes <- liftIO $ findPeers backend 1000000 - -- Fire of asynchronous requests for the slave controller - refs <- forM nodes $ \nid -> do - whereisRemoteAsync nid "slaveController" - ref <- monitorNode nid - return (nid, ref) - -- Wait for the replies - catMaybes <$> replicateM (length nodes) ( - receiveWait - [ matchIf (\(WhereIsReply label _) -> label == "slaveController") - (\(WhereIsReply _ mPid) -> - case mPid of - Nothing -> - return Nothing - Just pid -> do - let nid = processNodeId pid - Just ref = lookup nid refs - unmonitor ref - return (Just nid)) - , match (\(NodeMonitorNotification {}) -> return Nothing) - ]) + -- Fire off asynchronous requests for the slave controller + + bracket + (mapM monitorNode nodes) + (mapM unmonitor) + $ \_ -> do + + -- fire off whereis requests + forM nodes $ \nid -> whereisRemoteAsync nid "slaveController" + + -- Wait for the replies + catMaybes <$> replicateM (length nodes) ( + receiveWait + [ match (\(WhereIsReply "slaveController" mPid) -> return mPid) + , match (\(NodeMonitorNotification {}) -> return Nothing) + ]) -- | Terminate all slaves terminateAllSlaves :: Backend -> Process () terminateAllSlaves backend = do slaves <- findSlaves backend - forM_ slaves terminateSlave + forM_ slaves $ \pid -> send pid SlaveTerminate liftIO $ threadDelay 1000000 -------------------------------------------------------------------------------- @@ -353,8 +399,8 @@ startMaster backend proc = do node <- newLocalNode backend Node.runProcess node $ do slaves <- findSlaves backend - redirectLogsHere backend - proc slaves `finally` shutdownLogger + redirectLogsHere backend slaves + proc (map processNodeId slaves) `finally` shutdownLogger -- -- | shut down the logger process. This ensures that any pending From 9a190a3f4df2bb46900575fade5e821ed640a26f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:42:04 +0000 Subject: [PATCH 0508/2357] refactor async to use typed channels instead of MVar --- src/Control/Distributed/Platform/Async.hs | 226 +++++++++------------- tests/TestAsync.hs | 42 ++-- tests/TestMain.hs | 19 +- 3 files changed, 119 insertions(+), 168 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index d7588b79..a5c168ab 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,22 +1,22 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE StandaloneDeriving #-} module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId - , SpawnAsync + , AsyncTask , AsyncCancel , AsyncData - , Async() + , Async(worker) , AsyncResult(..) -- functions for starting/spawning , async -- and stopping/killing , cancel - , cancelAsync , cancelWait -- functions to query an async-result , poll @@ -27,14 +27,15 @@ module Control.Distributed.Platform.Async ) where import Control.Concurrent.MVar -import Control.Distributed.Platform +import Control.Distributed.Platform.Timer ( sendAfter , cancelTimer + , intervalToMs , TimerRef - , TimeInterval() ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) + , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Internal.Types @@ -42,6 +43,9 @@ import Control.Distributed.Process.Internal.Types ) import Control.Distributed.Process.Serializable +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) import Data.Maybe ( fromMaybe ) @@ -59,18 +63,25 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an 'AsyncGathererId' (to which replies should be --- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions 'AsyncWorkerId' in the @Process@ monad. -type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId +-- | A task to be performed asynchronously. This can either take the +-- form of an action that runs over some type @a@ in the @Process@ monad, +-- or a tuple that adds the node on which the asynchronous task should be +-- spawned - in the @Process a@ case the task is spawned on the local node +type AsyncTask a = Process a type AsyncData a = MVar (AsyncResult a) +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -- | An asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async AsyncRef AsyncRef (AsyncData a) +data Async a = Async { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } -- | Represents the result of an asynchronous action, which can be in several -- states at any given time. @@ -79,6 +90,19 @@ data AsyncResult a = | AsyncFailed DiedReason -- | a failed action and the failure reason | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) + deriving (Typeable) +$(derive makeBinary ''AsyncResult) + +deriving instance Eq a => Eq (AsyncResult a) + +deriving instance Show a => Show (AsyncResult a) + +--instance (Eq a) => Eq (AsyncResult a) where +-- (AsyncDone x) == (AsyncDone x') = x == x' +-- (AsyncFailed r) == (AsyncFailed r') = r == r' +-- AsyncCancelled == AsyncCancelled = True +-- AsyncPending == AsyncPending = True +-- _ == _ = False -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. @@ -99,53 +123,52 @@ type AsyncCancel = AsyncRef -> Process () -- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual -- @AsyncFailed DiedNormal@ which would normally result from this scenario. -- -async :: (Serializable a) => SpawnAsync -> Process (Async a) -async spawnF = do - mv <- liftIO $ newEmptyMVar - (wpid, gpid) <- spawnWorkers spawnF mv - return (Async wpid gpid mv) - where - spawnWorkers :: (Serializable a) => - SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) - spawnWorkers sp ad = do - root <- getSelfPid - - -- listener/response proxy - gpid <- spawnLocal $ do - proxy <- getSelfPid - worker <- sp proxy - - send root worker -- let the parent process know the worker pid - - monRef <- monitor worker - finally (pollUntilExit worker monRef ad) (unmonitor monRef) - - wpid <- expect - return (wpid, gpid) +async :: (Serializable a) => AsyncTask a -> Process (Async a) +async t = do + (wpid, gpid, chan) <- spawnWorkers t + return Async { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) => + AsyncTask a -> + Process (AsyncRef, AsyncRef, (InternalChannel a)) +spawnWorkers task = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + monRef <- monitor workerPid + finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> AsyncData a -> Process () - pollUntilExit pid ref ad = do + ProcessId -> MonitorRef -> InternalChannel a -> Process () + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\x -> return (Left x)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left a -> liftIO $ putMVar ad (AsyncDone a) - Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] - Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left CancelWait -> sendChan replyTo AsyncCancelled + Right DiedNormal -> return () + Right d -> sendChan replyTo (AsyncFailed d) --- note [recursion] --- We recurse /just once/ if we've seen a normal exit from our worker. We're --- absolutely sure about this, because once we've seen DiedNormal for the --- monitored process, it's not possible to see another monitor signal for it. --- Based on this, the only other kinds of message that can arrive are the --- return value from the worker or a cancellation from the coordinating process. - -- | Check whether an 'Async' has completed yet. The status of the asynchronous -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other @@ -153,15 +176,16 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = - liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) + AsyncPending -> return Nothing + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -175,12 +199,7 @@ waitCheckTimeout t hAsync = -- value. The outcome of the action is encoded as an 'AsyncResult'. -- wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = - nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) - where nullTimerRef :: Process TimerRef - nullTimerRef = do - nid <- getSelfNode - return (nullProcessId nid) +wait hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within @@ -189,83 +208,18 @@ wait hAsync = -- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = do - self <- getSelfPid - ar <- poll hAsync - case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync - _ -> return (Just ar) - --- | Private /wait/ implementation. -waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) -waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - where - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid - ------------------- - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle tRef wAuxPid = do - m <- waitOne asyncHandle -- note [wait loop] - case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) - Nothing -> waitLoop asyncHandle ref wAuxPid - ------------------- - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a' = do - m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending)] - case m of - Nothing -> check a' -- if we timed out, check the result again - Just _ -> return m -- (Just CancelWait) means we're done here - --- note [wait loop] --- This logic is a bit spaghetti-like so a little explanation: --- Firstly, we spawn a /waiter/ process so that timing out is simple. --- Doing this by creating a loop in the caller's process is just a mess, --- as should be obvious from the necessary type signature once you try it. --- --- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. Once we finally get a result back --- from 'check', we signal the caller's process with "finished" after which --- they can pull the results without delay by calling 'check' again. +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: Async a -> Process () +cancel (Async _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- --- This logic can easily be made to block indefinitely by simply not setting up --- a timer to signal us with @CancelWait@ - remember to pass an /empty/ --- process identifier (i.e., nullProcessId) in that case however, otherwise --- the call to 'cancelTimer' might crash. - --- | Cancel an asynchronous operation. The cancellation method to be used --- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or --- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the --- same way that message passing is asynchronous, whilst the former will block --- until a @ProcessMonitorNotification@ is received for all participants in the --- @Async@ action. -cancel :: Async a -> AsyncCancel -> Process () -cancel (Async w g d) asyncCancel = do - asyncCancel w - asyncCancel g - liftIO $ tryPutMVar d AsyncCancelled >> return () - --- | Given an @AsyncRef@, will kill the associated process. This call returns --- immediately. -cancelAsync :: AsyncCancel -cancelAsync = (flip kill) "cancelled" - --- | Given an @AsyncRef@, will kill the associated process and block until --- a @ProcessMonitorNotification@ is received, confirming that the process has --- indeed died. Passing an @AsyncRef@ for a process that has already died is --- not an error and will not block, so long as the monitor implementation --- continues to support this. -cancelWait :: AsyncCancel -cancelWait pid = do - ref <- monitor pid - cancelAsync pid - receiveWait [ - matchIf (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid' == pid) - (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () - \ No newline at end of file +cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index a6810bed..fe1a7b37 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -4,49 +4,39 @@ module TestAsync where import Prelude hiding (catch) -import Data.Binary (Binary(..)) -import Data.Typeable (Typeable) -import Data.DeriveTH -import Control.Monad (forever) -import Data.Maybe (fromMaybe) -import Control.Concurrent.MVar - ( MVar - , newEmptyMVar - , putMVar - , takeMVar - , withMVar - , tryTakeMVar - ) --- import Control.Applicative ((<$>), (<*>), pure, (<|>)) +import Data.Binary() +import Data.Typeable() import qualified Network.Transport as NT (Transport) import Network.Transport.TCP (TransportInternals) import Control.Distributed.Process -import Control.Distributed.Platform import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable(Serializable) -import Control.Distributed.Platform.Async +import Control.Distributed.Process.Serializable() import Control.Distributed.Platform +import Control.Distributed.Platform.Async -import Test.HUnit (Assertion) import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) -import Test.HUnit.Base (assertBool) import TestUtils -testAsyncPoll :: TestResult Bool -> Process () -testAsyncPoll = do - self <- getSelfPid - async $ do - return () - +testAsyncPoll :: TestResult (AsyncResult Ping) -> Process () +testAsyncPoll result = do + hAsync <- async $ say "task is running" >> return Ping + sleep $ seconds 1 + + ar <- poll hAsync + case ar of + AsyncPending -> + testProcessGo (worker hAsync) >> wait hAsync >>= \x -> stash result x + _ -> stash result ar >> return () + tests :: LocalNode -> [Test] tests localNode = [ testGroup "Async Tests" [ testCase "testAsyncPoll" (delayedAssertion "expected poll to return something useful" - localNode True testAsyncPoll) + localNode (AsyncDone Ping) testAsyncPoll) ] ] diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 25fe40ea..cec62740 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,18 +6,25 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT -import Network.Transport.TCP --- import TestGenServer (genServerTests) -import TestTimer (timerTests) +import Test.Framework + ( Test + , defaultMain + , testGroup + ) +import Network.Transport.TCP +-- import TestGenServer (genServerTests) +import TestTimer (timerTests) +import TestAsync (asyncTests) tests :: NT.Transport -> TransportInternals -> IO [Test] tests transport internals = do -- gsTestGroup <- genServerTests transport internals - timerTestGroup <- timerTests transport internals + asyncTestGroup <- asyncTests transport internals + timerTestGroup <- timerTests transport internals return [ - testGroup "Timer" timerTestGroup ] + testGroup "Async" asyncTestGroup + , testGroup "Timer" timerTestGroup ] -- , testGroup "GenServer" gsTestGroup ] main :: IO () From 4a93595e152a6d4f74e7cd78e03a472d4fb1dfa5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:42:04 +0000 Subject: [PATCH 0509/2357] refactor async to use typed channels instead of MVar --- src/Control/Distributed/Platform/Async.hs | 226 +++++++++------------- tests/TestMain.hs | 19 +- tests/TestUtils.hs | 62 ++++-- 3 files changed, 149 insertions(+), 158 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index d7588b79..a5c168ab 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,22 +1,22 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE StandaloneDeriving #-} module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId - , SpawnAsync + , AsyncTask , AsyncCancel , AsyncData - , Async() + , Async(worker) , AsyncResult(..) -- functions for starting/spawning , async -- and stopping/killing , cancel - , cancelAsync , cancelWait -- functions to query an async-result , poll @@ -27,14 +27,15 @@ module Control.Distributed.Platform.Async ) where import Control.Concurrent.MVar -import Control.Distributed.Platform +import Control.Distributed.Platform.Timer ( sendAfter , cancelTimer + , intervalToMs , TimerRef - , TimeInterval() ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) + , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Internal.Types @@ -42,6 +43,9 @@ import Control.Distributed.Process.Internal.Types ) import Control.Distributed.Process.Serializable +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) import Data.Maybe ( fromMaybe ) @@ -59,18 +63,25 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an 'AsyncGathererId' (to which replies should be --- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions 'AsyncWorkerId' in the @Process@ monad. -type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId +-- | A task to be performed asynchronously. This can either take the +-- form of an action that runs over some type @a@ in the @Process@ monad, +-- or a tuple that adds the node on which the asynchronous task should be +-- spawned - in the @Process a@ case the task is spawned on the local node +type AsyncTask a = Process a type AsyncData a = MVar (AsyncResult a) +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -- | An asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async AsyncRef AsyncRef (AsyncData a) +data Async a = Async { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } -- | Represents the result of an asynchronous action, which can be in several -- states at any given time. @@ -79,6 +90,19 @@ data AsyncResult a = | AsyncFailed DiedReason -- | a failed action and the failure reason | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) + deriving (Typeable) +$(derive makeBinary ''AsyncResult) + +deriving instance Eq a => Eq (AsyncResult a) + +deriving instance Show a => Show (AsyncResult a) + +--instance (Eq a) => Eq (AsyncResult a) where +-- (AsyncDone x) == (AsyncDone x') = x == x' +-- (AsyncFailed r) == (AsyncFailed r') = r == r' +-- AsyncCancelled == AsyncCancelled = True +-- AsyncPending == AsyncPending = True +-- _ == _ = False -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. @@ -99,53 +123,52 @@ type AsyncCancel = AsyncRef -> Process () -- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual -- @AsyncFailed DiedNormal@ which would normally result from this scenario. -- -async :: (Serializable a) => SpawnAsync -> Process (Async a) -async spawnF = do - mv <- liftIO $ newEmptyMVar - (wpid, gpid) <- spawnWorkers spawnF mv - return (Async wpid gpid mv) - where - spawnWorkers :: (Serializable a) => - SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) - spawnWorkers sp ad = do - root <- getSelfPid - - -- listener/response proxy - gpid <- spawnLocal $ do - proxy <- getSelfPid - worker <- sp proxy - - send root worker -- let the parent process know the worker pid - - monRef <- monitor worker - finally (pollUntilExit worker monRef ad) (unmonitor monRef) - - wpid <- expect - return (wpid, gpid) +async :: (Serializable a) => AsyncTask a -> Process (Async a) +async t = do + (wpid, gpid, chan) <- spawnWorkers t + return Async { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) => + AsyncTask a -> + Process (AsyncRef, AsyncRef, (InternalChannel a)) +spawnWorkers task = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + monRef <- monitor workerPid + finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> AsyncData a -> Process () - pollUntilExit pid ref ad = do + ProcessId -> MonitorRef -> InternalChannel a -> Process () + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\x -> return (Left x)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left a -> liftIO $ putMVar ad (AsyncDone a) - Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] - Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left CancelWait -> sendChan replyTo AsyncCancelled + Right DiedNormal -> return () + Right d -> sendChan replyTo (AsyncFailed d) --- note [recursion] --- We recurse /just once/ if we've seen a normal exit from our worker. We're --- absolutely sure about this, because once we've seen DiedNormal for the --- monitored process, it's not possible to see another monitor signal for it. --- Based on this, the only other kinds of message that can arrive are the --- return value from the worker or a cancellation from the coordinating process. - -- | Check whether an 'Async' has completed yet. The status of the asynchronous -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other @@ -153,15 +176,16 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = - liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) + AsyncPending -> return Nothing + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -175,12 +199,7 @@ waitCheckTimeout t hAsync = -- value. The outcome of the action is encoded as an 'AsyncResult'. -- wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = - nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) - where nullTimerRef :: Process TimerRef - nullTimerRef = do - nid <- getSelfNode - return (nullProcessId nid) +wait hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within @@ -189,83 +208,18 @@ wait hAsync = -- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = do - self <- getSelfPid - ar <- poll hAsync - case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync - _ -> return (Just ar) - --- | Private /wait/ implementation. -waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) -waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - where - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid - ------------------- - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle tRef wAuxPid = do - m <- waitOne asyncHandle -- note [wait loop] - case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) - Nothing -> waitLoop asyncHandle ref wAuxPid - ------------------- - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a' = do - m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending)] - case m of - Nothing -> check a' -- if we timed out, check the result again - Just _ -> return m -- (Just CancelWait) means we're done here - --- note [wait loop] --- This logic is a bit spaghetti-like so a little explanation: --- Firstly, we spawn a /waiter/ process so that timing out is simple. --- Doing this by creating a loop in the caller's process is just a mess, --- as should be obvious from the necessary type signature once you try it. --- --- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. Once we finally get a result back --- from 'check', we signal the caller's process with "finished" after which --- they can pull the results without delay by calling 'check' again. +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: Async a -> Process () +cancel (Async _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- --- This logic can easily be made to block indefinitely by simply not setting up --- a timer to signal us with @CancelWait@ - remember to pass an /empty/ --- process identifier (i.e., nullProcessId) in that case however, otherwise --- the call to 'cancelTimer' might crash. - --- | Cancel an asynchronous operation. The cancellation method to be used --- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or --- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the --- same way that message passing is asynchronous, whilst the former will block --- until a @ProcessMonitorNotification@ is received for all participants in the --- @Async@ action. -cancel :: Async a -> AsyncCancel -> Process () -cancel (Async w g d) asyncCancel = do - asyncCancel w - asyncCancel g - liftIO $ tryPutMVar d AsyncCancelled >> return () - --- | Given an @AsyncRef@, will kill the associated process. This call returns --- immediately. -cancelAsync :: AsyncCancel -cancelAsync = (flip kill) "cancelled" - --- | Given an @AsyncRef@, will kill the associated process and block until --- a @ProcessMonitorNotification@ is received, confirming that the process has --- indeed died. Passing an @AsyncRef@ for a process that has already died is --- not an error and will not block, so long as the monitor implementation --- continues to support this. -cancelWait :: AsyncCancel -cancelWait pid = do - ref <- monitor pid - cancelAsync pid - receiveWait [ - matchIf (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid' == pid) - (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () - \ No newline at end of file +cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 25fe40ea..cec62740 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,18 +6,25 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT -import Network.Transport.TCP --- import TestGenServer (genServerTests) -import TestTimer (timerTests) +import Test.Framework + ( Test + , defaultMain + , testGroup + ) +import Network.Transport.TCP +-- import TestGenServer (genServerTests) +import TestTimer (timerTests) +import TestAsync (asyncTests) tests :: NT.Transport -> TransportInternals -> IO [Test] tests transport internals = do -- gsTestGroup <- genServerTests transport internals - timerTestGroup <- timerTests transport internals + asyncTestGroup <- asyncTests transport internals + timerTestGroup <- timerTests transport internals return [ - testGroup "Timer" timerTestGroup ] + testGroup "Async" asyncTestGroup + , testGroup "Timer" timerTestGroup ] -- , testGroup "GenServer" gsTestGroup ] main :: IO () diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 209fe8e9..dcab2038 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -3,13 +3,20 @@ module TestUtils ( TestResult - , TestProcessControl + , noop + , stash + -- ping ! , Ping(Ping) + , ping + -- test process utilities + , TestProcessControl , startTestProcess + , runTestProcess + , testProcessGo + , testProcessStop + , testProcessReport , delayedAssertion , assertComplete - , noop - , stash ) where import Prelude hiding (catch) @@ -31,33 +38,53 @@ import Control.Monad (forever) import Test.HUnit (Assertion) import Test.HUnit.Base (assertBool) +-- | A mutable cell containing a test result. type TestResult a = MVar a +-- | A simple @Ping@ signal data Ping = Ping - deriving (Typeable) + deriving (Typeable, Eq, Show) $(derive makeBinary ''Ping) +ping :: ProcessId -> Process () +ping pid = send pid Ping + +-- | Control signals used to manage /test processes/ data TestProcessControl = Stop | Go | Report ProcessId deriving (Typeable) $(derive makeBinary ''TestProcessControl) +-- | Starts a test process on the local node. startTestProcess :: Process () -> Process ProcessId -startTestProcess proc = spawnLocal $ testProcess proc +startTestProcess proc = spawnLocal $ runTestProcess proc -testProcess :: Process () -> Process () -testProcess proc = forever $ do +-- | Runs a /test process/ around the supplied @proc@, which is executed +-- whenever the outer process loop receives a 'Go' signal. +runTestProcess :: Process () -> Process () +runTestProcess proc = forever $ do ctl <- expect case ctl of Stop -> terminate Go -> proc - Report p -> acquireAndRespond p - where acquireAndRespond :: ProcessId -> Process () - acquireAndRespond p = do - _ <- receiveWait [ - matchAny (\m -> forward m p) - ] - return () + Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () + +-- | Tell a /test process/ to continue executing +testProcessGo :: ProcessId -> Process () +testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go + +-- | Tell a /test process/ to stop (i.e., 'terminate') +testProcessStop :: ProcessId -> Process () +testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop + +-- | Tell a /test process/ to send a report (message) +-- back to the calling process +testProcessReport :: ProcessId -> Process () +testProcessReport pid = do + self <- getSelfPid + send pid $ Report self +-- | Run the supplied @testProc@ using an @MVar@ to collect and assert +-- against its result. Uses the supplied @note@ if the assertion fails. delayedAssertion :: (Eq a) => String -> LocalNode -> a -> (TestResult a -> Process ()) -> Assertion delayedAssertion note localNode expected testProc = do @@ -65,14 +92,17 @@ delayedAssertion note localNode expected testProc = do _ <- forkProcess localNode $ testProc result assertComplete note result expected +-- | Takes the value of @mv@ (using @takeMVar@) and asserts that it matches @a@ assertComplete :: (Eq a) => String -> MVar a -> a -> IO () assertComplete msg mv a = do - b <- takeMVar mv - assertBool msg (a == b) + b <- takeMVar mv + assertBool msg (a == b) +-- | Does exactly what it says on the tin, doing so in the @Process@ monad. noop :: Process () noop = return () +-- | Stashes a value in our 'TestResult' using @putMVar@ stash :: TestResult a -> a -> Process () stash mvar x = liftIO $ putMVar mvar x From 8e8372ad291d1514ef149ec5a0319297f185a987 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:42:04 +0000 Subject: [PATCH 0510/2357] refactor async to use typed channels instead of MVar --- src/Control/Distributed/Platform/Async.hs | 226 +++++++++------------- tests/TestMain.hs | 19 +- tests/TestUtils.hs | 62 ++++-- 3 files changed, 149 insertions(+), 158 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index d7588b79..a5c168ab 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,22 +1,22 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE StandaloneDeriving #-} module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId - , SpawnAsync + , AsyncTask , AsyncCancel , AsyncData - , Async() + , Async(worker) , AsyncResult(..) -- functions for starting/spawning , async -- and stopping/killing , cancel - , cancelAsync , cancelWait -- functions to query an async-result , poll @@ -27,14 +27,15 @@ module Control.Distributed.Platform.Async ) where import Control.Concurrent.MVar -import Control.Distributed.Platform +import Control.Distributed.Platform.Timer ( sendAfter , cancelTimer + , intervalToMs , TimerRef - , TimeInterval() ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) + , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Internal.Types @@ -42,6 +43,9 @@ import Control.Distributed.Process.Internal.Types ) import Control.Distributed.Process.Serializable +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) import Data.Maybe ( fromMaybe ) @@ -59,18 +63,25 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an 'AsyncGathererId' (to which replies should be --- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions 'AsyncWorkerId' in the @Process@ monad. -type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId +-- | A task to be performed asynchronously. This can either take the +-- form of an action that runs over some type @a@ in the @Process@ monad, +-- or a tuple that adds the node on which the asynchronous task should be +-- spawned - in the @Process a@ case the task is spawned on the local node +type AsyncTask a = Process a type AsyncData a = MVar (AsyncResult a) +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -- | An asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async AsyncRef AsyncRef (AsyncData a) +data Async a = Async { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } -- | Represents the result of an asynchronous action, which can be in several -- states at any given time. @@ -79,6 +90,19 @@ data AsyncResult a = | AsyncFailed DiedReason -- | a failed action and the failure reason | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) + deriving (Typeable) +$(derive makeBinary ''AsyncResult) + +deriving instance Eq a => Eq (AsyncResult a) + +deriving instance Show a => Show (AsyncResult a) + +--instance (Eq a) => Eq (AsyncResult a) where +-- (AsyncDone x) == (AsyncDone x') = x == x' +-- (AsyncFailed r) == (AsyncFailed r') = r == r' +-- AsyncCancelled == AsyncCancelled = True +-- AsyncPending == AsyncPending = True +-- _ == _ = False -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. @@ -99,53 +123,52 @@ type AsyncCancel = AsyncRef -> Process () -- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual -- @AsyncFailed DiedNormal@ which would normally result from this scenario. -- -async :: (Serializable a) => SpawnAsync -> Process (Async a) -async spawnF = do - mv <- liftIO $ newEmptyMVar - (wpid, gpid) <- spawnWorkers spawnF mv - return (Async wpid gpid mv) - where - spawnWorkers :: (Serializable a) => - SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) - spawnWorkers sp ad = do - root <- getSelfPid - - -- listener/response proxy - gpid <- spawnLocal $ do - proxy <- getSelfPid - worker <- sp proxy - - send root worker -- let the parent process know the worker pid - - monRef <- monitor worker - finally (pollUntilExit worker monRef ad) (unmonitor monRef) - - wpid <- expect - return (wpid, gpid) +async :: (Serializable a) => AsyncTask a -> Process (Async a) +async t = do + (wpid, gpid, chan) <- spawnWorkers t + return Async { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) => + AsyncTask a -> + Process (AsyncRef, AsyncRef, (InternalChannel a)) +spawnWorkers task = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + monRef <- monitor workerPid + finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> AsyncData a -> Process () - pollUntilExit pid ref ad = do + ProcessId -> MonitorRef -> InternalChannel a -> Process () + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\x -> return (Left x)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left a -> liftIO $ putMVar ad (AsyncDone a) - Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] - Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left CancelWait -> sendChan replyTo AsyncCancelled + Right DiedNormal -> return () + Right d -> sendChan replyTo (AsyncFailed d) --- note [recursion] --- We recurse /just once/ if we've seen a normal exit from our worker. We're --- absolutely sure about this, because once we've seen DiedNormal for the --- monitored process, it's not possible to see another monitor signal for it. --- Based on this, the only other kinds of message that can arrive are the --- return value from the worker or a cancellation from the coordinating process. - -- | Check whether an 'Async' has completed yet. The status of the asynchronous -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other @@ -153,15 +176,16 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = - liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) + AsyncPending -> return Nothing + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -175,12 +199,7 @@ waitCheckTimeout t hAsync = -- value. The outcome of the action is encoded as an 'AsyncResult'. -- wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = - nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) - where nullTimerRef :: Process TimerRef - nullTimerRef = do - nid <- getSelfNode - return (nullProcessId nid) +wait hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within @@ -189,83 +208,18 @@ wait hAsync = -- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = do - self <- getSelfPid - ar <- poll hAsync - case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync - _ -> return (Just ar) - --- | Private /wait/ implementation. -waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) -waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - where - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid - ------------------- - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle tRef wAuxPid = do - m <- waitOne asyncHandle -- note [wait loop] - case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) - Nothing -> waitLoop asyncHandle ref wAuxPid - ------------------- - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a' = do - m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending)] - case m of - Nothing -> check a' -- if we timed out, check the result again - Just _ -> return m -- (Just CancelWait) means we're done here - --- note [wait loop] --- This logic is a bit spaghetti-like so a little explanation: --- Firstly, we spawn a /waiter/ process so that timing out is simple. --- Doing this by creating a loop in the caller's process is just a mess, --- as should be obvious from the necessary type signature once you try it. --- --- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. Once we finally get a result back --- from 'check', we signal the caller's process with "finished" after which --- they can pull the results without delay by calling 'check' again. +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: Async a -> Process () +cancel (Async _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- --- This logic can easily be made to block indefinitely by simply not setting up --- a timer to signal us with @CancelWait@ - remember to pass an /empty/ --- process identifier (i.e., nullProcessId) in that case however, otherwise --- the call to 'cancelTimer' might crash. - --- | Cancel an asynchronous operation. The cancellation method to be used --- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or --- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the --- same way that message passing is asynchronous, whilst the former will block --- until a @ProcessMonitorNotification@ is received for all participants in the --- @Async@ action. -cancel :: Async a -> AsyncCancel -> Process () -cancel (Async w g d) asyncCancel = do - asyncCancel w - asyncCancel g - liftIO $ tryPutMVar d AsyncCancelled >> return () - --- | Given an @AsyncRef@, will kill the associated process. This call returns --- immediately. -cancelAsync :: AsyncCancel -cancelAsync = (flip kill) "cancelled" - --- | Given an @AsyncRef@, will kill the associated process and block until --- a @ProcessMonitorNotification@ is received, confirming that the process has --- indeed died. Passing an @AsyncRef@ for a process that has already died is --- not an error and will not block, so long as the monitor implementation --- continues to support this. -cancelWait :: AsyncCancel -cancelWait pid = do - ref <- monitor pid - cancelAsync pid - receiveWait [ - matchIf (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid' == pid) - (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () - \ No newline at end of file +cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync diff --git a/tests/TestMain.hs b/tests/TestMain.hs index 25fe40ea..cec62740 100644 --- a/tests/TestMain.hs +++ b/tests/TestMain.hs @@ -6,18 +6,25 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where -import Test.Framework (Test, defaultMain, testGroup) import qualified Network.Transport as NT -import Network.Transport.TCP --- import TestGenServer (genServerTests) -import TestTimer (timerTests) +import Test.Framework + ( Test + , defaultMain + , testGroup + ) +import Network.Transport.TCP +-- import TestGenServer (genServerTests) +import TestTimer (timerTests) +import TestAsync (asyncTests) tests :: NT.Transport -> TransportInternals -> IO [Test] tests transport internals = do -- gsTestGroup <- genServerTests transport internals - timerTestGroup <- timerTests transport internals + asyncTestGroup <- asyncTests transport internals + timerTestGroup <- timerTests transport internals return [ - testGroup "Timer" timerTestGroup ] + testGroup "Async" asyncTestGroup + , testGroup "Timer" timerTestGroup ] -- , testGroup "GenServer" gsTestGroup ] main :: IO () diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 209fe8e9..dcab2038 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -3,13 +3,20 @@ module TestUtils ( TestResult - , TestProcessControl + , noop + , stash + -- ping ! , Ping(Ping) + , ping + -- test process utilities + , TestProcessControl , startTestProcess + , runTestProcess + , testProcessGo + , testProcessStop + , testProcessReport , delayedAssertion , assertComplete - , noop - , stash ) where import Prelude hiding (catch) @@ -31,33 +38,53 @@ import Control.Monad (forever) import Test.HUnit (Assertion) import Test.HUnit.Base (assertBool) +-- | A mutable cell containing a test result. type TestResult a = MVar a +-- | A simple @Ping@ signal data Ping = Ping - deriving (Typeable) + deriving (Typeable, Eq, Show) $(derive makeBinary ''Ping) +ping :: ProcessId -> Process () +ping pid = send pid Ping + +-- | Control signals used to manage /test processes/ data TestProcessControl = Stop | Go | Report ProcessId deriving (Typeable) $(derive makeBinary ''TestProcessControl) +-- | Starts a test process on the local node. startTestProcess :: Process () -> Process ProcessId -startTestProcess proc = spawnLocal $ testProcess proc +startTestProcess proc = spawnLocal $ runTestProcess proc -testProcess :: Process () -> Process () -testProcess proc = forever $ do +-- | Runs a /test process/ around the supplied @proc@, which is executed +-- whenever the outer process loop receives a 'Go' signal. +runTestProcess :: Process () -> Process () +runTestProcess proc = forever $ do ctl <- expect case ctl of Stop -> terminate Go -> proc - Report p -> acquireAndRespond p - where acquireAndRespond :: ProcessId -> Process () - acquireAndRespond p = do - _ <- receiveWait [ - matchAny (\m -> forward m p) - ] - return () + Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () + +-- | Tell a /test process/ to continue executing +testProcessGo :: ProcessId -> Process () +testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go + +-- | Tell a /test process/ to stop (i.e., 'terminate') +testProcessStop :: ProcessId -> Process () +testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop + +-- | Tell a /test process/ to send a report (message) +-- back to the calling process +testProcessReport :: ProcessId -> Process () +testProcessReport pid = do + self <- getSelfPid + send pid $ Report self +-- | Run the supplied @testProc@ using an @MVar@ to collect and assert +-- against its result. Uses the supplied @note@ if the assertion fails. delayedAssertion :: (Eq a) => String -> LocalNode -> a -> (TestResult a -> Process ()) -> Assertion delayedAssertion note localNode expected testProc = do @@ -65,14 +92,17 @@ delayedAssertion note localNode expected testProc = do _ <- forkProcess localNode $ testProc result assertComplete note result expected +-- | Takes the value of @mv@ (using @takeMVar@) and asserts that it matches @a@ assertComplete :: (Eq a) => String -> MVar a -> a -> IO () assertComplete msg mv a = do - b <- takeMVar mv - assertBool msg (a == b) + b <- takeMVar mv + assertBool msg (a == b) +-- | Does exactly what it says on the tin, doing so in the @Process@ monad. noop :: Process () noop = return () +-- | Stashes a value in our 'TestResult' using @putMVar@ stash :: TestResult a -> a -> Process () stash mvar x = liftIO $ putMVar mvar x From 7eed6ece096f26afda14b3594f6b5a26d9d5599e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:42:04 +0000 Subject: [PATCH 0511/2357] refactor async to use typed channels instead of MVar --- src/Control/Distributed/Platform/Async.hs | 226 +++++++++------------- 1 file changed, 90 insertions(+), 136 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index d7588b79..a5c168ab 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,22 +1,22 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE StandaloneDeriving #-} module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId - , SpawnAsync + , AsyncTask , AsyncCancel , AsyncData - , Async() + , Async(worker) , AsyncResult(..) -- functions for starting/spawning , async -- and stopping/killing , cancel - , cancelAsync , cancelWait -- functions to query an async-result , poll @@ -27,14 +27,15 @@ module Control.Distributed.Platform.Async ) where import Control.Concurrent.MVar -import Control.Distributed.Platform +import Control.Distributed.Platform.Timer ( sendAfter , cancelTimer + , intervalToMs , TimerRef - , TimeInterval() ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) + , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Internal.Types @@ -42,6 +43,9 @@ import Control.Distributed.Process.Internal.Types ) import Control.Distributed.Process.Serializable +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) import Data.Maybe ( fromMaybe ) @@ -59,18 +63,25 @@ type AsyncWorkerId = AsyncRef -- | A reference to an asynchronous "gatherer" type AsyncGathererId = AsyncRef --- | A function that takes an 'AsyncGathererId' (to which replies should be --- sent) and spawns an asynchronous (user defined) action, returning the --- spawned actions 'AsyncWorkerId' in the @Process@ monad. -type SpawnAsync = AsyncGathererId -> Process AsyncWorkerId +-- | A task to be performed asynchronously. This can either take the +-- form of an action that runs over some type @a@ in the @Process@ monad, +-- or a tuple that adds the node on which the asynchronous task should be +-- spawned - in the @Process a@ case the task is spawned on the local node +type AsyncTask a = Process a type AsyncData a = MVar (AsyncResult a) +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -- | An asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async AsyncRef AsyncRef (AsyncData a) +data Async a = Async { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } -- | Represents the result of an asynchronous action, which can be in several -- states at any given time. @@ -79,6 +90,19 @@ data AsyncResult a = | AsyncFailed DiedReason -- | a failed action and the failure reason | AsyncCancelled -- | a cancelled action | AsyncPending -- | a pending action (that is still running) + deriving (Typeable) +$(derive makeBinary ''AsyncResult) + +deriving instance Eq a => Eq (AsyncResult a) + +deriving instance Show a => Show (AsyncResult a) + +--instance (Eq a) => Eq (AsyncResult a) where +-- (AsyncDone x) == (AsyncDone x') = x == x' +-- (AsyncFailed r) == (AsyncFailed r') = r == r' +-- AsyncCancelled == AsyncCancelled = True +-- AsyncPending == AsyncPending = True +-- _ == _ = False -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. @@ -99,53 +123,52 @@ type AsyncCancel = AsyncRef -> Process () -- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual -- @AsyncFailed DiedNormal@ which would normally result from this scenario. -- -async :: (Serializable a) => SpawnAsync -> Process (Async a) -async spawnF = do - mv <- liftIO $ newEmptyMVar - (wpid, gpid) <- spawnWorkers spawnF mv - return (Async wpid gpid mv) - where - spawnWorkers :: (Serializable a) => - SpawnAsync -> AsyncData a -> Process (AsyncRef, AsyncRef) - spawnWorkers sp ad = do - root <- getSelfPid - - -- listener/response proxy - gpid <- spawnLocal $ do - proxy <- getSelfPid - worker <- sp proxy - - send root worker -- let the parent process know the worker pid - - monRef <- monitor worker - finally (pollUntilExit worker monRef ad) (unmonitor monRef) - - wpid <- expect - return (wpid, gpid) +async :: (Serializable a) => AsyncTask a -> Process (Async a) +async t = do + (wpid, gpid, chan) <- spawnWorkers t + return Async { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) => + AsyncTask a -> + Process (AsyncRef, AsyncRef, (InternalChannel a)) +spawnWorkers task = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + monRef <- monitor workerPid + finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> AsyncData a -> Process () - pollUntilExit pid ref ad = do + ProcessId -> MonitorRef -> InternalChannel a -> Process () + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\x -> return (Left x)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left a -> liftIO $ putMVar ad (AsyncDone a) - Right DiedNormal -> pollUntilExit pid ref ad -- note [recursion] - Right d -> liftIO $ putMVar ad (AsyncFailed d) + Left CancelWait -> sendChan replyTo AsyncCancelled + Right DiedNormal -> return () + Right d -> sendChan replyTo (AsyncFailed d) --- note [recursion] --- We recurse /just once/ if we've seen a normal exit from our worker. We're --- absolutely sure about this, because once we've seen DiedNormal for the --- monitored process, it's not possible to see another monitor signal for it. --- Based on this, the only other kinds of message that can arrive are the --- return value from the worker or a cancellation from the coordinating process. - -- | Check whether an 'Async' has completed yet. The status of the asynchronous -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other @@ -153,15 +176,16 @@ async spawnF = do -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll (Async _ _ d) = - liftIO $ tryTakeMVar d >>= return . fromMaybe (AsyncPending) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) + AsyncPending -> return Nothing + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -175,12 +199,7 @@ waitCheckTimeout t hAsync = -- value. The outcome of the action is encoded as an 'AsyncResult'. -- wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = - nullTimerRef >>= waitAux hAsync >>= return . fromMaybe (AsyncPending) - where nullTimerRef :: Process TimerRef - nullTimerRef = do - nid <- getSelfNode - return (nullProcessId nid) +wait hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within @@ -189,83 +208,18 @@ wait hAsync = -- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = do - self <- getSelfPid - ar <- poll hAsync - case ar of - AsyncPending -> sendAfter t self CancelWait >>= waitAux hAsync - _ -> return (Just ar) - --- | Private /wait/ implementation. -waitAux :: (Serializable a) => - Async a -> TimerRef -> Process (Maybe (AsyncResult a)) -waitAux a ref = - getSelfPid >>= spawnWait a ref >> (do "finished" <- expect; check a) - where - spawnWait :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process (ProcessId) - spawnWait a' ref' pid = spawnLocal $ waitLoop a' ref' pid - ------------------- - waitLoop :: (Serializable a) => - Async a -> TimerRef -> ProcessId -> Process () - waitLoop asyncHandle tRef wAuxPid = do - m <- waitOne asyncHandle -- note [wait loop] - case m of - Just _ -> finally (send wAuxPid "finished") (cancelTimer tRef) - Nothing -> waitLoop asyncHandle ref wAuxPid - ------------------- - waitOne :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) - waitOne a' = do - m <- receiveTimeout 0 [ - match (\CancelWait -> return AsyncPending)] - case m of - Nothing -> check a' -- if we timed out, check the result again - Just _ -> return m -- (Just CancelWait) means we're done here - --- note [wait loop] --- This logic is a bit spaghetti-like so a little explanation: --- Firstly, we spawn a /waiter/ process so that timing out is simple. --- Doing this by creating a loop in the caller's process is just a mess, --- as should be obvious from the necessary type signature once you try it. --- --- Instead, the /waiter/ queries its mailbox continually and defers to 'check' --- to see if the AsyncResult is ready yet. Once we finally get a result back --- from 'check', we signal the caller's process with "finished" after which --- they can pull the results without delay by calling 'check' again. +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: Async a -> Process () +cancel (Async _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- --- This logic can easily be made to block indefinitely by simply not setting up --- a timer to signal us with @CancelWait@ - remember to pass an /empty/ --- process identifier (i.e., nullProcessId) in that case however, otherwise --- the call to 'cancelTimer' might crash. - --- | Cancel an asynchronous operation. The cancellation method to be used --- is passed in @asyncCancel@ and can be synchronous (see 'cancelWait') or --- asynchronous (see 'cancelAsync'). The latter is truly asynchronous, in the --- same way that message passing is asynchronous, whilst the former will block --- until a @ProcessMonitorNotification@ is received for all participants in the --- @Async@ action. -cancel :: Async a -> AsyncCancel -> Process () -cancel (Async w g d) asyncCancel = do - asyncCancel w - asyncCancel g - liftIO $ tryPutMVar d AsyncCancelled >> return () - --- | Given an @AsyncRef@, will kill the associated process. This call returns --- immediately. -cancelAsync :: AsyncCancel -cancelAsync = (flip kill) "cancelled" - --- | Given an @AsyncRef@, will kill the associated process and block until --- a @ProcessMonitorNotification@ is received, confirming that the process has --- indeed died. Passing an @AsyncRef@ for a process that has already died is --- not an error and will not block, so long as the monitor implementation --- continues to support this. -cancelWait :: AsyncCancel -cancelWait pid = do - ref <- monitor pid - cancelAsync pid - receiveWait [ - matchIf (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid' == pid) - (\(ProcessMonitorNotification _ _ r) -> return r) ] >> return () - \ No newline at end of file +cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync From 9d1e518dc7edff424468f1593532c46a95201ad5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:48:34 +0000 Subject: [PATCH 0512/2357] tidy up and document --- src/Control/Distributed/Platform/Async.hs | 28 ++++++++--------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index a5c168ab..978c530e 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -10,7 +10,6 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncData , Async(worker) , AsyncResult(..) -- functions for starting/spawning @@ -28,19 +27,13 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform.Timer - ( sendAfter - , cancelTimer - , intervalToMs - , TimerRef + ( intervalToMs ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) , TimeInterval() ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( nullProcessId - ) import Control.Distributed.Process.Serializable import Data.Binary @@ -69,8 +62,7 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a -type AsyncData a = MVar (AsyncResult a) - +-- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- | An asynchronous action spawned by 'async'. @@ -94,19 +86,17 @@ data AsyncResult a = $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) - deriving instance Show a => Show (AsyncResult a) ---instance (Eq a) => Eq (AsyncResult a) where --- (AsyncDone x) == (AsyncDone x') = x == x' --- (AsyncFailed r) == (AsyncFailed r') = r == r' --- AsyncCancelled == AsyncCancelled = True --- AsyncPending == AsyncPending = True --- _ == _ = False - -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () +type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] + +-- note [local cancel only] +-- The cancellation is only ever sent to the insulator process, which is always +-- run on the local node. That could be a limitation, as there's nothing in +-- 'Async' data profile to stop it being sent remotely. At *that* point, we'd +-- need to make the cancellation remote-able too however. -- | An asynchronous action spawned by 'async' or 'withAsync'. -- Asynchronous actions are executed in a separate @Process@, and From 5d4432ac7cd8729ae3e57940cfae251813351094 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:48:34 +0000 Subject: [PATCH 0513/2357] tidy up and document --- src/Control/Distributed/Platform/Async.hs | 28 ++++++++--------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index a5c168ab..978c530e 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -10,7 +10,6 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncData , Async(worker) , AsyncResult(..) -- functions for starting/spawning @@ -28,19 +27,13 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform.Timer - ( sendAfter - , cancelTimer - , intervalToMs - , TimerRef + ( intervalToMs ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) , TimeInterval() ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( nullProcessId - ) import Control.Distributed.Process.Serializable import Data.Binary @@ -69,8 +62,7 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a -type AsyncData a = MVar (AsyncResult a) - +-- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- | An asynchronous action spawned by 'async'. @@ -94,19 +86,17 @@ data AsyncResult a = $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) - deriving instance Show a => Show (AsyncResult a) ---instance (Eq a) => Eq (AsyncResult a) where --- (AsyncDone x) == (AsyncDone x') = x == x' --- (AsyncFailed r) == (AsyncFailed r') = r == r' --- AsyncCancelled == AsyncCancelled = True --- AsyncPending == AsyncPending = True --- _ == _ = False - -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () +type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] + +-- note [local cancel only] +-- The cancellation is only ever sent to the insulator process, which is always +-- run on the local node. That could be a limitation, as there's nothing in +-- 'Async' data profile to stop it being sent remotely. At *that* point, we'd +-- need to make the cancellation remote-able too however. -- | An asynchronous action spawned by 'async' or 'withAsync'. -- Asynchronous actions are executed in a separate @Process@, and From 35e4643dafcef4797d33581a1676536a48d1b2d8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:48:34 +0000 Subject: [PATCH 0514/2357] tidy up and document --- src/Control/Distributed/Platform/Async.hs | 28 ++++++++--------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index a5c168ab..978c530e 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -10,7 +10,6 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncData , Async(worker) , AsyncResult(..) -- functions for starting/spawning @@ -28,19 +27,13 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform.Timer - ( sendAfter - , cancelTimer - , intervalToMs - , TimerRef + ( intervalToMs ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) , TimeInterval() ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( nullProcessId - ) import Control.Distributed.Process.Serializable import Data.Binary @@ -69,8 +62,7 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a -type AsyncData a = MVar (AsyncResult a) - +-- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- | An asynchronous action spawned by 'async'. @@ -94,19 +86,17 @@ data AsyncResult a = $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) - deriving instance Show a => Show (AsyncResult a) ---instance (Eq a) => Eq (AsyncResult a) where --- (AsyncDone x) == (AsyncDone x') = x == x' --- (AsyncFailed r) == (AsyncFailed r') = r == r' --- AsyncCancelled == AsyncCancelled = True --- AsyncPending == AsyncPending = True --- _ == _ = False - -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () +type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] + +-- note [local cancel only] +-- The cancellation is only ever sent to the insulator process, which is always +-- run on the local node. That could be a limitation, as there's nothing in +-- 'Async' data profile to stop it being sent remotely. At *that* point, we'd +-- need to make the cancellation remote-able too however. -- | An asynchronous action spawned by 'async' or 'withAsync'. -- Asynchronous actions are executed in a separate @Process@, and From 6ab4a5c7089384dd4fdf5c2ff0ef8ce73bd71271 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 21:48:34 +0000 Subject: [PATCH 0515/2357] tidy up and document --- src/Control/Distributed/Platform/Async.hs | 28 ++++++++--------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index a5c168ab..978c530e 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -10,7 +10,6 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncData , Async(worker) , AsyncResult(..) -- functions for starting/spawning @@ -28,19 +27,13 @@ module Control.Distributed.Platform.Async import Control.Concurrent.MVar import Control.Distributed.Platform.Timer - ( sendAfter - , cancelTimer - , intervalToMs - , TimerRef + ( intervalToMs ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) , TimeInterval() ) import Control.Distributed.Process -import Control.Distributed.Process.Internal.Types - ( nullProcessId - ) import Control.Distributed.Process.Serializable import Data.Binary @@ -69,8 +62,7 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a -type AsyncData a = MVar (AsyncResult a) - +-- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- | An asynchronous action spawned by 'async'. @@ -94,19 +86,17 @@ data AsyncResult a = $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) - deriving instance Show a => Show (AsyncResult a) ---instance (Eq a) => Eq (AsyncResult a) where --- (AsyncDone x) == (AsyncDone x') = x == x' --- (AsyncFailed r) == (AsyncFailed r') = r == r' --- AsyncCancelled == AsyncCancelled = True --- AsyncPending == AsyncPending = True --- _ == _ = False - -- | An async cancellation takes an 'AsyncRef' and does some cancellation -- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () +type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] + +-- note [local cancel only] +-- The cancellation is only ever sent to the insulator process, which is always +-- run on the local node. That could be a limitation, as there's nothing in +-- 'Async' data profile to stop it being sent remotely. At *that* point, we'd +-- need to make the cancellation remote-able too however. -- | An asynchronous action spawned by 'async' or 'withAsync'. -- Asynchronous actions are executed in a separate @Process@, and From eff67b01235f44ce977b856301626cf0e2ccb93d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:14:43 +0000 Subject: [PATCH 0516/2357] proof that channels are not such a good idea after all? --- src/Control/Distributed/Platform/Async.hs | 6 +++--- tests/TestAsync.hs | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 978c530e..392692ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do + pollUntilExit pid ref chan@(replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) + , match (\c@(CancelWait) -> return (Left c)) ] case r of - Left CancelWait -> sendChan replyTo AsyncCancelled + Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index fe1a7b37..19c60246 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -27,16 +27,30 @@ testAsyncPoll result = do ar <- poll hAsync case ar of AsyncPending -> - testProcessGo (worker hAsync) >> wait hAsync >>= \x -> stash result x + testProcessGo (worker hAsync) >> wait hAsync >>= stash result _ -> stash result ar >> return () + +testAsyncCancel :: TestResult (AsyncResult Int) -> Process () +testAsyncCancel result = do + hAsync <- async $ say "task is running" >> return 42 + sleep $ milliseconds 100 + + AsyncPending <- poll hAsync -- nasty kind of assertion: use assertEquals? + + cancel hAsync + wait hAsync >>= stash result tests :: LocalNode -> [Test] tests localNode = [ - testGroup "Async Tests" [ + testGroup "Handling async results" [ testCase "testAsyncPoll" (delayedAssertion "expected poll to return something useful" localNode (AsyncDone Ping) testAsyncPoll) + , testCase "testAsyncCancel" + (delayedAssertion + "expected async task to have been cancelled" + localNode (AsyncCancelled) testAsyncCancel) ] ] From 30dfc26a3c3cc96ef59fcd4ad6436f28a467488b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:14:43 +0000 Subject: [PATCH 0517/2357] proof that channels are not such a good idea after all? --- src/Control/Distributed/Platform/Async.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 978c530e..392692ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do + pollUntilExit pid ref chan@(replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) + , match (\c@(CancelWait) -> return (Left c)) ] case r of - Left CancelWait -> sendChan replyTo AsyncCancelled + Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) From c121e2dd0826c80a179dbf21bc08b09e18f941f3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:14:43 +0000 Subject: [PATCH 0518/2357] proof that channels are not such a good idea after all? --- src/Control/Distributed/Platform/Async.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 978c530e..392692ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do + pollUntilExit pid ref chan@(replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) + , match (\c@(CancelWait) -> return (Left c)) ] case r of - Left CancelWait -> sendChan replyTo AsyncCancelled + Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) From a42f99251b24e8ae0557b2d9c26d68675cd8e855 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:14:43 +0000 Subject: [PATCH 0519/2357] proof that channels are not such a good idea after all? --- src/Control/Distributed/Platform/Async.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 978c530e..392692ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do + pollUntilExit pid ref chan@(replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) + , match (\c@(CancelWait) -> return (Left c)) ] case r of - Left CancelWait -> sendChan replyTo AsyncCancelled + Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) From 1cd013027a34d62ff75cc61dcf01995b2d3afa89 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:36:07 +0000 Subject: [PATCH 0520/2357] and then realise that the test cases are wrong --- src/Control/Distributed/Platform/Async.hs | 8 +++---- tests/TestAsync.hs | 28 +++++++++++------------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 392692ea..88f484ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -25,7 +25,6 @@ module Control.Distributed.Platform.Async , waitCheckTimeout ) where -import Control.Concurrent.MVar import Control.Distributed.Platform.Timer ( intervalToMs ) @@ -133,6 +132,7 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task + say "task complete" sendChan (fst chan) (AsyncDone r) send root workerPid -- let the parent process know the worker pid @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref chan@(replyTo, _) = do + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> return (Left c)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan + Left CancelWait -> sendChan replyTo AsyncCancelled Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 19c60246..578b545a 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -19,35 +19,33 @@ import Test.Framework.Providers.HUnit (testCase) import TestUtils -testAsyncPoll :: TestResult (AsyncResult Ping) -> Process () +testAsyncPoll :: TestResult (AsyncResult ()) -> Process () testAsyncPoll result = do - hAsync <- async $ say "task is running" >> return Ping - sleep $ seconds 1 - + hAsync <- async $ runTestProcess $ say "task is running" >> return () ar <- poll hAsync case ar of AsyncPending -> testProcessGo (worker hAsync) >> wait hAsync >>= stash result _ -> stash result ar >> return () -testAsyncCancel :: TestResult (AsyncResult Int) -> Process () +testAsyncCancel :: TestResult (AsyncResult ()) -> Process () testAsyncCancel result = do - hAsync <- async $ say "task is running" >> return 42 + hAsync <- async $ runTestProcess $ say "task is running" >> return () sleep $ milliseconds 100 - AsyncPending <- poll hAsync -- nasty kind of assertion: use assertEquals? - - cancel hAsync - wait hAsync >>= stash result + p <- poll hAsync -- nasty kind of assertion: use assertEquals? + case p of + AsyncPending -> cancel hAsync >> wait hAsync >>= \x -> do say (show x); stash result x + _ -> say (show p) >> stash result p tests :: LocalNode -> [Test] tests localNode = [ testGroup "Handling async results" [ - testCase "testAsyncPoll" - (delayedAssertion - "expected poll to return something useful" - localNode (AsyncDone Ping) testAsyncPoll) - , testCase "testAsyncCancel" +-- testCase "testAsyncPoll" +-- (delayedAssertion +-- "expected poll to return something useful" +-- localNode (AsyncDone ()) testAsyncPoll) + testCase "testAsyncCancel" (delayedAssertion "expected async task to have been cancelled" localNode (AsyncCancelled) testAsyncCancel) From 56c0d622981ad9def0573bf322e098ec6f7ff7aa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:36:07 +0000 Subject: [PATCH 0521/2357] and then realise that the test cases are wrong --- src/Control/Distributed/Platform/Async.hs | 8 ++++---- tests/TestUtils.hs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 392692ea..88f484ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -25,7 +25,6 @@ module Control.Distributed.Platform.Async , waitCheckTimeout ) where -import Control.Concurrent.MVar import Control.Distributed.Platform.Timer ( intervalToMs ) @@ -133,6 +132,7 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task + say "task complete" sendChan (fst chan) (AsyncDone r) send root workerPid -- let the parent process know the worker pid @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref chan@(replyTo, _) = do + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> return (Left c)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan + Left CancelWait -> sendChan replyTo AsyncCancelled Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index dcab2038..1a2dfb16 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -12,6 +12,7 @@ module TestUtils , TestProcessControl , startTestProcess , runTestProcess + , withTestProcess , testProcessGo , testProcessStop , testProcessReport @@ -68,6 +69,9 @@ runTestProcess proc = forever $ do Go -> proc Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () +withTestProcess :: ProcessId -> Process () -> Process () +withTestProcess pid proc = testProcessGo pid >> proc >> testProcessStop pid + -- | Tell a /test process/ to continue executing testProcessGo :: ProcessId -> Process () testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go From ed9591d8c42e1ca4a5e5975efddfa2e112a3a38e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:36:07 +0000 Subject: [PATCH 0522/2357] and then realise that the test cases are wrong --- src/Control/Distributed/Platform/Async.hs | 8 ++++---- tests/TestUtils.hs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 392692ea..88f484ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -25,7 +25,6 @@ module Control.Distributed.Platform.Async , waitCheckTimeout ) where -import Control.Concurrent.MVar import Control.Distributed.Platform.Timer ( intervalToMs ) @@ -133,6 +132,7 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task + say "task complete" sendChan (fst chan) (AsyncDone r) send root workerPid -- let the parent process know the worker pid @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref chan@(replyTo, _) = do + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> return (Left c)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan + Left CancelWait -> sendChan replyTo AsyncCancelled Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index dcab2038..1a2dfb16 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -12,6 +12,7 @@ module TestUtils , TestProcessControl , startTestProcess , runTestProcess + , withTestProcess , testProcessGo , testProcessStop , testProcessReport @@ -68,6 +69,9 @@ runTestProcess proc = forever $ do Go -> proc Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () +withTestProcess :: ProcessId -> Process () -> Process () +withTestProcess pid proc = testProcessGo pid >> proc >> testProcessStop pid + -- | Tell a /test process/ to continue executing testProcessGo :: ProcessId -> Process () testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go From fbe21274b12161d31ea76c87a9de44c58ccc937a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:36:07 +0000 Subject: [PATCH 0523/2357] and then realise that the test cases are wrong --- src/Control/Distributed/Platform/Async.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 392692ea..88f484ea 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -25,7 +25,6 @@ module Control.Distributed.Platform.Async , waitCheckTimeout ) where -import Control.Concurrent.MVar import Control.Distributed.Platform.Timer ( intervalToMs ) @@ -133,6 +132,7 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task + say "task complete" sendChan (fst chan) (AsyncDone r) send root workerPid -- let the parent process know the worker pid @@ -146,16 +146,16 @@ spawnWorkers task = do -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref chan@(replyTo, _) = do + pollUntilExit pid ref (replyTo, _) = do r <- receiveWait [ matchIf (\(ProcessMonitorNotification ref' pid' _) -> ref' == ref && pid == pid') (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> return (Left c)) + , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) ] case r of - Left CancelWait -> kill pid "cancel" >> pollUntilExit pid ref chan + Left CancelWait -> sendChan replyTo AsyncCancelled Right DiedNormal -> return () Right d -> sendChan replyTo (AsyncFailed d) From d3377b71bc48523eedc926171334e47076644346 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 12 Dec 2012 22:44:58 +0000 Subject: [PATCH 0524/2357] don't make the sample task block and the tests will pass --- tests/TestAsync.hs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 578b545a..0e068f58 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -21,11 +21,11 @@ import TestUtils testAsyncPoll :: TestResult (AsyncResult ()) -> Process () testAsyncPoll result = do - hAsync <- async $ runTestProcess $ say "task is running" >> return () + hAsync <- async $ do "go" <- expect; say "task is running" >> return () ar <- poll hAsync case ar of AsyncPending -> - testProcessGo (worker hAsync) >> wait hAsync >>= stash result + send (worker hAsync) "go" >> wait hAsync >>= stash result _ -> stash result ar >> return () testAsyncCancel :: TestResult (AsyncResult ()) -> Process () @@ -35,20 +35,20 @@ testAsyncCancel result = do p <- poll hAsync -- nasty kind of assertion: use assertEquals? case p of - AsyncPending -> cancel hAsync >> wait hAsync >>= \x -> do say (show x); stash result x + AsyncPending -> cancel hAsync >> wait hAsync >>= stash result _ -> say (show p) >> stash result p tests :: LocalNode -> [Test] tests localNode = [ testGroup "Handling async results" [ --- testCase "testAsyncPoll" --- (delayedAssertion --- "expected poll to return something useful" --- localNode (AsyncDone ()) testAsyncPoll) - testCase "testAsyncCancel" + testCase "testAsyncCancel" (delayedAssertion "expected async task to have been cancelled" localNode (AsyncCancelled) testAsyncCancel) + , testCase "testAsyncPoll" + (delayedAssertion + "expected poll to return something useful" + localNode (AsyncDone ()) testAsyncPoll) ] ] From 282a2d97d9f1f1e9c233c838e1ab0320717ff5fa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 13 Dec 2012 11:35:28 +0000 Subject: [PATCH 0525/2357] test waiting/timeouts; ensure we don't care if worker is unresponsive --- tests/TestAsync.hs | 51 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 0e068f58..c6c01c86 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -3,25 +3,24 @@ module TestAsync where -import Prelude hiding (catch) -import Data.Binary() -import Data.Typeable() -import qualified Network.Transport as NT (Transport) -import Network.Transport.TCP (TransportInternals) import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Platform import Control.Distributed.Platform.Async +import Data.Binary() +import Data.Typeable() +import qualified Network.Transport as NT (Transport) +import Network.Transport.TCP (TransportInternals) +import Prelude hiding (catch) import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) - import TestUtils testAsyncPoll :: TestResult (AsyncResult ()) -> Process () testAsyncPoll result = do - hAsync <- async $ do "go" <- expect; say "task is running" >> return () + hAsync <- async $ do "go" <- expect; say "running" >> return () ar <- poll hAsync case ar of AsyncPending -> @@ -30,13 +29,39 @@ testAsyncPoll result = do testAsyncCancel :: TestResult (AsyncResult ()) -> Process () testAsyncCancel result = do - hAsync <- async $ runTestProcess $ say "task is running" >> return () + hAsync <- async $ runTestProcess $ say "running" >> return () sleep $ milliseconds 100 p <- poll hAsync -- nasty kind of assertion: use assertEquals? case p of AsyncPending -> cancel hAsync >> wait hAsync >>= stash result _ -> say (show p) >> stash result p + +testAsyncCancelWait :: TestResult (Maybe (AsyncResult ())) -> Process () +testAsyncCancelWait result = do + testPid <- getSelfPid + p <- spawnLocal $ do + hAsync <- async $ runTestProcess $ say "running" >> (sleep $ seconds 60) + sleep $ milliseconds 100 + + send testPid "running" + + AsyncPending <- poll hAsync + cancelWait hAsync >>= send testPid + + "running" <- expect + d <- expectTimeout (intervalToMs $ seconds 5) + case d of + Nothing -> kill p "timed out" >> stash result Nothing + Just ar -> stash result (Just ar) + +testAsyncWaitTimeout :: TestResult (Maybe (AsyncResult ())) -> Process () +testAsyncWaitTimeout result = + let delay = seconds 1 + in do + hAsync <- async $ sleep $ seconds 20 + waitTimeout delay hAsync >>= stash result + cancelWait hAsync >> return () tests :: LocalNode -> [Test] tests localNode = [ @@ -47,8 +72,16 @@ tests localNode = [ localNode (AsyncCancelled) testAsyncCancel) , testCase "testAsyncPoll" (delayedAssertion - "expected poll to return something useful" + "expected poll to return a valid AsyncResult" localNode (AsyncDone ()) testAsyncPoll) + , testCase "testAsyncCancelWait" + (delayedAssertion + "expected cancelWait to complete some time" + localNode (Just AsyncCancelled) testAsyncCancelWait) + , testCase "testAsyncWaitTimeout" + (delayedAssertion + "expected waitTimeout to return Nothing when it times out" + localNode (Nothing) testAsyncWaitTimeout) ] ] From 277b23ae5b15cf49794d6b4d265ba2dff9159402 Mon Sep 17 00:00:00 2001 From: Simon Marlow Date: Thu, 13 Dec 2012 11:52:32 +0000 Subject: [PATCH 0526/2357] fix warnings --- .../Process/Backend/SimpleLocalnet.hs | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs index 3409eda5..3fb8323d 100644 --- a/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs +++ b/src/Control/Distributed/Process/Backend/SimpleLocalnet.hs @@ -104,7 +104,7 @@ import Data.Foldable (forM_) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Exception (throw) -import Control.Monad (forever, forM, replicateM, when, replicateM_) +import Control.Monad (forever, replicateM, replicateM_) import Control.Monad.IO.Class (liftIO) import Control.Concurrent (forkIO, threadDelay, ThreadId) import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_) @@ -116,17 +116,13 @@ import Control.Distributed.Process , WhereIsReply(..) , whereis , whereisRemoteAsync - , registerRemoteAsync - , reregisterRemoteAsync , getSelfPid , register , reregister , expect , nsendRemote , receiveWait - , receiveTimeout , match - , matchIf , processNodeId , monitorNode , monitor @@ -138,7 +134,6 @@ import Control.Distributed.Process , receiveChan , nsend , SendPort - , RegisterReply(..) , bracket , try , send @@ -180,21 +175,21 @@ data BackendState = BackendState { initializeBackend :: N.HostName -> N.ServiceName -> RemoteTable -> IO Backend initializeBackend host port rtable = do mTransport <- NT.createTransport host port NT.defaultTCPParameters - (recv, send) <- initMulticast "224.0.0.99" 9999 1024 + (recv, sendp) <- initMulticast "224.0.0.99" 9999 1024 (_, backendState) <- fixIO $ \ ~(tid, _) -> do backendState <- newMVar BackendState { _localNodes = [] , _peers = Set.empty , discoveryDaemon = tid } - tid' <- forkIO $ peerDiscoveryDaemon backendState recv send + tid' <- forkIO $ peerDiscoveryDaemon backendState recv sendp return (tid', backendState) case mTransport of Left err -> throw err Right transport -> let backend = Backend { newLocalNode = apiNewLocalNode transport rtable backendState - , findPeers = apiFindPeers send backendState + , findPeers = apiFindPeers sendp backendState , redirectLogsHere = apiRedirectLogsHere backend } in return backend @@ -214,8 +209,8 @@ apiFindPeers :: (PeerDiscoveryMsg -> IO ()) -> MVar BackendState -> Int -> IO [NodeId] -apiFindPeers send backendState delay = do - send PeerDiscoveryRequest +apiFindPeers sendfn backendState delay = do + sendfn PeerDiscoveryRequest threadDelay delay Set.toList . (^. peers) <$> readMVar backendState @@ -238,14 +233,14 @@ peerDiscoveryDaemon :: MVar BackendState -> IO (PeerDiscoveryMsg, N.SockAddr) -> (PeerDiscoveryMsg -> IO ()) -> IO () -peerDiscoveryDaemon backendState recv send = forever go +peerDiscoveryDaemon backendState recv sendfn = forever go where go = do (msg, _) <- recv case msg of PeerDiscoveryRequest -> do nodes <- (^. localNodes) <$> readMVar backendState - forM_ nodes $ send . PeerDiscoveryReply . Node.localNodeId + forM_ nodes $ sendfn . PeerDiscoveryReply . Node.localNodeId PeerDiscoveryReply nid -> modifyMVar_ backendState $ return . (peers ^: Set.insert nid) @@ -272,8 +267,8 @@ apiRedirectLogsHere _backend slavecontrollers = do -- Wait for the replies replicateM_ (length slavecontrollers) $ do receiveWait - [ match (\(RedirectLogsReply from ok) -> return ()) - , match (\m@(NodeMonitorNotification {}) -> return ()) + [ match (\(RedirectLogsReply {}) -> return ()) + , match (\(NodeMonitorNotification {}) -> return ()) ] -------------------------------------------------------------------------------- @@ -335,8 +330,8 @@ slaveController = do ok <- case (r :: Either ProcessRegistrationException ()) of Right _ -> return True Left _ -> do - r <- try (register "logger" loggerPid) - case (r :: Either ProcessRegistrationException ()) of + s <- try (register "logger" loggerPid) + case (s :: Either ProcessRegistrationException ()) of Right _ -> return True Left _ -> return False pid <- getSelfPid @@ -359,7 +354,7 @@ findSlaves backend = do $ \_ -> do -- fire off whereis requests - forM nodes $ \nid -> whereisRemoteAsync nid "slaveController" + forM_ nodes $ \nid -> whereisRemoteAsync nid "slaveController" -- Wait for the replies catMaybes <$> replicateM (length nodes) ( From ebd3c24c0913dbfc14a808351507700915efd11f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 16:38:14 +0000 Subject: [PATCH 0527/2357] update async docs to reflect contract changes --- src/Control/Distributed/Platform/Async.hs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 88f484ea..b3e7c9f2 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -102,15 +102,10 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and their coordinating --- processes (including the caller to functions such as 'wait'). Given the --- process identifier of a gatherer, a worker that wishes to publish some --- results should send these to the gatherer process when it is finished. --- Workers that do not return anything should simply exit normally (i.e., they --- should not call @exit selfPid reason@ not @terminate@ in the base Cloud --- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ --- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual --- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (Async a) async t = do From 0052dfa842e1e4a2d1ebf5137ed4021aae2deeca Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 16:38:14 +0000 Subject: [PATCH 0528/2357] update async docs to reflect contract changes --- src/Control/Distributed/Platform/Async.hs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 88f484ea..b3e7c9f2 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -102,15 +102,10 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and their coordinating --- processes (including the caller to functions such as 'wait'). Given the --- process identifier of a gatherer, a worker that wishes to publish some --- results should send these to the gatherer process when it is finished. --- Workers that do not return anything should simply exit normally (i.e., they --- should not call @exit selfPid reason@ not @terminate@ in the base Cloud --- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ --- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual --- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (Async a) async t = do From 08bc37bdd33e84c4d680beec9dd8b271c7704d99 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 16:38:14 +0000 Subject: [PATCH 0529/2357] update async docs to reflect contract changes --- src/Control/Distributed/Platform/Async.hs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 88f484ea..b3e7c9f2 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -102,15 +102,10 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and their coordinating --- processes (including the caller to functions such as 'wait'). Given the --- process identifier of a gatherer, a worker that wishes to publish some --- results should send these to the gatherer process when it is finished. --- Workers that do not return anything should simply exit normally (i.e., they --- should not call @exit selfPid reason@ not @terminate@ in the base Cloud --- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ --- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual --- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (Async a) async t = do From f07fe404e187c0b9babd521a8e022b39cd80191f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 16:38:14 +0000 Subject: [PATCH 0530/2357] update async docs to reflect contract changes --- src/Control/Distributed/Platform/Async.hs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 88f484ea..b3e7c9f2 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -102,15 +102,10 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- There is currently a contract between async workers and their coordinating --- processes (including the caller to functions such as 'wait'). Given the --- process identifier of a gatherer, a worker that wishes to publish some --- results should send these to the gatherer process when it is finished. --- Workers that do not return anything should simply exit normally (i.e., they --- should not call @exit selfPid reason@ not @terminate@ in the base Cloud --- Haskell APIs) and providing the type of the 'Async' action is @Async ()@ --- then the 'AsyncResult' will be @AsyncDone ()@ instead of the usual --- @AsyncFailed DiedNormal@ which would normally result from this scenario. +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (Async a) async t = do From 85e638fe37e92862b45e2a10eecc047206d4eea4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 20:11:41 +0000 Subject: [PATCH 0531/2357] strip out the readme (for now) --- README.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/README.md b/README.md index 40f7aaac..9b259147 100644 --- a/README.md +++ b/README.md @@ -1,30 +1 @@ -# Welcome to Cloud Haskell Platform - -TODO - -# Tests and coverage - -The following script will configure cabal and run the unit tests with coverage enabled - -./test-report.sh - -# Profiling - -The following scripts will configure and run the executable with profiling enabled - -./profiling/configure.sh -./profiling/run.sh - - -# Join in! - -We are happy to receive bug reports, fixes, documentation enhancements, -and other improvements. - -Please report bugs via the -[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). - -Master [git repository](http://github.com/hyperthunk/distributed-process-platform): - -* `git clone git://github.com/hyperthunk/distributed-process-platform.git` - +# Cloud Haskell Platform From 97caa5298c648abfa8011d93cfc94ae1091fadc9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 20:11:41 +0000 Subject: [PATCH 0532/2357] strip out the readme (for now) --- README.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/README.md b/README.md index 40f7aaac..9b259147 100644 --- a/README.md +++ b/README.md @@ -1,30 +1 @@ -# Welcome to Cloud Haskell Platform - -TODO - -# Tests and coverage - -The following script will configure cabal and run the unit tests with coverage enabled - -./test-report.sh - -# Profiling - -The following scripts will configure and run the executable with profiling enabled - -./profiling/configure.sh -./profiling/run.sh - - -# Join in! - -We are happy to receive bug reports, fixes, documentation enhancements, -and other improvements. - -Please report bugs via the -[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). - -Master [git repository](http://github.com/hyperthunk/distributed-process-platform): - -* `git clone git://github.com/hyperthunk/distributed-process-platform.git` - +# Cloud Haskell Platform From ad03fb7a045c082602f3e48927c6d2cb0170bdae Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 20:11:41 +0000 Subject: [PATCH 0533/2357] strip out the readme (for now) --- README.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/README.md b/README.md index 40f7aaac..9b259147 100644 --- a/README.md +++ b/README.md @@ -1,30 +1 @@ -# Welcome to Cloud Haskell Platform - -TODO - -# Tests and coverage - -The following script will configure cabal and run the unit tests with coverage enabled - -./test-report.sh - -# Profiling - -The following scripts will configure and run the executable with profiling enabled - -./profiling/configure.sh -./profiling/run.sh - - -# Join in! - -We are happy to receive bug reports, fixes, documentation enhancements, -and other improvements. - -Please report bugs via the -[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). - -Master [git repository](http://github.com/hyperthunk/distributed-process-platform): - -* `git clone git://github.com/hyperthunk/distributed-process-platform.git` - +# Cloud Haskell Platform From ac64d041950e390e16f62dcf2bcfafbe9cf063b7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 20:11:41 +0000 Subject: [PATCH 0534/2357] strip out the readme (for now) --- README.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/README.md b/README.md index 40f7aaac..9b259147 100644 --- a/README.md +++ b/README.md @@ -1,30 +1 @@ -# Welcome to Cloud Haskell Platform - -TODO - -# Tests and coverage - -The following script will configure cabal and run the unit tests with coverage enabled - -./test-report.sh - -# Profiling - -The following scripts will configure and run the executable with profiling enabled - -./profiling/configure.sh -./profiling/run.sh - - -# Join in! - -We are happy to receive bug reports, fixes, documentation enhancements, -and other improvements. - -Please report bugs via the -[github issue tracker](http://github.com/hyperthunk/distributed-process-platform/issues). - -Master [git repository](http://github.com/hyperthunk/distributed-process-platform): - -* `git clone git://github.com/hyperthunk/distributed-process-platform.git` - +# Cloud Haskell Platform From 51491b3f6176aca1c4555553e883e1e12af23e0e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:05:01 +0000 Subject: [PATCH 0535/2357] Now that we've stolen Simon's lovely API, pinch half his documentation too --- src/Control/Distributed/Platform/Async.hs | 38 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3e7c9f2..9fa568d0 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,8 +1,32 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE StandaloneDeriving #-} +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + module Control.Distributed.Platform.Async ( -- types/data AsyncRef @@ -74,13 +98,13 @@ data Async a = Async { , channel :: (InternalChannel a) } --- | Represents the result of an asynchronous action, which can be in several --- states at any given time. +-- | Represents the result of an asynchronous action, which can be in one of +-- several states at any given time. data AsyncResult a = - AsyncDone a -- | a completed action and its result - | AsyncFailed DiedReason -- | a failed action and the failure reason - | AsyncCancelled -- | a cancelled action - | AsyncPending -- | a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) From 04d434f38a3db609af7526a4fad987aa9801ecf0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:05:01 +0000 Subject: [PATCH 0536/2357] Now that we've stolen Simon's lovely API, pinch half his documentation too --- src/Control/Distributed/Platform/Async.hs | 38 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3e7c9f2..9fa568d0 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,8 +1,32 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE StandaloneDeriving #-} +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + module Control.Distributed.Platform.Async ( -- types/data AsyncRef @@ -74,13 +98,13 @@ data Async a = Async { , channel :: (InternalChannel a) } --- | Represents the result of an asynchronous action, which can be in several --- states at any given time. +-- | Represents the result of an asynchronous action, which can be in one of +-- several states at any given time. data AsyncResult a = - AsyncDone a -- | a completed action and its result - | AsyncFailed DiedReason -- | a failed action and the failure reason - | AsyncCancelled -- | a cancelled action - | AsyncPending -- | a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) From aa01c62b3bd80f7bf72a2913e852b59a80d8981d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:05:01 +0000 Subject: [PATCH 0537/2357] Now that we've stolen Simon's lovely API, pinch half his documentation too --- src/Control/Distributed/Platform/Async.hs | 38 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3e7c9f2..9fa568d0 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,8 +1,32 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE StandaloneDeriving #-} +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + module Control.Distributed.Platform.Async ( -- types/data AsyncRef @@ -74,13 +98,13 @@ data Async a = Async { , channel :: (InternalChannel a) } --- | Represents the result of an asynchronous action, which can be in several --- states at any given time. +-- | Represents the result of an asynchronous action, which can be in one of +-- several states at any given time. data AsyncResult a = - AsyncDone a -- | a completed action and its result - | AsyncFailed DiedReason -- | a failed action and the failure reason - | AsyncCancelled -- | a cancelled action - | AsyncPending -- | a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) From 56d81df47dc42b4a1fdbd67e55aa1620411f2b82 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:05:01 +0000 Subject: [PATCH 0538/2357] Now that we've stolen Simon's lovely API, pinch half his documentation too --- src/Control/Distributed/Platform/Async.hs | 38 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index b3e7c9f2..9fa568d0 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -1,8 +1,32 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE StandaloneDeriving #-} +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + module Control.Distributed.Platform.Async ( -- types/data AsyncRef @@ -74,13 +98,13 @@ data Async a = Async { , channel :: (InternalChannel a) } --- | Represents the result of an asynchronous action, which can be in several --- states at any given time. +-- | Represents the result of an asynchronous action, which can be in one of +-- several states at any given time. data AsyncResult a = - AsyncDone a -- | a completed action and its result - | AsyncFailed DiedReason -- | a failed action and the failure reason - | AsyncCancelled -- | a cancelled action - | AsyncPending -- | a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) From 94bc0593d73abea9c74b7c7d2c18746677546fd6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:07:48 +0000 Subject: [PATCH 0539/2357] licensed under the same conditions as distributed-process (core) --- LICENCE | 40 ++++++++++++++++++------------ distributed-process-platform.cabal | 7 +++++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/LICENCE b/LICENCE index dc1e0389..fbd21e32 100644 --- a/LICENCE +++ b/LICENCE @@ -1,22 +1,30 @@ -Copyright (c) 2005 - 2013 Nebularis. +Copyright Tim Watson, 2011-2012 All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..2d6e1664 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,7 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform +Copyright: Tim Watson +Author: Tim Watson +Maintainer: watson.timothy@gmail.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process-platform +Bug-Reports: mailto:watson.timothy@gmail.com synopsis: TODO description: TODO category: Control From 4a9bf6427de64d9f547f2f9b3abd9abbc8d62f38 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:07:48 +0000 Subject: [PATCH 0540/2357] licensed under the same conditions as distributed-process (core) --- LICENCE | 40 ++++++++++++++++++------------ distributed-process-platform.cabal | 7 +++++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/LICENCE b/LICENCE index dc1e0389..fbd21e32 100644 --- a/LICENCE +++ b/LICENCE @@ -1,22 +1,30 @@ -Copyright (c) 2005 - 2013 Nebularis. +Copyright Tim Watson, 2011-2012 All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..2d6e1664 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,7 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform +Copyright: Tim Watson +Author: Tim Watson +Maintainer: watson.timothy@gmail.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process-platform +Bug-Reports: mailto:watson.timothy@gmail.com synopsis: TODO description: TODO category: Control From 8b71dc96646a2c192ba99e9f87126bbbc4683bee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:07:48 +0000 Subject: [PATCH 0541/2357] licensed under the same conditions as distributed-process (core) --- LICENCE | 40 ++++++++++++++++++------------ distributed-process-platform.cabal | 7 +++++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/LICENCE b/LICENCE index dc1e0389..fbd21e32 100644 --- a/LICENCE +++ b/LICENCE @@ -1,22 +1,30 @@ -Copyright (c) 2005 - 2013 Nebularis. +Copyright Tim Watson, 2011-2012 All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..2d6e1664 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,7 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform +Copyright: Tim Watson +Author: Tim Watson +Maintainer: watson.timothy@gmail.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process-platform +Bug-Reports: mailto:watson.timothy@gmail.com synopsis: TODO description: TODO category: Control From a83ffeebd7da968ae502ea0c3bee93cb68bb7395 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:07:48 +0000 Subject: [PATCH 0542/2357] licensed under the same conditions as distributed-process (core) --- LICENCE | 40 ++++++++++++++++++------------ distributed-process-platform.cabal | 7 +++++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/LICENCE b/LICENCE index dc1e0389..fbd21e32 100644 --- a/LICENCE +++ b/LICENCE @@ -1,22 +1,30 @@ -Copyright (c) 2005 - 2013 Nebularis. +Copyright Tim Watson, 2011-2012 All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index bdf58e3c..2d6e1664 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,7 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -homepage: http://github.com/hyperthunk/distributed-process-platform +Copyright: Tim Watson +Author: Tim Watson +Maintainer: watson.timothy@gmail.com +Stability: experimental +Homepage: http://github.com/haskell-distributed/distributed-process-platform +Bug-Reports: mailto:watson.timothy@gmail.com synopsis: TODO description: TODO category: Control From 1ef072b00573875ecac7fdc088a2a4ea671e933c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:09:10 +0000 Subject: [PATCH 0543/2357] Update LICENCE --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index fbd21e32..bee23260 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright Tim Watson, 2011-2012 +Copyright Tim Watson, 2012-2013 All rights reserved. From a3cde383222d863f6cda88118c03130969266973 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:09:10 +0000 Subject: [PATCH 0544/2357] Update LICENCE --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index fbd21e32..bee23260 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright Tim Watson, 2011-2012 +Copyright Tim Watson, 2012-2013 All rights reserved. From 2429fa58d8df8dafc13093e2436f3198fee90381 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:09:10 +0000 Subject: [PATCH 0545/2357] Update LICENCE --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index fbd21e32..bee23260 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright Tim Watson, 2011-2012 +Copyright Tim Watson, 2012-2013 All rights reserved. From 0a704748a8e4eb9364990b255ded8809878c4b36 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 20 Dec 2012 23:09:10 +0000 Subject: [PATCH 0546/2357] Update LICENCE --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index fbd21e32..bee23260 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright Tim Watson, 2011-2012 +Copyright Tim Watson, 2012-2013 All rights reserved. From 662ec3bead47640dbdada51a7664692a426cbc36 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:36:08 +0000 Subject: [PATCH 0547/2357] clarify the semantics by marking the API as being explicitly about channels --- src/Control/Distributed/Platform/Async.hs | 150 +++++++++++++--------- tests/TestAsync.hs | 57 ++++++-- 2 files changed, 131 insertions(+), 76 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 9fa568d0..dad005ae 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -34,19 +34,20 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , Async(worker) + , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning - , async + , asyncChan + , asyncChanLinked -- and stopping/killing - , cancel - , cancelWait + , cancelChan + , cancelChanWait -- functions to query an async-result - , poll - , check - , wait - , waitTimeout - , waitCheckTimeout + , pollChan + , checkChan + , waitChan + , waitChanTimeout + , waitChanCheckTimeout ) where import Control.Distributed.Platform.Timer @@ -88,11 +89,13 @@ type AsyncTask a = Process a -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An asynchronous action spawned by 'async'. +-- | An handle for an asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async { +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { worker :: AsyncWorkerId , insulator :: AsyncGathererId , channel :: (InternalChannel a) @@ -101,10 +104,11 @@ data Async a = Async { -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = - AsyncDone a -- ^ a completed action and its result - | AsyncFailed DiedReason -- ^ a failed action and the failure reason - | AsyncCancelled -- ^ a cancelled action - | AsyncPending -- ^ a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncLinkFailed DiedReason -- ^ a link failure and the reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) @@ -121,29 +125,40 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. --- | An asynchronous action spawned by 'async' or 'withAsync'. --- Asynchronous actions are executed in a separate @Process@, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). +-- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should -- exit normally (i.e., they should not call the @exit selfPid reason@ nor -- @terminate@ primitives), otherwise the 'AsyncResult' will end up being -- @AsyncFailed DiedException@ instead of containing the result. -- -async :: (Serializable a) => AsyncTask a -> Process (Async a) -async t = do - (wpid, gpid, chan) <- spawnWorkers t - return Async { +asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChan = asyncChanDo False + +-- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChanLinked = asyncChanDo True + +asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncChanDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { worker = wpid , insulator = gpid , channel = chan } -spawnWorkers :: (Serializable a) => - AsyncTask a -> - Process (AsyncRef, AsyncRef, (InternalChannel a)) -spawnWorkers task = do +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -151,84 +166,91 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task - say "task complete" sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - monRef <- monitor workerPid - finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) workerPid <- expect return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do - r <- receiveWait [ - matchIf - (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid == pid') - (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right DiedNormal -> return () - Right d -> sendChan replyTo (AsyncFailed d) - --- | Check whether an 'Async' has completed yet. The status of the asynchronous + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other -- constructors otherwise. This function does not block waiting for the result. -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. -poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll hAsync = do +pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +pollChan hAsync = do r <- receiveChanTimeout 0 $ snd (channel hAsync) return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. -check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) -check hAsync = poll hAsync >>= \r -> case r of +checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +checkChan hAsync = pollChan hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the -- result has not been made available, otherwise one of the other constructors. -waitCheckTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (AsyncResult a) -waitCheckTimeout t hAsync = - waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +waitChanCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitChanCheckTimeout t hAsync = + waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) -- | Wait for an asynchronous action to complete, and return its -- value. The outcome of the action is encoded as an 'AsyncResult'. -- -wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = receiveChan $ snd (channel hAsync) +waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +waitChan hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then -- consider using 'wait' or 'waitCheckTimeout' instead. -waitTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = +waitChanTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitChanTimeout t hAsync = receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. -cancel :: Async a -> Process () -cancel (Async _ g _) = send g CancelWait +cancelChan :: AsyncChan a -> Process () +cancelChan (AsyncChan _ g _) = send g CancelWait -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- -cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index c6c01c86..9fb60824 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -1,8 +1,13 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE ScopedTypeVariables #-} module TestAsync where +import Control.Concurrent.MVar + ( newEmptyMVar + , takeMVar + , MVar) import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() @@ -20,34 +25,34 @@ import TestUtils testAsyncPoll :: TestResult (AsyncResult ()) -> Process () testAsyncPoll result = do - hAsync <- async $ do "go" <- expect; say "running" >> return () - ar <- poll hAsync + hAsync <- asyncChan $ do "go" <- expect; say "running" >> return () + ar <- pollChan hAsync case ar of AsyncPending -> - send (worker hAsync) "go" >> wait hAsync >>= stash result + send (worker hAsync) "go" >> waitChan hAsync >>= stash result _ -> stash result ar >> return () testAsyncCancel :: TestResult (AsyncResult ()) -> Process () testAsyncCancel result = do - hAsync <- async $ runTestProcess $ say "running" >> return () + hAsync <- asyncChan $ runTestProcess $ say "running" >> return () sleep $ milliseconds 100 - p <- poll hAsync -- nasty kind of assertion: use assertEquals? + p <- pollChan hAsync -- nasty kind of assertion: use assertEquals? case p of - AsyncPending -> cancel hAsync >> wait hAsync >>= stash result + AsyncPending -> cancelChan hAsync >> waitChan hAsync >>= stash result _ -> say (show p) >> stash result p testAsyncCancelWait :: TestResult (Maybe (AsyncResult ())) -> Process () testAsyncCancelWait result = do testPid <- getSelfPid p <- spawnLocal $ do - hAsync <- async $ runTestProcess $ say "running" >> (sleep $ seconds 60) + hAsync <- asyncChan $ runTestProcess $ say "running" >> (sleep $ seconds 60) sleep $ milliseconds 100 send testPid "running" - AsyncPending <- poll hAsync - cancelWait hAsync >>= send testPid + AsyncPending <- pollChan hAsync + cancelChanWait hAsync >>= send testPid "running" <- expect d <- expectTimeout (intervalToMs $ seconds 5) @@ -59,9 +64,33 @@ testAsyncWaitTimeout :: TestResult (Maybe (AsyncResult ())) -> Process () testAsyncWaitTimeout result = let delay = seconds 1 in do - hAsync <- async $ sleep $ seconds 20 - waitTimeout delay hAsync >>= stash result - cancelWait hAsync >> return () + hAsync <- asyncChan $ sleep $ seconds 20 + waitChanTimeout delay hAsync >>= stash result + cancelChanWait hAsync >> return () + +testAsyncLinked :: TestResult Bool -> Process () +testAsyncLinked result = do + mv :: MVar (AsyncChan ()) <- liftIO $ newEmptyMVar + pid <- spawnLocal $ do + h <- asyncChanLinked $ do + "waiting" <- expect + return () + stash mv h + "sleeping" <- expect + return () + + hAsync <- liftIO $ takeMVar mv + + mref <- monitor $ worker hAsync + exit pid "stop" + + ProcessMonitorNotification mref' _ _ <- expect + + -- since the initial caller died and we used 'asyncLinked', the async should + -- pick up on the exit signal and set the result accordingly, however the + -- ReceivePort is no longer valid, so we can't wait on it! We have to ensure + -- that the worker is really dead then.... + stash result $ mref == mref' tests :: LocalNode -> [Test] tests localNode = [ @@ -82,6 +111,10 @@ tests localNode = [ (delayedAssertion "expected waitTimeout to return Nothing when it times out" localNode (Nothing) testAsyncWaitTimeout) + , testCase "testAsyncLinked" + (delayedAssertion + "expected linked process to die with originator" + localNode True testAsyncLinked) ] ] From 0082ae55544bda4c45e5045cf87db9d30ac47df2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:36:08 +0000 Subject: [PATCH 0548/2357] clarify the semantics by marking the API as being explicitly about channels --- src/Control/Distributed/Platform/Async.hs | 150 +++++++++++++--------- 1 file changed, 86 insertions(+), 64 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 9fa568d0..dad005ae 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -34,19 +34,20 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , Async(worker) + , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning - , async + , asyncChan + , asyncChanLinked -- and stopping/killing - , cancel - , cancelWait + , cancelChan + , cancelChanWait -- functions to query an async-result - , poll - , check - , wait - , waitTimeout - , waitCheckTimeout + , pollChan + , checkChan + , waitChan + , waitChanTimeout + , waitChanCheckTimeout ) where import Control.Distributed.Platform.Timer @@ -88,11 +89,13 @@ type AsyncTask a = Process a -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An asynchronous action spawned by 'async'. +-- | An handle for an asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async { +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { worker :: AsyncWorkerId , insulator :: AsyncGathererId , channel :: (InternalChannel a) @@ -101,10 +104,11 @@ data Async a = Async { -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = - AsyncDone a -- ^ a completed action and its result - | AsyncFailed DiedReason -- ^ a failed action and the failure reason - | AsyncCancelled -- ^ a cancelled action - | AsyncPending -- ^ a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncLinkFailed DiedReason -- ^ a link failure and the reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) @@ -121,29 +125,40 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. --- | An asynchronous action spawned by 'async' or 'withAsync'. --- Asynchronous actions are executed in a separate @Process@, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). +-- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should -- exit normally (i.e., they should not call the @exit selfPid reason@ nor -- @terminate@ primitives), otherwise the 'AsyncResult' will end up being -- @AsyncFailed DiedException@ instead of containing the result. -- -async :: (Serializable a) => AsyncTask a -> Process (Async a) -async t = do - (wpid, gpid, chan) <- spawnWorkers t - return Async { +asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChan = asyncChanDo False + +-- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChanLinked = asyncChanDo True + +asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncChanDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { worker = wpid , insulator = gpid , channel = chan } -spawnWorkers :: (Serializable a) => - AsyncTask a -> - Process (AsyncRef, AsyncRef, (InternalChannel a)) -spawnWorkers task = do +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -151,84 +166,91 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task - say "task complete" sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - monRef <- monitor workerPid - finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) workerPid <- expect return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do - r <- receiveWait [ - matchIf - (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid == pid') - (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right DiedNormal -> return () - Right d -> sendChan replyTo (AsyncFailed d) - --- | Check whether an 'Async' has completed yet. The status of the asynchronous + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other -- constructors otherwise. This function does not block waiting for the result. -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. -poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll hAsync = do +pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +pollChan hAsync = do r <- receiveChanTimeout 0 $ snd (channel hAsync) return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. -check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) -check hAsync = poll hAsync >>= \r -> case r of +checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +checkChan hAsync = pollChan hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the -- result has not been made available, otherwise one of the other constructors. -waitCheckTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (AsyncResult a) -waitCheckTimeout t hAsync = - waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +waitChanCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitChanCheckTimeout t hAsync = + waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) -- | Wait for an asynchronous action to complete, and return its -- value. The outcome of the action is encoded as an 'AsyncResult'. -- -wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = receiveChan $ snd (channel hAsync) +waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +waitChan hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then -- consider using 'wait' or 'waitCheckTimeout' instead. -waitTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = +waitChanTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitChanTimeout t hAsync = receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. -cancel :: Async a -> Process () -cancel (Async _ g _) = send g CancelWait +cancelChan :: AsyncChan a -> Process () +cancelChan (AsyncChan _ g _) = send g CancelWait -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- -cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync From 2cfed2e04316d983123228e8770e50734728fe5a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:36:08 +0000 Subject: [PATCH 0549/2357] clarify the semantics by marking the API as being explicitly about channels --- src/Control/Distributed/Platform/Async.hs | 150 +++++++++++++--------- 1 file changed, 86 insertions(+), 64 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 9fa568d0..dad005ae 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -34,19 +34,20 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , Async(worker) + , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning - , async + , asyncChan + , asyncChanLinked -- and stopping/killing - , cancel - , cancelWait + , cancelChan + , cancelChanWait -- functions to query an async-result - , poll - , check - , wait - , waitTimeout - , waitCheckTimeout + , pollChan + , checkChan + , waitChan + , waitChanTimeout + , waitChanCheckTimeout ) where import Control.Distributed.Platform.Timer @@ -88,11 +89,13 @@ type AsyncTask a = Process a -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An asynchronous action spawned by 'async'. +-- | An handle for an asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async { +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { worker :: AsyncWorkerId , insulator :: AsyncGathererId , channel :: (InternalChannel a) @@ -101,10 +104,11 @@ data Async a = Async { -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = - AsyncDone a -- ^ a completed action and its result - | AsyncFailed DiedReason -- ^ a failed action and the failure reason - | AsyncCancelled -- ^ a cancelled action - | AsyncPending -- ^ a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncLinkFailed DiedReason -- ^ a link failure and the reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) @@ -121,29 +125,40 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. --- | An asynchronous action spawned by 'async' or 'withAsync'. --- Asynchronous actions are executed in a separate @Process@, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). +-- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should -- exit normally (i.e., they should not call the @exit selfPid reason@ nor -- @terminate@ primitives), otherwise the 'AsyncResult' will end up being -- @AsyncFailed DiedException@ instead of containing the result. -- -async :: (Serializable a) => AsyncTask a -> Process (Async a) -async t = do - (wpid, gpid, chan) <- spawnWorkers t - return Async { +asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChan = asyncChanDo False + +-- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChanLinked = asyncChanDo True + +asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncChanDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { worker = wpid , insulator = gpid , channel = chan } -spawnWorkers :: (Serializable a) => - AsyncTask a -> - Process (AsyncRef, AsyncRef, (InternalChannel a)) -spawnWorkers task = do +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -151,84 +166,91 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task - say "task complete" sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - monRef <- monitor workerPid - finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) workerPid <- expect return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do - r <- receiveWait [ - matchIf - (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid == pid') - (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right DiedNormal -> return () - Right d -> sendChan replyTo (AsyncFailed d) - --- | Check whether an 'Async' has completed yet. The status of the asynchronous + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other -- constructors otherwise. This function does not block waiting for the result. -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. -poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll hAsync = do +pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +pollChan hAsync = do r <- receiveChanTimeout 0 $ snd (channel hAsync) return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. -check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) -check hAsync = poll hAsync >>= \r -> case r of +checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +checkChan hAsync = pollChan hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the -- result has not been made available, otherwise one of the other constructors. -waitCheckTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (AsyncResult a) -waitCheckTimeout t hAsync = - waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +waitChanCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitChanCheckTimeout t hAsync = + waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) -- | Wait for an asynchronous action to complete, and return its -- value. The outcome of the action is encoded as an 'AsyncResult'. -- -wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = receiveChan $ snd (channel hAsync) +waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +waitChan hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then -- consider using 'wait' or 'waitCheckTimeout' instead. -waitTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = +waitChanTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitChanTimeout t hAsync = receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. -cancel :: Async a -> Process () -cancel (Async _ g _) = send g CancelWait +cancelChan :: AsyncChan a -> Process () +cancelChan (AsyncChan _ g _) = send g CancelWait -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- -cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync From 3c5d5b4fad78e86b0719926b404f93b9cd7c8864 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:36:08 +0000 Subject: [PATCH 0550/2357] clarify the semantics by marking the API as being explicitly about channels --- src/Control/Distributed/Platform/Async.hs | 150 +++++++++++++--------- 1 file changed, 86 insertions(+), 64 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 9fa568d0..dad005ae 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -34,19 +34,20 @@ module Control.Distributed.Platform.Async , AsyncGathererId , AsyncTask , AsyncCancel - , Async(worker) + , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning - , async + , asyncChan + , asyncChanLinked -- and stopping/killing - , cancel - , cancelWait + , cancelChan + , cancelChanWait -- functions to query an async-result - , poll - , check - , wait - , waitTimeout - , waitCheckTimeout + , pollChan + , checkChan + , waitChan + , waitChanTimeout + , waitChanCheckTimeout ) where import Control.Distributed.Platform.Timer @@ -88,11 +89,13 @@ type AsyncTask a = Process a -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An asynchronous action spawned by 'async'. +-- | An handle for an asynchronous action spawned by 'async'. -- Asynchronous operations are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -data Async a = Async { +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { worker :: AsyncWorkerId , insulator :: AsyncGathererId , channel :: (InternalChannel a) @@ -101,10 +104,11 @@ data Async a = Async { -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = - AsyncDone a -- ^ a completed action and its result - | AsyncFailed DiedReason -- ^ a failed action and the failure reason - | AsyncCancelled -- ^ a cancelled action - | AsyncPending -- ^ a pending action (that is still running) + AsyncDone a -- ^ a completed action and its result + | AsyncFailed DiedReason -- ^ a failed action and the failure reason + | AsyncLinkFailed DiedReason -- ^ a link failure and the reason + | AsyncCancelled -- ^ a cancelled action + | AsyncPending -- ^ a pending action (that is still running) deriving (Typeable) $(derive makeBinary ''AsyncResult) @@ -121,29 +125,40 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. --- | An asynchronous action spawned by 'async' or 'withAsync'. --- Asynchronous actions are executed in a separate @Process@, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). +-- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should -- exit normally (i.e., they should not call the @exit selfPid reason@ nor -- @terminate@ primitives), otherwise the 'AsyncResult' will end up being -- @AsyncFailed DiedException@ instead of containing the result. -- -async :: (Serializable a) => AsyncTask a -> Process (Async a) -async t = do - (wpid, gpid, chan) <- spawnWorkers t - return Async { +asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChan = asyncChanDo False + +-- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncChanLinked = asyncChanDo True + +asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncChanDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { worker = wpid , insulator = gpid , channel = chan } -spawnWorkers :: (Serializable a) => - AsyncTask a -> - Process (AsyncRef, AsyncRef, (InternalChannel a)) -spawnWorkers task = do +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -151,84 +166,91 @@ spawnWorkers task = do insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do r <- task - say "task complete" sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - monRef <- monitor workerPid - finally (pollUntilExit workerPid monRef chan) (unmonitor monRef) + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) workerPid <- expect return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) => - ProcessId -> MonitorRef -> InternalChannel a -> Process () - pollUntilExit pid ref (replyTo, _) = do - r <- receiveWait [ - matchIf - (\(ProcessMonitorNotification ref' pid' _) -> - ref' == ref && pid == pid') - (\(ProcessMonitorNotification _ _ r) -> return (Right r)) - , match (\c@(CancelWait) -> kill pid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right DiedNormal -> return () - Right d -> sendChan replyTo (AsyncFailed d) - --- | Check whether an 'Async' has completed yet. The status of the asynchronous + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other -- constructors otherwise. This function does not block waiting for the result. -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. -- See 'Async'. -poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll hAsync = do +pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +pollChan hAsync = do r <- receiveChanTimeout 0 $ snd (channel hAsync) return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. -check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) -check hAsync = poll hAsync >>= \r -> case r of +checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +checkChan hAsync = pollChan hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the -- result has not been made available, otherwise one of the other constructors. -waitCheckTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (AsyncResult a) -waitCheckTimeout t hAsync = - waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) +waitChanCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitChanCheckTimeout t hAsync = + waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) -- | Wait for an asynchronous action to complete, and return its -- value. The outcome of the action is encoded as an 'AsyncResult'. -- -wait :: (Serializable a) => Async a -> Process (AsyncResult a) -wait hAsync = receiveChan $ snd (channel hAsync) +waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +waitChan hAsync = receiveChan $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then -- consider using 'wait' or 'waitCheckTimeout' instead. -waitTimeout :: (Serializable a) => - TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout t hAsync = +waitChanTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitChanTimeout t hAsync = receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. -cancel :: Async a -> Process () -cancel (Async _ g _) = send g CancelWait +cancelChan :: AsyncChan a -> Process () +cancelChan (AsyncChan _ g _) = send g CancelWait -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. -- -cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync From ffd53a02163f0335312254e97f4b90b4c36a00ab Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:42:34 +0000 Subject: [PATCH 0551/2357] Extra primitives --- distributed-process-platform.cabal | 6 +++- src/Control/Distributed/Platform.hs | 6 +++- .../Platform/Internal/Primitives.hs | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2d6e1664..8ab903ff 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,13 +29,16 @@ library distributed-static, binary, mtl, + stm >= 2.3 && < 2.5, transformers hs-source-dirs: src ghc-options: -Wall exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async + other-modules: Control.Distributed.Platform.Internal.Primitives test-suite PlatformTests type: exitcode-stdio-1.0 @@ -61,5 +64,6 @@ test-suite PlatformTests other-modules: Control.Distributed.Platform.Timer, Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..a5672b78 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,8 +2,11 @@ -- module Control.Distributed.Platform ( + -- extra primitives + spawnLinkLocal + , spawnMonitorLocal -- time interval handling - milliseconds + , milliseconds , seconds , minutes , hours @@ -15,6 +18,7 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Internal.Primitives -------------------------------------------------------------------------------- -- API -- diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs new file mode 100644 index 00000000..ffe09e6b --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -0,0 +1,33 @@ +-- | Common Entities used throughout -platform. +-- NB: Please DO NOT use this module as a dumping ground. +-- +module Control.Distributed.Platform.Internal.Primitives + ( spawnLinkLocal + , spawnMonitorLocal + ) +where + +import Control.Distributed.Process + ( link + , spawnLocal + , monitor + , Process() + , ProcessId + , MonitorRef) + +-- | Node local version of 'Control.Distributed.Process.spawnLink'. +-- Note that this is just the sequential composition of 'spawn' and 'link'. +-- (The "Unified" semantics that underlies Cloud Haskell does not even support +-- a synchronous link operation) +spawnLinkLocal :: Process () -> Process ProcessId +spawnLinkLocal p = do + pid <- spawnLocal p + link pid + return pid + +-- | Like 'spawnLinkLocal', but monitor the spawned process +spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) +spawnMonitorLocal p = do + pid <- spawnLocal p + ref <- monitor pid + return (pid, ref) From 13eff72ad97d76178ded984f410083076c479136 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:42:34 +0000 Subject: [PATCH 0552/2357] Extra primitives --- distributed-process-platform.cabal | 6 +++- src/Control/Distributed/Platform.hs | 6 +++- .../Platform/Internal/Primitives.hs | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2d6e1664..8ab903ff 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,13 +29,16 @@ library distributed-static, binary, mtl, + stm >= 2.3 && < 2.5, transformers hs-source-dirs: src ghc-options: -Wall exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async + other-modules: Control.Distributed.Platform.Internal.Primitives test-suite PlatformTests type: exitcode-stdio-1.0 @@ -61,5 +64,6 @@ test-suite PlatformTests other-modules: Control.Distributed.Platform.Timer, Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..a5672b78 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,8 +2,11 @@ -- module Control.Distributed.Platform ( + -- extra primitives + spawnLinkLocal + , spawnMonitorLocal -- time interval handling - milliseconds + , milliseconds , seconds , minutes , hours @@ -15,6 +18,7 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Internal.Primitives -------------------------------------------------------------------------------- -- API -- diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs new file mode 100644 index 00000000..ffe09e6b --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -0,0 +1,33 @@ +-- | Common Entities used throughout -platform. +-- NB: Please DO NOT use this module as a dumping ground. +-- +module Control.Distributed.Platform.Internal.Primitives + ( spawnLinkLocal + , spawnMonitorLocal + ) +where + +import Control.Distributed.Process + ( link + , spawnLocal + , monitor + , Process() + , ProcessId + , MonitorRef) + +-- | Node local version of 'Control.Distributed.Process.spawnLink'. +-- Note that this is just the sequential composition of 'spawn' and 'link'. +-- (The "Unified" semantics that underlies Cloud Haskell does not even support +-- a synchronous link operation) +spawnLinkLocal :: Process () -> Process ProcessId +spawnLinkLocal p = do + pid <- spawnLocal p + link pid + return pid + +-- | Like 'spawnLinkLocal', but monitor the spawned process +spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) +spawnMonitorLocal p = do + pid <- spawnLocal p + ref <- monitor pid + return (pid, ref) From f597525b430cc214d4943779380e917c90ddd0a9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:42:34 +0000 Subject: [PATCH 0553/2357] Extra primitives --- distributed-process-platform.cabal | 6 +++- src/Control/Distributed/Platform.hs | 6 +++- .../Platform/Internal/Primitives.hs | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2d6e1664..8ab903ff 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,13 +29,16 @@ library distributed-static, binary, mtl, + stm >= 2.3 && < 2.5, transformers hs-source-dirs: src ghc-options: -Wall exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async + other-modules: Control.Distributed.Platform.Internal.Primitives test-suite PlatformTests type: exitcode-stdio-1.0 @@ -61,5 +64,6 @@ test-suite PlatformTests other-modules: Control.Distributed.Platform.Timer, Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..a5672b78 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,8 +2,11 @@ -- module Control.Distributed.Platform ( + -- extra primitives + spawnLinkLocal + , spawnMonitorLocal -- time interval handling - milliseconds + , milliseconds , seconds , minutes , hours @@ -15,6 +18,7 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Internal.Primitives -------------------------------------------------------------------------------- -- API -- diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs new file mode 100644 index 00000000..ffe09e6b --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -0,0 +1,33 @@ +-- | Common Entities used throughout -platform. +-- NB: Please DO NOT use this module as a dumping ground. +-- +module Control.Distributed.Platform.Internal.Primitives + ( spawnLinkLocal + , spawnMonitorLocal + ) +where + +import Control.Distributed.Process + ( link + , spawnLocal + , monitor + , Process() + , ProcessId + , MonitorRef) + +-- | Node local version of 'Control.Distributed.Process.spawnLink'. +-- Note that this is just the sequential composition of 'spawn' and 'link'. +-- (The "Unified" semantics that underlies Cloud Haskell does not even support +-- a synchronous link operation) +spawnLinkLocal :: Process () -> Process ProcessId +spawnLinkLocal p = do + pid <- spawnLocal p + link pid + return pid + +-- | Like 'spawnLinkLocal', but monitor the spawned process +spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) +spawnMonitorLocal p = do + pid <- spawnLocal p + ref <- monitor pid + return (pid, ref) From 852ec1545253e88eb4e42094f30650ae9e7ca8c7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:42:34 +0000 Subject: [PATCH 0554/2357] Extra primitives --- distributed-process-platform.cabal | 6 +++- src/Control/Distributed/Platform.hs | 6 +++- .../Platform/Internal/Primitives.hs | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 2d6e1664..8ab903ff 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,13 +29,16 @@ library distributed-static, binary, mtl, + stm >= 2.3 && < 2.5, transformers hs-source-dirs: src ghc-options: -Wall exposed-modules: Control.Distributed.Platform, Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer + Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async + other-modules: Control.Distributed.Platform.Internal.Primitives test-suite PlatformTests type: exitcode-stdio-1.0 @@ -61,5 +64,6 @@ test-suite PlatformTests other-modules: Control.Distributed.Platform.Timer, Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives extensions: CPP main-is: TestMain.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index 3dcfffd2..a5672b78 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -2,8 +2,11 @@ -- module Control.Distributed.Platform ( + -- extra primitives + spawnLinkLocal + , spawnMonitorLocal -- time interval handling - milliseconds + , milliseconds , seconds , minutes , hours @@ -15,6 +18,7 @@ module Control.Distributed.Platform ) where import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Platform.Internal.Primitives -------------------------------------------------------------------------------- -- API -- diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs new file mode 100644 index 00000000..ffe09e6b --- /dev/null +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -0,0 +1,33 @@ +-- | Common Entities used throughout -platform. +-- NB: Please DO NOT use this module as a dumping ground. +-- +module Control.Distributed.Platform.Internal.Primitives + ( spawnLinkLocal + , spawnMonitorLocal + ) +where + +import Control.Distributed.Process + ( link + , spawnLocal + , monitor + , Process() + , ProcessId + , MonitorRef) + +-- | Node local version of 'Control.Distributed.Process.spawnLink'. +-- Note that this is just the sequential composition of 'spawn' and 'link'. +-- (The "Unified" semantics that underlies Cloud Haskell does not even support +-- a synchronous link operation) +spawnLinkLocal :: Process () -> Process ProcessId +spawnLinkLocal p = do + pid <- spawnLocal p + link pid + return pid + +-- | Like 'spawnLinkLocal', but monitor the spawned process +spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) +spawnMonitorLocal p = do + pid <- spawnLocal p + ref <- monitor pid + return (pid, ref) From e206644f19f9ef89bcb5d768b6d29b195ee01ad2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:58:21 +0000 Subject: [PATCH 0555/2357] rework testing infrastructure --- distributed-process-platform.cabal | 5 +++-- tests/TestMain.hs | 28 ---------------------------- 2 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 tests/TestMain.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8ab903ff..75c83ac2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,7 @@ library Control.Distributed.Platform.Async other-modules: Control.Distributed.Platform.Internal.Primitives -test-suite PlatformTests +test-suite TimerTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -65,5 +65,6 @@ test-suite PlatformTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + TestUtils extensions: CPP - main-is: TestMain.hs + main-is: TestTimer.hs diff --git a/tests/TestMain.hs b/tests/TestMain.hs deleted file mode 100644 index 06ad8584..00000000 --- a/tests/TestMain.hs +++ /dev/null @@ -1,28 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -module Main where - -import Test.Framework (Test, defaultMain, testGroup) -import qualified Network.Transport as NT -import Network.Transport.TCP -import TestGenServer (genServerTests) -import TestTimer (timerTests) - -tests :: NT.Transport -> TransportInternals -> IO [Test] -tests transport internals = do - -- gsTestGroup <- genServerTests transport internals - timerTestGroup <- timerTests transport internals - return [ - testGroup "Timer" timerTestGroup ] - -- , testGroup "GenServer" gsTestGroup ] - -main :: IO () -main = do - Right (transport, internals) <- createTransportExposeInternals - "127.0.0.1" "8080" defaultTCPParameters - testData <- tests transport internals - defaultMain testData From 80a846fd2baa9524b2b191247953382da2ef7ac2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:58:21 +0000 Subject: [PATCH 0556/2357] rework testing infrastructure --- distributed-process-platform.cabal | 5 +- tests/TestMain.hs | 28 ------- tests/TestUtils.hs | 121 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 30 deletions(-) delete mode 100644 tests/TestMain.hs create mode 100644 tests/TestUtils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8ab903ff..75c83ac2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,7 @@ library Control.Distributed.Platform.Async other-modules: Control.Distributed.Platform.Internal.Primitives -test-suite PlatformTests +test-suite TimerTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -65,5 +65,6 @@ test-suite PlatformTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + TestUtils extensions: CPP - main-is: TestMain.hs + main-is: TestTimer.hs diff --git a/tests/TestMain.hs b/tests/TestMain.hs deleted file mode 100644 index 06ad8584..00000000 --- a/tests/TestMain.hs +++ /dev/null @@ -1,28 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -module Main where - -import Test.Framework (Test, defaultMain, testGroup) -import qualified Network.Transport as NT -import Network.Transport.TCP -import TestGenServer (genServerTests) -import TestTimer (timerTests) - -tests :: NT.Transport -> TransportInternals -> IO [Test] -tests transport internals = do - -- gsTestGroup <- genServerTests transport internals - timerTestGroup <- timerTests transport internals - return [ - testGroup "Timer" timerTestGroup ] - -- , testGroup "GenServer" gsTestGroup ] - -main :: IO () -main = do - Right (transport, internals) <- createTransportExposeInternals - "127.0.0.1" "8080" defaultTCPParameters - testData <- tests transport internals - defaultMain testData diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs new file mode 100644 index 00000000..17022abb --- /dev/null +++ b/tests/TestUtils.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module TestUtils + ( TestResult + , noop + , stash + -- ping ! + , Ping(Ping) + , ping + -- test process utilities + , TestProcessControl + , startTestProcess + , runTestProcess + , testProcessGo + , testProcessStop + , testProcessReport + , delayedAssertion + , assertComplete + -- runners + , testMain + ) where + +import Prelude hiding (catch) +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + ) +import Control.Distributed.Process +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() + +import Control.Monad (forever) + +import Test.HUnit (Assertion) +import Test.HUnit.Base (assertBool) +import Test.Framework (Test, defaultMain) + +import Network.Transport.TCP +import qualified Network.Transport as NT + +-- | A mutable cell containing a test result. +type TestResult a = MVar a + +-- | A simple @Ping@ signal +data Ping = Ping + deriving (Typeable, Eq, Show) +$(derive makeBinary ''Ping) + +ping :: ProcessId -> Process () +ping pid = send pid Ping + +-- | Control signals used to manage /test processes/ +data TestProcessControl = Stop | Go | Report ProcessId + deriving (Typeable) +$(derive makeBinary ''TestProcessControl) + +-- | Starts a test process on the local node. +startTestProcess :: Process () -> Process ProcessId +startTestProcess proc = spawnLocal $ runTestProcess proc + +-- | Runs a /test process/ around the supplied @proc@, which is executed +-- whenever the outer process loop receives a 'Go' signal. +runTestProcess :: Process () -> Process () +runTestProcess proc = forever $ do + ctl <- expect + case ctl of + Stop -> terminate + Go -> proc + Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () + +-- | Tell a /test process/ to continue executing +testProcessGo :: ProcessId -> Process () +testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go + +-- | Tell a /test process/ to stop (i.e., 'terminate') +testProcessStop :: ProcessId -> Process () +testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop + +-- | Tell a /test process/ to send a report (message) +-- back to the calling process +testProcessReport :: ProcessId -> Process () +testProcessReport pid = do + self <- getSelfPid + send pid $ Report self + +-- | Run the supplied @testProc@ using an @MVar@ to collect and assert +-- against its result. Uses the supplied @note@ if the assertion fails. +delayedAssertion :: (Eq a) => String -> LocalNode -> a -> + (TestResult a -> Process ()) -> Assertion +delayedAssertion note localNode expected testProc = do + result <- newEmptyMVar + _ <- forkProcess localNode $ testProc result + assertComplete note result expected + +-- | Takes the value of @mv@ (using @takeMVar@) and asserts that it matches @a@ +assertComplete :: (Eq a) => String -> MVar a -> a -> IO () +assertComplete msg mv a = do + b <- takeMVar mv + assertBool msg (a == b) + +-- | Does exactly what it says on the tin, doing so in the @Process@ monad. +noop :: Process () +noop = return () + +-- | Stashes a value in our 'TestResult' using @putMVar@ +stash :: TestResult a -> a -> Process () +stash mvar x = liftIO $ putMVar mvar x + +testMain :: (NT.Transport -> IO [Test]) -> IO () +testMain builder = do + Right (transport, _) <- createTransportExposeInternals + "127.0.0.1" "8080" defaultTCPParameters + testData <- builder transport + defaultMain testData + From 0ecd3841b4e534726f3e7cb4779e8897153b065f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:58:21 +0000 Subject: [PATCH 0557/2357] rework testing infrastructure --- distributed-process-platform.cabal | 5 +- tests/TestMain.hs | 28 ------- tests/TestUtils.hs | 121 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 30 deletions(-) delete mode 100644 tests/TestMain.hs create mode 100644 tests/TestUtils.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8ab903ff..75c83ac2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,7 @@ library Control.Distributed.Platform.Async other-modules: Control.Distributed.Platform.Internal.Primitives -test-suite PlatformTests +test-suite TimerTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -65,5 +65,6 @@ test-suite PlatformTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + TestUtils extensions: CPP - main-is: TestMain.hs + main-is: TestTimer.hs diff --git a/tests/TestMain.hs b/tests/TestMain.hs deleted file mode 100644 index 06ad8584..00000000 --- a/tests/TestMain.hs +++ /dev/null @@ -1,28 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -module Main where - -import Test.Framework (Test, defaultMain, testGroup) -import qualified Network.Transport as NT -import Network.Transport.TCP -import TestGenServer (genServerTests) -import TestTimer (timerTests) - -tests :: NT.Transport -> TransportInternals -> IO [Test] -tests transport internals = do - -- gsTestGroup <- genServerTests transport internals - timerTestGroup <- timerTests transport internals - return [ - testGroup "Timer" timerTestGroup ] - -- , testGroup "GenServer" gsTestGroup ] - -main :: IO () -main = do - Right (transport, internals) <- createTransportExposeInternals - "127.0.0.1" "8080" defaultTCPParameters - testData <- tests transport internals - defaultMain testData diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs new file mode 100644 index 00000000..17022abb --- /dev/null +++ b/tests/TestUtils.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +module TestUtils + ( TestResult + , noop + , stash + -- ping ! + , Ping(Ping) + , ping + -- test process utilities + , TestProcessControl + , startTestProcess + , runTestProcess + , testProcessGo + , testProcessStop + , testProcessReport + , delayedAssertion + , assertComplete + -- runners + , testMain + ) where + +import Prelude hiding (catch) +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH +import Control.Concurrent.MVar + ( MVar + , newEmptyMVar + , putMVar + , takeMVar + ) +import Control.Distributed.Process +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() + +import Control.Monad (forever) + +import Test.HUnit (Assertion) +import Test.HUnit.Base (assertBool) +import Test.Framework (Test, defaultMain) + +import Network.Transport.TCP +import qualified Network.Transport as NT + +-- | A mutable cell containing a test result. +type TestResult a = MVar a + +-- | A simple @Ping@ signal +data Ping = Ping + deriving (Typeable, Eq, Show) +$(derive makeBinary ''Ping) + +ping :: ProcessId -> Process () +ping pid = send pid Ping + +-- | Control signals used to manage /test processes/ +data TestProcessControl = Stop | Go | Report ProcessId + deriving (Typeable) +$(derive makeBinary ''TestProcessControl) + +-- | Starts a test process on the local node. +startTestProcess :: Process () -> Process ProcessId +startTestProcess proc = spawnLocal $ runTestProcess proc + +-- | Runs a /test process/ around the supplied @proc@, which is executed +-- whenever the outer process loop receives a 'Go' signal. +runTestProcess :: Process () -> Process () +runTestProcess proc = forever $ do + ctl <- expect + case ctl of + Stop -> terminate + Go -> proc + Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () + +-- | Tell a /test process/ to continue executing +testProcessGo :: ProcessId -> Process () +testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go + +-- | Tell a /test process/ to stop (i.e., 'terminate') +testProcessStop :: ProcessId -> Process () +testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop + +-- | Tell a /test process/ to send a report (message) +-- back to the calling process +testProcessReport :: ProcessId -> Process () +testProcessReport pid = do + self <- getSelfPid + send pid $ Report self + +-- | Run the supplied @testProc@ using an @MVar@ to collect and assert +-- against its result. Uses the supplied @note@ if the assertion fails. +delayedAssertion :: (Eq a) => String -> LocalNode -> a -> + (TestResult a -> Process ()) -> Assertion +delayedAssertion note localNode expected testProc = do + result <- newEmptyMVar + _ <- forkProcess localNode $ testProc result + assertComplete note result expected + +-- | Takes the value of @mv@ (using @takeMVar@) and asserts that it matches @a@ +assertComplete :: (Eq a) => String -> MVar a -> a -> IO () +assertComplete msg mv a = do + b <- takeMVar mv + assertBool msg (a == b) + +-- | Does exactly what it says on the tin, doing so in the @Process@ monad. +noop :: Process () +noop = return () + +-- | Stashes a value in our 'TestResult' using @putMVar@ +stash :: TestResult a -> a -> Process () +stash mvar x = liftIO $ putMVar mvar x + +testMain :: (NT.Transport -> IO [Test]) -> IO () +testMain builder = do + Right (transport, _) <- createTransportExposeInternals + "127.0.0.1" "8080" defaultTCPParameters + testData <- builder transport + defaultMain testData + From 53e73c96aa99c8bf09e6fb708bbee42391e53534 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 21 Dec 2012 17:58:21 +0000 Subject: [PATCH 0558/2357] rework testing infrastructure --- distributed-process-platform.cabal | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 8ab903ff..75c83ac2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,7 @@ library Control.Distributed.Platform.Async other-modules: Control.Distributed.Platform.Internal.Primitives -test-suite PlatformTests +test-suite TimerTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -65,5 +65,6 @@ test-suite PlatformTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + TestUtils extensions: CPP - main-is: TestMain.hs + main-is: TestTimer.hs From 2ec695b66f2acf84abd030f05c270b362a8fcaae Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:04:24 +0000 Subject: [PATCH 0559/2357] Async => AsyncChan; add test case for waitAny --- distributed-process-platform.cabal | 10 +- src/Control/Distributed/Platform/Async.hs | 173 +------------ .../Distributed/Platform/Async/AsyncChan.hs | 230 ++++++++++++++++++ tests/TestAsync.hs | 45 ++-- 4 files changed, 270 insertions(+), 188 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncChan.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b6fe03f2..50c8a1a3 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,10 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async.AsyncChan + other-modules: + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives test-suite AsyncTests @@ -66,7 +68,8 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestAsync.hs @@ -95,6 +98,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index dad005ae..cc56b376 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -27,45 +27,21 @@ -- ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async - ( -- types/data +module Control.Distributed.Platform.Async + ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncChan(worker) , AsyncResult(..) - -- functions for starting/spawning - , asyncChan - , asyncChanLinked - -- and stopping/killing - , cancelChan - , cancelChanWait - -- functions to query an async-result - , pollChan - , checkChan - , waitChan - , waitChanTimeout - , waitChanCheckTimeout ) where -import Control.Distributed.Platform.Timer - ( intervalToMs - ) -import Control.Distributed.Platform.Internal.Types - ( CancelWait(..) - , TimeInterval() - ) import Control.Distributed.Process -import Control.Distributed.Process.Serializable import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) -import Data.Maybe - ( fromMaybe - ) -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- @@ -86,21 +62,6 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Private channel used to synchronise task results -type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). --- --- Handles of this type cannot cross remote boundaries. -data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId - , channel :: (InternalChannel a) - } - -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = @@ -124,133 +85,3 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- run on the local node. That could be a limitation, as there's nothing in -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. - --- | Spawns an asynchronous action in a new process. --- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. --- -asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChan = asyncChanDo False - --- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. --- -asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChanLinked = asyncChanDo True - -asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) -asyncChanDo shouldLink task = do - (wpid, gpid, chan) <- spawnWorkers task shouldLink - return AsyncChan { - worker = wpid - , insulator = gpid - , channel = chan - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) -spawnWorkers task shouldLink = do - root <- getSelfPid - chan <- newChan - - -- listener/response proxy - insulatorPid <- spawnLocal $ do - workerPid <- spawnLocal $ do - r <- task - sendChan (fst chan) (AsyncDone r) - - send root workerPid -- let the parent process know the worker pid - - wref <- monitor workerPid - rref <- case shouldLink of - True -> monitor root >>= return . Just - False -> return Nothing - finally (pollUntilExit workerPid chan) - (unmonitor wref >> - return (maybe (return ()) unmonitor rref)) - - workerPid <- expect - return (workerPid, insulatorPid, chan) - where - -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) - => ProcessId - -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - -> Process () - pollUntilExit wpid (replyTo, _) = do - r <- receiveWait [ - match (\(ProcessMonitorNotification _ pid' r) -> - return (Right (pid', r))) - , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right (fpid, d) - | fpid == wpid -> case d of - DiedNormal -> return () - _ -> sendChan replyTo (AsyncFailed d) - | otherwise -> kill wpid "linkFailed" - --- | Check whether an 'AsyncChan' has completed yet. The status of the --- action is encoded in the returned 'AsyncResult'. If the action has not --- completed, the result will be 'AsyncPending', or one of the other --- constructors otherwise. This function does not block waiting for the result. --- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. --- See 'Async'. -pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -pollChan hAsync = do - r <- receiveChanTimeout 0 $ snd (channel hAsync) - return $ fromMaybe (AsyncPending) r - --- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. --- See 'poll'. -checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) -checkChan hAsync = pollChan hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) - --- | Wait for an asynchronous operation to complete or timeout. This variant --- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the --- result has not been made available, otherwise one of the other constructors. -waitChanCheckTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (AsyncResult a) -waitChanCheckTimeout t hAsync = - waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) - --- | Wait for an asynchronous action to complete, and return its --- value. The outcome of the action is encoded as an 'AsyncResult'. --- -waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -waitChan hAsync = receiveChan $ snd (channel hAsync) - --- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within --- the specified delay, otherwise @Just asyncResult@ is returned. If you want --- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. -waitChanTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) -waitChanTimeout t hAsync = - receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) - --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. -cancelChan :: AsyncChan a -> Process () -cancelChan (AsyncChan _ g _) = send g CancelWait - --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs new file mode 100644 index 00000000..0d5a300c --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncChan + ( -- types/data + AsyncRef + , AsyncWorkerId + , AsyncGathererId + , AsyncTask + , AsyncCancel + , AsyncChan(worker) + , AsyncResult(..) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing + , cancel + , cancelWait + -- functions to query an async-result + , poll + , check + , wait + , waitAny + , waitTimeout + , waitCheckTimeout + ) where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | Private channel used to synchronise task results +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncLinked = async + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r + +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. +check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. +waitCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +wait hAsync = receiveChan $ snd (channel hAsync) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'wait' or 'waitCheckTimeout' instead. +waitTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Wait for any of the supplied @AsyncChans@s to complete. If multiple +-- 'Async's complete, then the value returned corresponds to the first +-- completed 'Async' in the list. Only /unread/ 'Async's are of value here, +-- because 'AsyncChan' does not hold on to its result after it has been read! +-- +-- This function is analagous to the @mergePortsBiased@ primitive. +-- See 'Control.Distibuted.Process.mergePortsBiased' +waitAny :: (Serializable a) + => [AsyncChan a] + -> Process (AsyncResult a) +waitAny asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChan + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: AsyncChan a -> Process () +cancel (AsyncChan _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 698a8451..35950e9b 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -12,7 +12,7 @@ import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Platform -import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Async.AsyncChan import Data.Binary() import Data.Typeable() import qualified Network.Transport as NT (Transport) @@ -24,34 +24,34 @@ import TestUtils testAsyncPoll :: TestResult (AsyncResult ()) -> Process () testAsyncPoll result = do - hAsync <- asyncChan $ do "go" <- expect; say "running" >> return () - ar <- pollChan hAsync + hAsync <- async $ do "go" <- expect; say "running" >> return () + ar <- poll hAsync case ar of AsyncPending -> - send (worker hAsync) "go" >> waitChan hAsync >>= stash result + send (worker hAsync) "go" >> wait hAsync >>= stash result _ -> stash result ar >> return () testAsyncCancel :: TestResult (AsyncResult ()) -> Process () testAsyncCancel result = do - hAsync <- asyncChan $ runTestProcess $ say "running" >> return () + hAsync <- async $ runTestProcess $ say "running" >> return () sleep $ milliseconds 100 - p <- pollChan hAsync -- nasty kind of assertion: use assertEquals? + p <- poll hAsync -- nasty kind of assertion: use assertEquals? case p of - AsyncPending -> cancelChan hAsync >> waitChan hAsync >>= stash result + AsyncPending -> cancel hAsync >> wait hAsync >>= stash result _ -> say (show p) >> stash result p testAsyncCancelWait :: TestResult (Maybe (AsyncResult ())) -> Process () testAsyncCancelWait result = do testPid <- getSelfPid p <- spawnLocal $ do - hAsync <- asyncChan $ runTestProcess $ say "running" >> (sleep $ seconds 60) + hAsync <- async $ runTestProcess $ say "running" >> (sleep $ seconds 60) sleep $ milliseconds 100 send testPid "running" - AsyncPending <- pollChan hAsync - cancelChanWait hAsync >>= send testPid + AsyncPending <- poll hAsync + cancelWait hAsync >>= send testPid "running" <- expect d <- expectTimeout (intervalToMs $ seconds 5) @@ -63,15 +63,15 @@ testAsyncWaitTimeout :: TestResult (Maybe (AsyncResult ())) -> Process () testAsyncWaitTimeout result = let delay = seconds 1 in do - hAsync <- asyncChan $ sleep $ seconds 20 - waitChanTimeout delay hAsync >>= stash result - cancelChanWait hAsync >> return () + hAsync <- async $ sleep $ seconds 20 + waitTimeout delay hAsync >>= stash result + cancelWait hAsync >> return () testAsyncLinked :: TestResult Bool -> Process () testAsyncLinked result = do mv :: MVar (AsyncChan ()) <- liftIO $ newEmptyMVar pid <- spawnLocal $ do - h <- asyncChanLinked $ do + h <- asyncLinked $ do "waiting" <- expect return () stash mv h @@ -90,6 +90,19 @@ testAsyncLinked result = do -- ReceivePort is no longer valid, so we can't wait on it! We have to ensure -- that the worker is really dead then.... stash result $ mref == mref' + +testAsyncWaitAny :: TestResult String -> Process () +testAsyncWaitAny result = do + p1 <- async $ expect >>= return + p2 <- async $ expect >>= return + p3 <- async $ expect >>= return + send (worker p3) "c" + AsyncDone r1 <- waitAny [p1, p2, p3] + send (worker p1) "a" + AsyncDone r2 <- waitAny [p1, p2, p3] + send (worker p2) "b" + AsyncDone r3 <- waitAny [p1, p2, p3] + stash result $ foldl (++) "" [r1, r2, r3] tests :: LocalNode -> [Test] tests localNode = [ @@ -114,6 +127,10 @@ tests localNode = [ (delayedAssertion "expected linked process to die with originator" localNode True testAsyncLinked) + , testCase "testAsyncWaitAny" + (delayedAssertion + "expected waitAny to mimic mergePortsBiased" + localNode "cab" testAsyncWaitAny) ] ] From 80298df612e2d422a982a0d1ec29b178786b25c2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:04:24 +0000 Subject: [PATCH 0560/2357] Async => AsyncChan; add test case for waitAny --- distributed-process-platform.cabal | 10 +- src/Control/Distributed/Platform/Async.hs | 173 +------------ .../Distributed/Platform/Async/AsyncChan.hs | 230 ++++++++++++++++++ 3 files changed, 239 insertions(+), 174 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncChan.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b6fe03f2..50c8a1a3 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,10 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async.AsyncChan + other-modules: + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives test-suite AsyncTests @@ -66,7 +68,8 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestAsync.hs @@ -95,6 +98,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index dad005ae..cc56b376 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -27,45 +27,21 @@ -- ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async - ( -- types/data +module Control.Distributed.Platform.Async + ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncChan(worker) , AsyncResult(..) - -- functions for starting/spawning - , asyncChan - , asyncChanLinked - -- and stopping/killing - , cancelChan - , cancelChanWait - -- functions to query an async-result - , pollChan - , checkChan - , waitChan - , waitChanTimeout - , waitChanCheckTimeout ) where -import Control.Distributed.Platform.Timer - ( intervalToMs - ) -import Control.Distributed.Platform.Internal.Types - ( CancelWait(..) - , TimeInterval() - ) import Control.Distributed.Process -import Control.Distributed.Process.Serializable import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) -import Data.Maybe - ( fromMaybe - ) -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- @@ -86,21 +62,6 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Private channel used to synchronise task results -type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). --- --- Handles of this type cannot cross remote boundaries. -data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId - , channel :: (InternalChannel a) - } - -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = @@ -124,133 +85,3 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- run on the local node. That could be a limitation, as there's nothing in -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. - --- | Spawns an asynchronous action in a new process. --- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. --- -asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChan = asyncChanDo False - --- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. --- -asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChanLinked = asyncChanDo True - -asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) -asyncChanDo shouldLink task = do - (wpid, gpid, chan) <- spawnWorkers task shouldLink - return AsyncChan { - worker = wpid - , insulator = gpid - , channel = chan - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) -spawnWorkers task shouldLink = do - root <- getSelfPid - chan <- newChan - - -- listener/response proxy - insulatorPid <- spawnLocal $ do - workerPid <- spawnLocal $ do - r <- task - sendChan (fst chan) (AsyncDone r) - - send root workerPid -- let the parent process know the worker pid - - wref <- monitor workerPid - rref <- case shouldLink of - True -> monitor root >>= return . Just - False -> return Nothing - finally (pollUntilExit workerPid chan) - (unmonitor wref >> - return (maybe (return ()) unmonitor rref)) - - workerPid <- expect - return (workerPid, insulatorPid, chan) - where - -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) - => ProcessId - -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - -> Process () - pollUntilExit wpid (replyTo, _) = do - r <- receiveWait [ - match (\(ProcessMonitorNotification _ pid' r) -> - return (Right (pid', r))) - , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right (fpid, d) - | fpid == wpid -> case d of - DiedNormal -> return () - _ -> sendChan replyTo (AsyncFailed d) - | otherwise -> kill wpid "linkFailed" - --- | Check whether an 'AsyncChan' has completed yet. The status of the --- action is encoded in the returned 'AsyncResult'. If the action has not --- completed, the result will be 'AsyncPending', or one of the other --- constructors otherwise. This function does not block waiting for the result. --- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. --- See 'Async'. -pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -pollChan hAsync = do - r <- receiveChanTimeout 0 $ snd (channel hAsync) - return $ fromMaybe (AsyncPending) r - --- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. --- See 'poll'. -checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) -checkChan hAsync = pollChan hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) - --- | Wait for an asynchronous operation to complete or timeout. This variant --- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the --- result has not been made available, otherwise one of the other constructors. -waitChanCheckTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (AsyncResult a) -waitChanCheckTimeout t hAsync = - waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) - --- | Wait for an asynchronous action to complete, and return its --- value. The outcome of the action is encoded as an 'AsyncResult'. --- -waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -waitChan hAsync = receiveChan $ snd (channel hAsync) - --- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within --- the specified delay, otherwise @Just asyncResult@ is returned. If you want --- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. -waitChanTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) -waitChanTimeout t hAsync = - receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) - --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. -cancelChan :: AsyncChan a -> Process () -cancelChan (AsyncChan _ g _) = send g CancelWait - --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs new file mode 100644 index 00000000..0d5a300c --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncChan + ( -- types/data + AsyncRef + , AsyncWorkerId + , AsyncGathererId + , AsyncTask + , AsyncCancel + , AsyncChan(worker) + , AsyncResult(..) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing + , cancel + , cancelWait + -- functions to query an async-result + , poll + , check + , wait + , waitAny + , waitTimeout + , waitCheckTimeout + ) where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | Private channel used to synchronise task results +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncLinked = async + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r + +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. +check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. +waitCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +wait hAsync = receiveChan $ snd (channel hAsync) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'wait' or 'waitCheckTimeout' instead. +waitTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Wait for any of the supplied @AsyncChans@s to complete. If multiple +-- 'Async's complete, then the value returned corresponds to the first +-- completed 'Async' in the list. Only /unread/ 'Async's are of value here, +-- because 'AsyncChan' does not hold on to its result after it has been read! +-- +-- This function is analagous to the @mergePortsBiased@ primitive. +-- See 'Control.Distibuted.Process.mergePortsBiased' +waitAny :: (Serializable a) + => [AsyncChan a] + -> Process (AsyncResult a) +waitAny asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChan + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: AsyncChan a -> Process () +cancel (AsyncChan _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync From dd711a62fbfe86a0412b8153d344a82a9b5283bf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:04:24 +0000 Subject: [PATCH 0561/2357] Async => AsyncChan; add test case for waitAny --- distributed-process-platform.cabal | 10 +- src/Control/Distributed/Platform/Async.hs | 173 +------------ .../Distributed/Platform/Async/AsyncChan.hs | 230 ++++++++++++++++++ 3 files changed, 239 insertions(+), 174 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncChan.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b6fe03f2..50c8a1a3 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,10 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async.AsyncChan + other-modules: + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives test-suite AsyncTests @@ -66,7 +68,8 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestAsync.hs @@ -95,6 +98,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index dad005ae..cc56b376 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -27,45 +27,21 @@ -- ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async - ( -- types/data +module Control.Distributed.Platform.Async + ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncChan(worker) , AsyncResult(..) - -- functions for starting/spawning - , asyncChan - , asyncChanLinked - -- and stopping/killing - , cancelChan - , cancelChanWait - -- functions to query an async-result - , pollChan - , checkChan - , waitChan - , waitChanTimeout - , waitChanCheckTimeout ) where -import Control.Distributed.Platform.Timer - ( intervalToMs - ) -import Control.Distributed.Platform.Internal.Types - ( CancelWait(..) - , TimeInterval() - ) import Control.Distributed.Process -import Control.Distributed.Process.Serializable import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) -import Data.Maybe - ( fromMaybe - ) -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- @@ -86,21 +62,6 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Private channel used to synchronise task results -type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). --- --- Handles of this type cannot cross remote boundaries. -data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId - , channel :: (InternalChannel a) - } - -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = @@ -124,133 +85,3 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- run on the local node. That could be a limitation, as there's nothing in -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. - --- | Spawns an asynchronous action in a new process. --- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. --- -asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChan = asyncChanDo False - --- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. --- -asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChanLinked = asyncChanDo True - -asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) -asyncChanDo shouldLink task = do - (wpid, gpid, chan) <- spawnWorkers task shouldLink - return AsyncChan { - worker = wpid - , insulator = gpid - , channel = chan - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) -spawnWorkers task shouldLink = do - root <- getSelfPid - chan <- newChan - - -- listener/response proxy - insulatorPid <- spawnLocal $ do - workerPid <- spawnLocal $ do - r <- task - sendChan (fst chan) (AsyncDone r) - - send root workerPid -- let the parent process know the worker pid - - wref <- monitor workerPid - rref <- case shouldLink of - True -> monitor root >>= return . Just - False -> return Nothing - finally (pollUntilExit workerPid chan) - (unmonitor wref >> - return (maybe (return ()) unmonitor rref)) - - workerPid <- expect - return (workerPid, insulatorPid, chan) - where - -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) - => ProcessId - -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - -> Process () - pollUntilExit wpid (replyTo, _) = do - r <- receiveWait [ - match (\(ProcessMonitorNotification _ pid' r) -> - return (Right (pid', r))) - , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right (fpid, d) - | fpid == wpid -> case d of - DiedNormal -> return () - _ -> sendChan replyTo (AsyncFailed d) - | otherwise -> kill wpid "linkFailed" - --- | Check whether an 'AsyncChan' has completed yet. The status of the --- action is encoded in the returned 'AsyncResult'. If the action has not --- completed, the result will be 'AsyncPending', or one of the other --- constructors otherwise. This function does not block waiting for the result. --- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. --- See 'Async'. -pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -pollChan hAsync = do - r <- receiveChanTimeout 0 $ snd (channel hAsync) - return $ fromMaybe (AsyncPending) r - --- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. --- See 'poll'. -checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) -checkChan hAsync = pollChan hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) - --- | Wait for an asynchronous operation to complete or timeout. This variant --- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the --- result has not been made available, otherwise one of the other constructors. -waitChanCheckTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (AsyncResult a) -waitChanCheckTimeout t hAsync = - waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) - --- | Wait for an asynchronous action to complete, and return its --- value. The outcome of the action is encoded as an 'AsyncResult'. --- -waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -waitChan hAsync = receiveChan $ snd (channel hAsync) - --- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within --- the specified delay, otherwise @Just asyncResult@ is returned. If you want --- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. -waitChanTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) -waitChanTimeout t hAsync = - receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) - --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. -cancelChan :: AsyncChan a -> Process () -cancelChan (AsyncChan _ g _) = send g CancelWait - --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs new file mode 100644 index 00000000..0d5a300c --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncChan + ( -- types/data + AsyncRef + , AsyncWorkerId + , AsyncGathererId + , AsyncTask + , AsyncCancel + , AsyncChan(worker) + , AsyncResult(..) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing + , cancel + , cancelWait + -- functions to query an async-result + , poll + , check + , wait + , waitAny + , waitTimeout + , waitCheckTimeout + ) where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | Private channel used to synchronise task results +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncLinked = async + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r + +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. +check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. +waitCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +wait hAsync = receiveChan $ snd (channel hAsync) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'wait' or 'waitCheckTimeout' instead. +waitTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Wait for any of the supplied @AsyncChans@s to complete. If multiple +-- 'Async's complete, then the value returned corresponds to the first +-- completed 'Async' in the list. Only /unread/ 'Async's are of value here, +-- because 'AsyncChan' does not hold on to its result after it has been read! +-- +-- This function is analagous to the @mergePortsBiased@ primitive. +-- See 'Control.Distibuted.Process.mergePortsBiased' +waitAny :: (Serializable a) + => [AsyncChan a] + -> Process (AsyncResult a) +waitAny asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChan + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: AsyncChan a -> Process () +cancel (AsyncChan _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync From f1e90d3a7a705673a4accf4ea5a742361e4b7ca5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:04:24 +0000 Subject: [PATCH 0562/2357] Async => AsyncChan; add test case for waitAny --- distributed-process-platform.cabal | 10 +- src/Control/Distributed/Platform/Async.hs | 173 +------------ .../Distributed/Platform/Async/AsyncChan.hs | 230 ++++++++++++++++++ 3 files changed, 239 insertions(+), 174 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncChan.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b6fe03f2..50c8a1a3 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,10 @@ library Control.Distributed.Platform, Control.Distributed.Platform.GenServer, Control.Distributed.Platform.Timer, + Control.Distributed.Platform.Async.AsyncChan + other-modules: + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives test-suite AsyncTests @@ -66,7 +68,8 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestAsync.hs @@ -95,6 +98,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils + TestUtils, + Control.Distributed.Platform.Async extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index dad005ae..cc56b376 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -27,45 +27,21 @@ -- ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async - ( -- types/data +module Control.Distributed.Platform.Async + ( -- types/data AsyncRef , AsyncWorkerId , AsyncGathererId , AsyncTask , AsyncCancel - , AsyncChan(worker) , AsyncResult(..) - -- functions for starting/spawning - , asyncChan - , asyncChanLinked - -- and stopping/killing - , cancelChan - , cancelChanWait - -- functions to query an async-result - , pollChan - , checkChan - , waitChan - , waitChanTimeout - , waitChanCheckTimeout ) where -import Control.Distributed.Platform.Timer - ( intervalToMs - ) -import Control.Distributed.Platform.Internal.Types - ( CancelWait(..) - , TimeInterval() - ) import Control.Distributed.Process -import Control.Distributed.Process.Serializable import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) -import Data.Maybe - ( fromMaybe - ) -------------------------------------------------------------------------------- -- Cloud Haskell Async Process API -- @@ -86,21 +62,6 @@ type AsyncGathererId = AsyncRef -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Private channel used to synchronise task results -type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and --- operations are provided for waiting for asynchronous actions to --- complete and obtaining their results (see e.g. 'wait'). --- --- Handles of this type cannot cross remote boundaries. -data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId - , channel :: (InternalChannel a) - } - -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = @@ -124,133 +85,3 @@ type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] -- run on the local node. That could be a limitation, as there's nothing in -- 'Async' data profile to stop it being sent remotely. At *that* point, we'd -- need to make the cancellation remote-able too however. - --- | Spawns an asynchronous action in a new process. --- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. --- -asyncChan :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChan = asyncChanDo False - --- | This is a useful variant of 'asyncChan' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. --- -asyncChanLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) -asyncChanLinked = asyncChanDo True - -asyncChanDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) -asyncChanDo shouldLink task = do - (wpid, gpid, chan) <- spawnWorkers task shouldLink - return AsyncChan { - worker = wpid - , insulator = gpid - , channel = chan - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) -spawnWorkers task shouldLink = do - root <- getSelfPid - chan <- newChan - - -- listener/response proxy - insulatorPid <- spawnLocal $ do - workerPid <- spawnLocal $ do - r <- task - sendChan (fst chan) (AsyncDone r) - - send root workerPid -- let the parent process know the worker pid - - wref <- monitor workerPid - rref <- case shouldLink of - True -> monitor root >>= return . Just - False -> return Nothing - finally (pollUntilExit workerPid chan) - (unmonitor wref >> - return (maybe (return ()) unmonitor rref)) - - workerPid <- expect - return (workerPid, insulatorPid, chan) - where - -- blocking receive until we see an input message - pollUntilExit :: (Serializable a) - => ProcessId - -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) - -> Process () - pollUntilExit wpid (replyTo, _) = do - r <- receiveWait [ - match (\(ProcessMonitorNotification _ pid' r) -> - return (Right (pid', r))) - , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) - ] - case r of - Left CancelWait -> sendChan replyTo AsyncCancelled - Right (fpid, d) - | fpid == wpid -> case d of - DiedNormal -> return () - _ -> sendChan replyTo (AsyncFailed d) - | otherwise -> kill wpid "linkFailed" - --- | Check whether an 'AsyncChan' has completed yet. The status of the --- action is encoded in the returned 'AsyncResult'. If the action has not --- completed, the result will be 'AsyncPending', or one of the other --- constructors otherwise. This function does not block waiting for the result. --- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. --- See 'Async'. -pollChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -pollChan hAsync = do - r <- receiveChanTimeout 0 $ snd (channel hAsync) - return $ fromMaybe (AsyncPending) r - --- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. --- See 'poll'. -checkChan :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) -checkChan hAsync = pollChan hAsync >>= \r -> case r of - AsyncPending -> return Nothing - ar -> return (Just ar) - --- | Wait for an asynchronous operation to complete or timeout. This variant --- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the --- result has not been made available, otherwise one of the other constructors. -waitChanCheckTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (AsyncResult a) -waitChanCheckTimeout t hAsync = - waitChanTimeout t hAsync >>= return . fromMaybe (AsyncPending) - --- | Wait for an asynchronous action to complete, and return its --- value. The outcome of the action is encoded as an 'AsyncResult'. --- -waitChan :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -waitChan hAsync = receiveChan $ snd (channel hAsync) - --- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within --- the specified delay, otherwise @Just asyncResult@ is returned. If you want --- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. -waitChanTimeout :: (Serializable a) => - TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) -waitChanTimeout t hAsync = - receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) - --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. -cancelChan :: AsyncChan a -> Process () -cancelChan (AsyncChan _ g _) = send g CancelWait - --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelChanWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelChanWait hAsync = cancelChan hAsync >> waitChan hAsync diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs new file mode 100644 index 00000000..0d5a300c --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +-- The basic type is @'Async' a@, which represents an asynchronous +-- @Process@ that will return a value of type @a@, or exit with a failure +-- reason. An @Async@ corresponds logically to a worker @Process@, and its +-- 'ProcessId' can be obtained with 'worker', although that should rarely +-- be necessary. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncChan + ( -- types/data + AsyncRef + , AsyncWorkerId + , AsyncGathererId + , AsyncTask + , AsyncCancel + , AsyncChan(worker) + , AsyncResult(..) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing + , cancel + , cancelWait + -- functions to query an async-result + , poll + , check + , wait + , waitAny + , waitTimeout + , waitCheckTimeout + ) where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | Private channel used to synchronise task results +type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncChan a = AsyncChan { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , channel :: (InternalChannel a) + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) +asyncLinked = async + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo shouldLink task = do + (wpid, gpid, chan) <- spawnWorkers task shouldLink + return AsyncChan { + worker = wpid + , insulator = gpid + , channel = chan + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, + (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) +spawnWorkers task shouldLink = do + root <- getSelfPid + chan <- newChan + + -- listener/response proxy + insulatorPid <- spawnLocal $ do + workerPid <- spawnLocal $ do + r <- task + sendChan (fst chan) (AsyncDone r) + + send root workerPid -- let the parent process know the worker pid + + wref <- monitor workerPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit workerPid chan) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + return (workerPid, insulatorPid, chan) + where + -- blocking receive until we see an input message + pollUntilExit :: (Serializable a) + => ProcessId + -> (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) + -> Process () + pollUntilExit wpid (replyTo, _) = do + r <- receiveWait [ + match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + , match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + ] + case r of + Left CancelWait -> sendChan replyTo AsyncCancelled + Right (fpid, d) + | fpid == wpid -> case d of + DiedNormal -> return () + _ -> sendChan replyTo (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncChan' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +poll hAsync = do + r <- receiveChanTimeout 0 $ snd (channel hAsync) + return $ fromMaybe (AsyncPending) r + +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. +check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) + +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. +waitCheckTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (AsyncResult a) +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) + +-- | Wait for an asynchronous action to complete, and return its +-- value. The outcome of the action is encoded as an 'AsyncResult'. +-- +wait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +wait hAsync = receiveChan $ snd (channel hAsync) + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'wait' or 'waitCheckTimeout' instead. +waitTimeout :: (Serializable a) => + TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) +waitTimeout t hAsync = + receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + +-- | Wait for any of the supplied @AsyncChans@s to complete. If multiple +-- 'Async's complete, then the value returned corresponds to the first +-- completed 'Async' in the list. Only /unread/ 'Async's are of value here, +-- because 'AsyncChan' does not hold on to its result after it has been read! +-- +-- This function is analagous to the @mergePortsBiased@ primitive. +-- See 'Control.Distibuted.Process.mergePortsBiased' +waitAny :: (Serializable a) + => [AsyncChan a] + -> Process (AsyncResult a) +waitAny asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChan + +-- | Cancel an asynchronous operation. To wait for cancellation to complete, use +-- 'cancelWait' instead. +cancel :: AsyncChan a -> Process () +cancel (AsyncChan _ g _) = send g CancelWait + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync From 8e571151b1b24ec37a1f13be1d785a2d9c134301 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:17 +0000 Subject: [PATCH 0563/2357] Reduce documentation for 'Async' until the API is stable --- src/Control/Distributed/Platform/Async.hs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index cc56b376..279a982d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,19 +12,13 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- This module provides a set of operations for spawning Process operations --- and waiting for their results. It is a thin layer over the basic +-- The modules herein provides a set of operations for spawning Processes +-- and waiting for theie results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". -- The main feature it provides is a pre-canned set of APIs for waiting on the -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async From 94ea33367442b19bab4c537c44d9b0b97a254758 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:17 +0000 Subject: [PATCH 0564/2357] Reduce documentation for 'Async' until the API is stable --- src/Control/Distributed/Platform/Async.hs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index cc56b376..279a982d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,19 +12,13 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- This module provides a set of operations for spawning Process operations --- and waiting for their results. It is a thin layer over the basic +-- The modules herein provides a set of operations for spawning Processes +-- and waiting for theie results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". -- The main feature it provides is a pre-canned set of APIs for waiting on the -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async From e611d622726ea5bca5e7c9802ccee56864c81c02 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:17 +0000 Subject: [PATCH 0565/2357] Reduce documentation for 'Async' until the API is stable --- src/Control/Distributed/Platform/Async.hs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index cc56b376..279a982d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,19 +12,13 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- This module provides a set of operations for spawning Process operations --- and waiting for their results. It is a thin layer over the basic +-- The modules herein provides a set of operations for spawning Processes +-- and waiting for theie results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". -- The main feature it provides is a pre-canned set of APIs for waiting on the -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async From d9711d6b65e28a64579d234340aadaf8a1b0bdf7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:17 +0000 Subject: [PATCH 0566/2357] Reduce documentation for 'Async' until the API is stable --- src/Control/Distributed/Platform/Async.hs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index cc56b376..279a982d 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,19 +12,13 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- This module provides a set of operations for spawning Process operations --- and waiting for their results. It is a thin layer over the basic +-- The modules herein provides a set of operations for spawning Processes +-- and waiting for theie results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". -- The main feature it provides is a pre-canned set of APIs for waiting on the -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async From 0c1556cf82a108fa4bd568e1da3d6375542ed01d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:32 +0000 Subject: [PATCH 0567/2357] waitAnyTimeout and tests --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 9 +++++++++ tests/TestAsync.hs | 13 ++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 0d5a300c..f29b7daa 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -47,6 +47,7 @@ module Control.Distributed.Platform.Async.AsyncChan , check , wait , waitAny + , waitAnyTimeout , waitTimeout , waitCheckTimeout ) where @@ -216,6 +217,14 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +waitAnyTimeout :: (Serializable a) + => TimeInterval + -> [AsyncChan a] + -> Process (Maybe (AsyncResult a)) +waitAnyTimeout delay asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) + -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. cancel :: AsyncChan a -> Process () diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 35950e9b..49a43110 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -103,7 +103,14 @@ testAsyncWaitAny result = do send (worker p2) "b" AsyncDone r3 <- waitAny [p1, p2, p3] stash result $ foldl (++) "" [r1, r2, r3] - + +testAsyncWaitAnyTimeout :: TestResult (Maybe (AsyncResult String)) -> Process () +testAsyncWaitAnyTimeout result = do + p1 <- asyncLinked $ expect >>= return + p2 <- asyncLinked $ expect >>= return + p3 <- asyncLinked $ expect >>= return + waitAnyTimeout (seconds 1) [p1, p2, p3] >>= stash result + tests :: LocalNode -> [Test] tests localNode = [ testGroup "Handling async results" [ @@ -131,6 +138,10 @@ tests localNode = [ (delayedAssertion "expected waitAny to mimic mergePortsBiased" localNode "cab" testAsyncWaitAny) + , testCase "testAsyncWaitAnyTimeout" + (delayedAssertion + "expected waitAnyTimeout to handle idle channels properly" + localNode Nothing testAsyncWaitAnyTimeout) ] ] From f667db79fe9b1d85fc3986d53d2f840ff9e09920 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:32 +0000 Subject: [PATCH 0568/2357] waitAnyTimeout and tests --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 0d5a300c..f29b7daa 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -47,6 +47,7 @@ module Control.Distributed.Platform.Async.AsyncChan , check , wait , waitAny + , waitAnyTimeout , waitTimeout , waitCheckTimeout ) where @@ -216,6 +217,14 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +waitAnyTimeout :: (Serializable a) + => TimeInterval + -> [AsyncChan a] + -> Process (Maybe (AsyncResult a)) +waitAnyTimeout delay asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) + -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. cancel :: AsyncChan a -> Process () From 3d0f1689533e6d94f0afa0cc63b4db3a96c9542a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:32 +0000 Subject: [PATCH 0569/2357] waitAnyTimeout and tests --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 0d5a300c..f29b7daa 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -47,6 +47,7 @@ module Control.Distributed.Platform.Async.AsyncChan , check , wait , waitAny + , waitAnyTimeout , waitTimeout , waitCheckTimeout ) where @@ -216,6 +217,14 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +waitAnyTimeout :: (Serializable a) + => TimeInterval + -> [AsyncChan a] + -> Process (Maybe (AsyncResult a)) +waitAnyTimeout delay asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) + -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. cancel :: AsyncChan a -> Process () From 592876b3022790b08246e502d20591765de3e153 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:14:32 +0000 Subject: [PATCH 0570/2357] waitAnyTimeout and tests --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 0d5a300c..f29b7daa 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -47,6 +47,7 @@ module Control.Distributed.Platform.Async.AsyncChan , check , wait , waitAny + , waitAnyTimeout , waitTimeout , waitCheckTimeout ) where @@ -216,6 +217,14 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +waitAnyTimeout :: (Serializable a) + => TimeInterval + -> [AsyncChan a] + -> Process (Maybe (AsyncResult a)) +waitAnyTimeout delay asyncs = + let ports = map (snd . channel) asyncs + in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) + -- | Cancel an asynchronous operation. To wait for cancellation to complete, use -- 'cancelWait' instead. cancel :: AsyncChan a -> Process () From a176e15dd00b597dbab34b449e23a0cbd0433c67 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:44:11 +0000 Subject: [PATCH 0571/2357] cancelWith = send worker a trap-able exit signal --- .../Distributed/Platform/Async/AsyncChan.hs | 27 +++++++++++++++++-- tests/TestAsync.hs | 13 ++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index f29b7daa..1eec79ff 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -41,6 +41,7 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel + , cancelWith , cancelWait -- functions to query an async-result , poll @@ -217,6 +218,7 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +-- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) => TimeInterval -> [AsyncChan a] @@ -225,11 +227,32 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. +-- | Cancel an asynchronous operation. Cancellation is achieved using message +-- passing, and is therefore asynchronous in nature. To wait for cancellation +-- to complete, use 'cancelWait' instead. The notes about the asynchronous +-- nature of 'cancelWait' apply here also. +-- +-- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait +-- | Cancels an asynchronous operation using the supplied exit reason. +-- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do +-- apply here, but more importantly this function sends an /exit signal/ to the +-- asynchronous worker, which leads to the following semantics: +-- +-- 1. if the worker already completed, this function does nothing +-- 2. the worker might complete after this call, but before the signal arrives +-- 3. the worker might ignore the exit signal using @catchExit@ +-- +-- In case of (3), this function will have no effect. You should use 'cancel' +-- if you need to guarantee that the asynchronous task is unable to ignore +-- the cancellation instruction. +-- +-- See 'Control.Distributed.Process.exit' +cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () +cancelWith reason = (flip exit) reason . worker + -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 49a43110..d6737da0 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -66,7 +66,7 @@ testAsyncWaitTimeout result = hAsync <- async $ sleep $ seconds 20 waitTimeout delay hAsync >>= stash result cancelWait hAsync >> return () - + testAsyncLinked :: TestResult Bool -> Process () testAsyncLinked result = do mv :: MVar (AsyncChan ()) <- liftIO $ newEmptyMVar @@ -111,6 +111,13 @@ testAsyncWaitAnyTimeout result = do p3 <- asyncLinked $ expect >>= return waitAnyTimeout (seconds 1) [p1, p2, p3] >>= stash result +testAsyncCancelWith :: TestResult Bool -> Process () +testAsyncCancelWith result = do + p1 <- async $ do { s :: String <- expect; return s } + cancelWith "foo" p1 + AsyncFailed (DiedException _) <- wait p1 + stash result True + tests :: LocalNode -> [Test] tests localNode = [ testGroup "Handling async results" [ @@ -142,6 +149,10 @@ tests localNode = [ (delayedAssertion "expected waitAnyTimeout to handle idle channels properly" localNode Nothing testAsyncWaitAnyTimeout) + , testCase "testAsyncCancelWith" + (delayedAssertion + "expected the worker to have been killed with the given signal" + localNode True testAsyncCancelWith) ] ] From d360ac09c8e72e36088f885d3591d6f9bb52132f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:44:11 +0000 Subject: [PATCH 0572/2357] cancelWith = send worker a trap-able exit signal --- .../Distributed/Platform/Async/AsyncChan.hs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index f29b7daa..1eec79ff 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -41,6 +41,7 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel + , cancelWith , cancelWait -- functions to query an async-result , poll @@ -217,6 +218,7 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +-- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) => TimeInterval -> [AsyncChan a] @@ -225,11 +227,32 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. +-- | Cancel an asynchronous operation. Cancellation is achieved using message +-- passing, and is therefore asynchronous in nature. To wait for cancellation +-- to complete, use 'cancelWait' instead. The notes about the asynchronous +-- nature of 'cancelWait' apply here also. +-- +-- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait +-- | Cancels an asynchronous operation using the supplied exit reason. +-- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do +-- apply here, but more importantly this function sends an /exit signal/ to the +-- asynchronous worker, which leads to the following semantics: +-- +-- 1. if the worker already completed, this function does nothing +-- 2. the worker might complete after this call, but before the signal arrives +-- 3. the worker might ignore the exit signal using @catchExit@ +-- +-- In case of (3), this function will have no effect. You should use 'cancel' +-- if you need to guarantee that the asynchronous task is unable to ignore +-- the cancellation instruction. +-- +-- See 'Control.Distributed.Process.exit' +cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () +cancelWith reason = (flip exit) reason . worker + -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ From dfc9d1ba51b44cf3422625d90bc8ee2f8ab236ee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:44:11 +0000 Subject: [PATCH 0573/2357] cancelWith = send worker a trap-able exit signal --- .../Distributed/Platform/Async/AsyncChan.hs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index f29b7daa..1eec79ff 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -41,6 +41,7 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel + , cancelWith , cancelWait -- functions to query an async-result , poll @@ -217,6 +218,7 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +-- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) => TimeInterval -> [AsyncChan a] @@ -225,11 +227,32 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. +-- | Cancel an asynchronous operation. Cancellation is achieved using message +-- passing, and is therefore asynchronous in nature. To wait for cancellation +-- to complete, use 'cancelWait' instead. The notes about the asynchronous +-- nature of 'cancelWait' apply here also. +-- +-- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait +-- | Cancels an asynchronous operation using the supplied exit reason. +-- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do +-- apply here, but more importantly this function sends an /exit signal/ to the +-- asynchronous worker, which leads to the following semantics: +-- +-- 1. if the worker already completed, this function does nothing +-- 2. the worker might complete after this call, but before the signal arrives +-- 3. the worker might ignore the exit signal using @catchExit@ +-- +-- In case of (3), this function will have no effect. You should use 'cancel' +-- if you need to guarantee that the asynchronous task is unable to ignore +-- the cancellation instruction. +-- +-- See 'Control.Distributed.Process.exit' +cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () +cancelWith reason = (flip exit) reason . worker + -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ From 1f22b3617c6e95c9af28e2777c5ad35bd94a284d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:44:11 +0000 Subject: [PATCH 0574/2357] cancelWith = send worker a trap-able exit signal --- .../Distributed/Platform/Async/AsyncChan.hs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index f29b7daa..1eec79ff 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -41,6 +41,7 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel + , cancelWith , cancelWait -- functions to query an async-result , poll @@ -217,6 +218,7 @@ waitAny asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChan +-- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) => TimeInterval -> [AsyncChan a] @@ -225,11 +227,32 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. To wait for cancellation to complete, use --- 'cancelWait' instead. +-- | Cancel an asynchronous operation. Cancellation is achieved using message +-- passing, and is therefore asynchronous in nature. To wait for cancellation +-- to complete, use 'cancelWait' instead. The notes about the asynchronous +-- nature of 'cancelWait' apply here also. +-- +-- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait +-- | Cancels an asynchronous operation using the supplied exit reason. +-- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do +-- apply here, but more importantly this function sends an /exit signal/ to the +-- asynchronous worker, which leads to the following semantics: +-- +-- 1. if the worker already completed, this function does nothing +-- 2. the worker might complete after this call, but before the signal arrives +-- 3. the worker might ignore the exit signal using @catchExit@ +-- +-- In case of (3), this function will have no effect. You should use 'cancel' +-- if you need to guarantee that the asynchronous task is unable to ignore +-- the cancellation instruction. +-- +-- See 'Control.Distributed.Process.exit' +cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () +cancelWith reason = (flip exit) reason . worker + -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to -- cancel will race with the asynchronous worker, so it is /entirely possible/ From 9c3f90a88ae935483b53e4ff0a3935e6600cc23e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:46:12 +0000 Subject: [PATCH 0575/2357] complete renaming to AsyncChan --- distributed-process-platform.cabal | 2 +- tests/TestAsync.hs | 166 ----------------------------- 2 files changed, 1 insertion(+), 167 deletions(-) delete mode 100644 tests/TestAsync.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 50c8a1a3..43b2dc56 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -71,7 +71,7 @@ test-suite AsyncTests TestUtils, Control.Distributed.Platform.Async extensions: CPP - main-is: TestAsync.hs + main-is: TestAsyncChan.hs test-suite TimerTests type: exitcode-stdio-1.0 diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs deleted file mode 100644 index d6737da0..00000000 --- a/tests/TestAsync.hs +++ /dev/null @@ -1,166 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE ScopedTypeVariables #-} - -module Main where - -import Control.Concurrent.MVar - ( newEmptyMVar - , takeMVar - , MVar) -import Control.Distributed.Process -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() -import Control.Distributed.Platform -import Control.Distributed.Platform.Async.AsyncChan -import Data.Binary() -import Data.Typeable() -import qualified Network.Transport as NT (Transport) -import Prelude hiding (catch) - -import Test.Framework (Test, testGroup) -import Test.Framework.Providers.HUnit (testCase) -import TestUtils - -testAsyncPoll :: TestResult (AsyncResult ()) -> Process () -testAsyncPoll result = do - hAsync <- async $ do "go" <- expect; say "running" >> return () - ar <- poll hAsync - case ar of - AsyncPending -> - send (worker hAsync) "go" >> wait hAsync >>= stash result - _ -> stash result ar >> return () - -testAsyncCancel :: TestResult (AsyncResult ()) -> Process () -testAsyncCancel result = do - hAsync <- async $ runTestProcess $ say "running" >> return () - sleep $ milliseconds 100 - - p <- poll hAsync -- nasty kind of assertion: use assertEquals? - case p of - AsyncPending -> cancel hAsync >> wait hAsync >>= stash result - _ -> say (show p) >> stash result p - -testAsyncCancelWait :: TestResult (Maybe (AsyncResult ())) -> Process () -testAsyncCancelWait result = do - testPid <- getSelfPid - p <- spawnLocal $ do - hAsync <- async $ runTestProcess $ say "running" >> (sleep $ seconds 60) - sleep $ milliseconds 100 - - send testPid "running" - - AsyncPending <- poll hAsync - cancelWait hAsync >>= send testPid - - "running" <- expect - d <- expectTimeout (intervalToMs $ seconds 5) - case d of - Nothing -> kill p "timed out" >> stash result Nothing - Just ar -> stash result (Just ar) - -testAsyncWaitTimeout :: TestResult (Maybe (AsyncResult ())) -> Process () -testAsyncWaitTimeout result = - let delay = seconds 1 - in do - hAsync <- async $ sleep $ seconds 20 - waitTimeout delay hAsync >>= stash result - cancelWait hAsync >> return () - -testAsyncLinked :: TestResult Bool -> Process () -testAsyncLinked result = do - mv :: MVar (AsyncChan ()) <- liftIO $ newEmptyMVar - pid <- spawnLocal $ do - h <- asyncLinked $ do - "waiting" <- expect - return () - stash mv h - "sleeping" <- expect - return () - - hAsync <- liftIO $ takeMVar mv - - mref <- monitor $ worker hAsync - exit pid "stop" - - ProcessMonitorNotification mref' _ _ <- expect - - -- since the initial caller died and we used 'asyncLinked', the async should - -- pick up on the exit signal and set the result accordingly, however the - -- ReceivePort is no longer valid, so we can't wait on it! We have to ensure - -- that the worker is really dead then.... - stash result $ mref == mref' - -testAsyncWaitAny :: TestResult String -> Process () -testAsyncWaitAny result = do - p1 <- async $ expect >>= return - p2 <- async $ expect >>= return - p3 <- async $ expect >>= return - send (worker p3) "c" - AsyncDone r1 <- waitAny [p1, p2, p3] - send (worker p1) "a" - AsyncDone r2 <- waitAny [p1, p2, p3] - send (worker p2) "b" - AsyncDone r3 <- waitAny [p1, p2, p3] - stash result $ foldl (++) "" [r1, r2, r3] - -testAsyncWaitAnyTimeout :: TestResult (Maybe (AsyncResult String)) -> Process () -testAsyncWaitAnyTimeout result = do - p1 <- asyncLinked $ expect >>= return - p2 <- asyncLinked $ expect >>= return - p3 <- asyncLinked $ expect >>= return - waitAnyTimeout (seconds 1) [p1, p2, p3] >>= stash result - -testAsyncCancelWith :: TestResult Bool -> Process () -testAsyncCancelWith result = do - p1 <- async $ do { s :: String <- expect; return s } - cancelWith "foo" p1 - AsyncFailed (DiedException _) <- wait p1 - stash result True - -tests :: LocalNode -> [Test] -tests localNode = [ - testGroup "Handling async results" [ - testCase "testAsyncCancel" - (delayedAssertion - "expected async task to have been cancelled" - localNode (AsyncCancelled) testAsyncCancel) - , testCase "testAsyncPoll" - (delayedAssertion - "expected poll to return a valid AsyncResult" - localNode (AsyncDone ()) testAsyncPoll) - , testCase "testAsyncCancelWait" - (delayedAssertion - "expected cancelWait to complete some time" - localNode (Just AsyncCancelled) testAsyncCancelWait) - , testCase "testAsyncWaitTimeout" - (delayedAssertion - "expected waitTimeout to return Nothing when it times out" - localNode (Nothing) testAsyncWaitTimeout) - , testCase "testAsyncLinked" - (delayedAssertion - "expected linked process to die with originator" - localNode True testAsyncLinked) - , testCase "testAsyncWaitAny" - (delayedAssertion - "expected waitAny to mimic mergePortsBiased" - localNode "cab" testAsyncWaitAny) - , testCase "testAsyncWaitAnyTimeout" - (delayedAssertion - "expected waitAnyTimeout to handle idle channels properly" - localNode Nothing testAsyncWaitAnyTimeout) - , testCase "testAsyncCancelWith" - (delayedAssertion - "expected the worker to have been killed with the given signal" - localNode True testAsyncCancelWith) - ] - ] - -asyncTests :: NT.Transport -> IO [Test] -asyncTests transport = do - localNode <- newLocalNode transport initRemoteTable - let testData = tests localNode - return testData - -main :: IO () -main = testMain $ asyncTests From bba1a502d7ba3814ca238ab404a5e4a61b334f78 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:46:12 +0000 Subject: [PATCH 0576/2357] complete renaming to AsyncChan --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 50c8a1a3..43b2dc56 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -71,7 +71,7 @@ test-suite AsyncTests TestUtils, Control.Distributed.Platform.Async extensions: CPP - main-is: TestAsync.hs + main-is: TestAsyncChan.hs test-suite TimerTests type: exitcode-stdio-1.0 From 8788c1caed0352c654c992a3153cff91bf6239ee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:46:12 +0000 Subject: [PATCH 0577/2357] complete renaming to AsyncChan --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 50c8a1a3..43b2dc56 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -71,7 +71,7 @@ test-suite AsyncTests TestUtils, Control.Distributed.Platform.Async extensions: CPP - main-is: TestAsync.hs + main-is: TestAsyncChan.hs test-suite TimerTests type: exitcode-stdio-1.0 From 3674502e4608a24cfdc8ed85c1beec5c4be12a76 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 23 Dec 2012 02:46:12 +0000 Subject: [PATCH 0578/2357] complete renaming to AsyncChan --- distributed-process-platform.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 50c8a1a3..43b2dc56 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -71,7 +71,7 @@ test-suite AsyncTests TestUtils, Control.Distributed.Platform.Async extensions: CPP - main-is: TestAsync.hs + main-is: TestAsyncChan.hs test-suite TimerTests type: exitcode-stdio-1.0 From 2d78267ee3c8f1b6559dd59a5d3541be6b30edac Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 28 Dec 2012 14:48:41 +0000 Subject: [PATCH 0579/2357] break up the test fixtures for async implementations --- distributed-process-platform.cabal | 16 +++- .../Distributed/Platform/Async/AsyncChan.hs | 8 +- .../Distributed/Platform/Async/AsyncSTM.hs | 91 +++++++++++++++++++ tests/TestAsync.hs | 19 ++++ 4 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncSTM.hs create mode 100644 tests/TestAsync.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 43b2dc56..d1769e7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,8 @@ library Control.Distributed.Platform.Async.AsyncChan other-modules: Control.Distributed.Platform.Internal.Primitives, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM test-suite AsyncTests @@ -57,6 +58,7 @@ test-suite AsyncTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers @@ -69,9 +71,12 @@ test-suite AsyncTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP - main-is: TestAsyncChan.hs + main-is: TestAsync.hs test-suite TimerTests type: exitcode-stdio-1.0 @@ -99,6 +104,9 @@ test-suite TimerTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1eec79ff..ff882cc4 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -4,7 +4,7 @@ ----------------------------------------------------------------------------- -- | --- Module : Control.Distributed.Platform.Async +-- Module : Control.Distributed.Platform.Async.AsyncChan -- Copyright : (c) Tim Watson 2012 -- License : BSD3 (see the file LICENSE) -- @@ -19,12 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs new file mode 100644 index 00000000..47651f75 --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async.AsyncSTM +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncSTM where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) +import Control.Concurrent.STM +import GHC.Conc + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncSTM a = AsyncSTM { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , hWait :: STM a + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +asyncLinked = asyncDo False + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo shouldLink task = do + (wpid, gpid, hRes) <- spawnWorkers task shouldLink + return AsyncSTM { + worker = wpid + , insulator = gpid + , hWait = hRes + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, STM a) +spawnWorkers task shouldLink = undefined diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs new file mode 100644 index 00000000..a119b53e --- /dev/null +++ b/tests/TestAsync.hs @@ -0,0 +1,19 @@ +module Main where + +import Test.Framework (Test, defaultMain, testGroup) +import qualified Network.Transport as NT +import Network.Transport.TCP +import TestAsyncChan +import TestAsyncSTM +import TestUtils + +allAsyncTests :: NT.Transport -> IO [Test] +allAsyncTests transport = do + chanTestGroup <- asyncChanTests transport + stmTestGroup <- asyncStmTests transport + return [ + testGroup "AsyncChan" chanTestGroup + , testGroup "AsyncSTM" stmTestGroup ] + +main :: IO () +main = testMain $ allAsyncTests From 0c2af32115669ba142419011d5afcbafdc01228d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 28 Dec 2012 14:48:41 +0000 Subject: [PATCH 0580/2357] break up the test fixtures for async implementations --- distributed-process-platform.cabal | 16 +++- .../Distributed/Platform/Async/AsyncChan.hs | 8 +- .../Distributed/Platform/Async/AsyncSTM.hs | 91 +++++++++++++++++++ 3 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncSTM.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 43b2dc56..d1769e7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,8 @@ library Control.Distributed.Platform.Async.AsyncChan other-modules: Control.Distributed.Platform.Internal.Primitives, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM test-suite AsyncTests @@ -57,6 +58,7 @@ test-suite AsyncTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers @@ -69,9 +71,12 @@ test-suite AsyncTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP - main-is: TestAsyncChan.hs + main-is: TestAsync.hs test-suite TimerTests type: exitcode-stdio-1.0 @@ -99,6 +104,9 @@ test-suite TimerTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1eec79ff..ff882cc4 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -4,7 +4,7 @@ ----------------------------------------------------------------------------- -- | --- Module : Control.Distributed.Platform.Async +-- Module : Control.Distributed.Platform.Async.AsyncChan -- Copyright : (c) Tim Watson 2012 -- License : BSD3 (see the file LICENSE) -- @@ -19,12 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs new file mode 100644 index 00000000..47651f75 --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async.AsyncSTM +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncSTM where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) +import Control.Concurrent.STM +import GHC.Conc + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncSTM a = AsyncSTM { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , hWait :: STM a + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +asyncLinked = asyncDo False + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo shouldLink task = do + (wpid, gpid, hRes) <- spawnWorkers task shouldLink + return AsyncSTM { + worker = wpid + , insulator = gpid + , hWait = hRes + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, STM a) +spawnWorkers task shouldLink = undefined From d41f30f2fe03fcb6a0a07010cc0a493df28df0ca Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 28 Dec 2012 14:48:41 +0000 Subject: [PATCH 0581/2357] break up the test fixtures for async implementations --- distributed-process-platform.cabal | 16 +++- .../Distributed/Platform/Async/AsyncChan.hs | 8 +- .../Distributed/Platform/Async/AsyncSTM.hs | 91 +++++++++++++++++++ 3 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncSTM.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 43b2dc56..d1769e7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,8 @@ library Control.Distributed.Platform.Async.AsyncChan other-modules: Control.Distributed.Platform.Internal.Primitives, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM test-suite AsyncTests @@ -57,6 +58,7 @@ test-suite AsyncTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers @@ -69,9 +71,12 @@ test-suite AsyncTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP - main-is: TestAsyncChan.hs + main-is: TestAsync.hs test-suite TimerTests type: exitcode-stdio-1.0 @@ -99,6 +104,9 @@ test-suite TimerTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1eec79ff..ff882cc4 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -4,7 +4,7 @@ ----------------------------------------------------------------------------- -- | --- Module : Control.Distributed.Platform.Async +-- Module : Control.Distributed.Platform.Async.AsyncChan -- Copyright : (c) Tim Watson 2012 -- License : BSD3 (see the file LICENSE) -- @@ -19,12 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs new file mode 100644 index 00000000..47651f75 --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async.AsyncSTM +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncSTM where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) +import Control.Concurrent.STM +import GHC.Conc + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncSTM a = AsyncSTM { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , hWait :: STM a + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +asyncLinked = asyncDo False + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo shouldLink task = do + (wpid, gpid, hRes) <- spawnWorkers task shouldLink + return AsyncSTM { + worker = wpid + , insulator = gpid + , hWait = hRes + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, STM a) +spawnWorkers task shouldLink = undefined From 05594f0bef7e79c261dfe47a00f398f683146a8c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 28 Dec 2012 14:48:41 +0000 Subject: [PATCH 0582/2357] break up the test fixtures for async implementations --- distributed-process-platform.cabal | 16 +++- .../Distributed/Platform/Async/AsyncChan.hs | 8 +- .../Distributed/Platform/Async/AsyncSTM.hs | 91 +++++++++++++++++++ 3 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/Control/Distributed/Platform/Async/AsyncSTM.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 43b2dc56..d1769e7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -40,7 +40,8 @@ library Control.Distributed.Platform.Async.AsyncChan other-modules: Control.Distributed.Platform.Internal.Primitives, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM test-suite AsyncTests @@ -57,6 +58,7 @@ test-suite AsyncTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.7, test-framework-hunit, transformers @@ -69,9 +71,12 @@ test-suite AsyncTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP - main-is: TestAsyncChan.hs + main-is: TestAsync.hs test-suite TimerTests type: exitcode-stdio-1.0 @@ -99,6 +104,9 @@ test-suite TimerTests Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives TestUtils, - Control.Distributed.Platform.Async + Control.Distributed.Platform.Async, + Control.Distributed.Platform.Async.AsyncSTM, + TestAsyncSTM, + TestAsync extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1eec79ff..ff882cc4 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -4,7 +4,7 @@ ----------------------------------------------------------------------------- -- | --- Module : Control.Distributed.Platform.Async +-- Module : Control.Distributed.Platform.Async.AsyncChan -- Copyright : (c) Tim Watson 2012 -- License : BSD3 (see the file LICENSE) -- @@ -19,12 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- The basic type is @'Async' a@, which represents an asynchronous --- @Process@ that will return a value of type @a@, or exit with a failure --- reason. An @Async@ corresponds logically to a worker @Process@, and its --- 'ProcessId' can be obtained with 'worker', although that should rarely --- be necessary. --- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs new file mode 100644 index 00000000..47651f75 --- /dev/null +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Platform.Async.AsyncSTM +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of operations for spawning Process operations +-- and waiting for their results. It is a thin layer over the basic +-- concurrency operations provided by "Control.Distributed.Process". +-- The main feature it provides is a pre-canned set of APIs for waiting on the +-- result of one or more asynchronously running (and potentially distributed) +-- processes. +-- +----------------------------------------------------------------------------- + +module Control.Distributed.Platform.Async.AsyncSTM where + +import Control.Distributed.Platform.Async +import Control.Distributed.Platform.Timer + ( intervalToMs + ) +import Control.Distributed.Platform.Internal.Types + ( CancelWait(..) + , TimeInterval() + ) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable + +import Data.Maybe + ( fromMaybe + ) +import Control.Concurrent.STM +import GHC.Conc + +-------------------------------------------------------------------------------- +-- Cloud Haskell Async Process API -- +-------------------------------------------------------------------------------- + +-- | An handle for an asynchronous action spawned by 'async'. +-- Asynchronous operations are run in a separate process, and +-- operations are provided for waiting for asynchronous actions to +-- complete and obtaining their results (see e.g. 'wait'). +-- +-- Handles of this type cannot cross remote boundaries. +data AsyncSTM a = AsyncSTM { + worker :: AsyncWorkerId + , insulator :: AsyncGathererId + , hWait :: STM a + } + +-- | Spawns an asynchronous action in a new process. +-- +-- There is currently a contract for async workers which is that they should +-- exit normally (i.e., they should not call the @exit selfPid reason@ nor +-- @terminate@ primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +async = asyncDo True + +-- | This is a useful variant of 'async' that ensures an @AsyncChan@ is +-- never left running unintentionally. We ensure that if the caller's process +-- exits, that the worker is killed. Because an @AsyncChan@ can only be used +-- by the initial caller's process, if that process dies then the result +-- (if any) is discarded. +-- +asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) +asyncLinked = asyncDo False + +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo shouldLink task = do + (wpid, gpid, hRes) <- spawnWorkers task shouldLink + return AsyncSTM { + worker = wpid + , insulator = gpid + , hWait = hRes + } + +spawnWorkers :: (Serializable a) + => AsyncTask a + -> Bool + -> Process (AsyncRef, AsyncRef, STM a) +spawnWorkers task shouldLink = undefined From 69381d398230ff2f07f4734bbf9fc52d7c809f02 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 01:47:10 +0000 Subject: [PATCH 0583/2357] make sure the worker doesn't race with the insulator's monitor call; clarify a bit --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index ff882cc4..1dcda7bc 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -121,6 +121,7 @@ spawnWorkers task shouldLink = do -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do + () <- expect r <- task sendChan (fst chan) (AsyncDone r) @@ -135,6 +136,7 @@ spawnWorkers task shouldLink = do return (maybe (return ()) unmonitor rref)) workerPid <- expect + send workerPid () return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message @@ -209,8 +211,9 @@ waitAny :: (Serializable a) => [AsyncChan a] -> Process (AsyncResult a) waitAny asyncs = - let ports = map (snd . channel) asyncs - in mergePortsBiased ports >>= receiveChan + let ports = map (snd . channel) asyncs in recv ports + where recv :: (Serializable a) => [ReceivePort a] -> Process a + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) From 45d2eba2e23f58a0e789d0a104df75fec74a3c12 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 01:47:10 +0000 Subject: [PATCH 0584/2357] make sure the worker doesn't race with the insulator's monitor call; clarify a bit --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index ff882cc4..1dcda7bc 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -121,6 +121,7 @@ spawnWorkers task shouldLink = do -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do + () <- expect r <- task sendChan (fst chan) (AsyncDone r) @@ -135,6 +136,7 @@ spawnWorkers task shouldLink = do return (maybe (return ()) unmonitor rref)) workerPid <- expect + send workerPid () return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message @@ -209,8 +211,9 @@ waitAny :: (Serializable a) => [AsyncChan a] -> Process (AsyncResult a) waitAny asyncs = - let ports = map (snd . channel) asyncs - in mergePortsBiased ports >>= receiveChan + let ports = map (snd . channel) asyncs in recv ports + where recv :: (Serializable a) => [ReceivePort a] -> Process a + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) From 2d3264f248eeccc21b6bb890267d4b08defd9ac3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 01:47:10 +0000 Subject: [PATCH 0585/2357] make sure the worker doesn't race with the insulator's monitor call; clarify a bit --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index ff882cc4..1dcda7bc 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -121,6 +121,7 @@ spawnWorkers task shouldLink = do -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do + () <- expect r <- task sendChan (fst chan) (AsyncDone r) @@ -135,6 +136,7 @@ spawnWorkers task shouldLink = do return (maybe (return ()) unmonitor rref)) workerPid <- expect + send workerPid () return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message @@ -209,8 +211,9 @@ waitAny :: (Serializable a) => [AsyncChan a] -> Process (AsyncResult a) waitAny asyncs = - let ports = map (snd . channel) asyncs - in mergePortsBiased ports >>= receiveChan + let ports = map (snd . channel) asyncs in recv ports + where recv :: (Serializable a) => [ReceivePort a] -> Process a + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) From 670cdd473fc6d55d95f7ac8a9710c203421748cd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 01:47:10 +0000 Subject: [PATCH 0586/2357] make sure the worker doesn't race with the insulator's monitor call; clarify a bit --- src/Control/Distributed/Platform/Async/AsyncChan.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index ff882cc4..1dcda7bc 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -121,6 +121,7 @@ spawnWorkers task shouldLink = do -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do + () <- expect r <- task sendChan (fst chan) (AsyncDone r) @@ -135,6 +136,7 @@ spawnWorkers task shouldLink = do return (maybe (return ()) unmonitor rref)) workerPid <- expect + send workerPid () return (workerPid, insulatorPid, chan) where -- blocking receive until we see an input message @@ -209,8 +211,9 @@ waitAny :: (Serializable a) => [AsyncChan a] -> Process (AsyncResult a) waitAny asyncs = - let ports = map (snd . channel) asyncs - in mergePortsBiased ports >>= receiveChan + let ports = map (snd . channel) asyncs in recv ports + where recv :: (Serializable a) => [ReceivePort a] -> Process a + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) From 6ffba7b59c34738f5d0880ed68b527d7e65601f8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 02:24:05 +0000 Subject: [PATCH 0587/2357] strip unused types, add documentation --- src/Control/Distributed/Platform/Async.hs | 32 +---- .../Distributed/Platform/Async/AsyncChan.hs | 111 +++++++++++------- .../Distributed/Platform/Async/AsyncSTM.hs | 4 +- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 279a982d..f00ce481 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,22 +12,18 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- The modules herein provides a set of operations for spawning Processes --- and waiting for theie results. It is a thin layer over the basic --- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. --- +-- The modules in the @Async@ package provide operations for spawning Processes, +-- waiting for their results, cancelling them and various other utilities. The +-- two primary implementation are @AsyncChan@ which provides an API which is +-- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that +-- can be used by (as in shared across) multiple processes on a local node. +-- Both abstractions can run asynchronous operations on remote node. ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncResult(..) ) where @@ -44,12 +40,6 @@ import Data.Typeable (Typeable) -- | A reference to an asynchronous action type AsyncRef = ProcessId --- | A reference to an asynchronous worker -type AsyncWorkerId = AsyncRef - --- | A reference to an asynchronous "gatherer" -type AsyncGathererId = AsyncRef - -- | A task to be performed asynchronously. This can either take the -- form of an action that runs over some type @a@ in the @Process@ monad, -- or a tuple that adds the node on which the asynchronous task should be @@ -69,13 +59,3 @@ $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) deriving instance Show a => Show (AsyncResult a) - --- | An async cancellation takes an 'AsyncRef' and does some cancellation --- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] - --- note [local cancel only] --- The cancellation is only ever sent to the insulator process, which is always --- run on the local node. That could be a limitation, as there's nothing in --- 'Async' data profile to stop it being sent remotely. At *that* point, we'd --- need to make the cancellation remote-able too however. diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1dcda7bc..7bc1403b 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,15 +19,14 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- +-- +-- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning @@ -35,8 +34,9 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel - , cancelWith , cancelWait + , cancelWith + , cancelKill -- functions to query an async-result , poll , check @@ -62,40 +62,54 @@ import Data.Maybe ( fromMaybe ) --------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- --------------------------------------------------------------------------------- - -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and +-- | A handle for an asynchronous action spawned by 'async'. +-- Asynchronous actions are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries. Furthermore, handles +-- of this type /must not/ be passed to functions in this module by processes +-- other than the caller of 'async' - that is, this module provides asynchronous +-- actions whose results are accessible *only* by the initiating process. +-- +-- See 'async' data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , channel :: (InternalChannel a) } -- | Spawns an asynchronous action in a new process. +-- We ensure that if the caller's process exits, that the worker is killed. +-- Because an @AsyncChan@ can only be used by the initial caller's process, if +-- that process dies then the result (if any) is discarded. If a process other +-- than the initial caller attempts to obtain the result of an asynchronous +-- action, the behaviour is undefined. It is /highly likely/ that such a +-- process will block indefinitely, quite possible that such behaviour could lead +-- to deadlock and almost certain that resource starvation will occur. /Do Not/ +-- share the handles returned by this function across multiple processes. -- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. +-- If you need to spawn an asynchronous operation whose handle can be shared by +-- multiple processes then use the 'AsyncSTM' module instead. +-- +-- There is currently a contract for async workers, that they should +-- exit normally (i.e., they should not call the @exit@ or @kill@ with their own +-- 'ProcessId' nor use the @terminate@ primitive to cease functining), otherwise +-- the 'AsyncResult' will end up being @AsyncFailed DiedException@ instead of +-- containing the desired result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) async = asyncDo True --- | This is a useful variant of 'async' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. +-- | For *AsyncChan*, 'async' already ensures an @AsyncChan@ is +-- never left running unintentionally. This function is provided for compatibility +-- with other /async/ implementations that may offer different semantics for +-- @async@ with regards linking. +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async @@ -112,8 +126,7 @@ asyncDo shouldLink task = do spawnWorkers :: (Serializable a) => AsyncTask a -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) + -> Process (AsyncRef, AsyncRef, InternalChannel a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -224,36 +237,52 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. Cancellation is achieved using message --- passing, and is therefore asynchronous in nature. To wait for cancellation --- to complete, use 'cancelWait' instead. The notes about the asynchronous --- nature of 'cancelWait' apply here also. +-- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. +-- To wait for cancellation to complete, use 'cancelWait' instead. The notes +-- about the asynchronous nature of 'cancelWait' apply here also. -- -- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait --- | Cancels an asynchronous operation using the supplied exit reason. --- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do --- apply here, but more importantly this function sends an /exit signal/ to the --- asynchronous worker, which leads to the following semantics: +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For +-- example, the worker may complete its task after this function is called, but +-- before the cancellation instruction is acted upon. +-- +-- If you wish to stop an asychronous operation /immediately/ (with caveats) then +-- consider using 'cancelWith' or 'cancelKill' instead. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync + +-- | Cancel an asynchronous operation immediately. +-- This operation is performed by sending an /exit signal/ to the asynchronous +-- worker, which leads to the following semantics: -- --- 1. if the worker already completed, this function does nothing +-- 1. if the worker already completed, this function has no effect -- 2. the worker might complete after this call, but before the signal arrives -- 3. the worker might ignore the exit signal using @catchExit@ -- --- In case of (3), this function will have no effect. You should use 'cancel' +-- In case of (3), this function has no effect. You should use 'cancel' -- if you need to guarantee that the asynchronous task is unable to ignore -- the cancellation instruction. -- +-- You should also consider that when sending exit signals to a process, the +-- definition of 'immediately' is somewhat vague and a scheduler might take +-- time to handle the request, which can lead to situations similar to (1) as +-- listed above, if the scheduler to which the calling process' thread is bound +-- decides to GC whilst another scheduler on which the worker is running is able +-- to continue. +-- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () cancelWith reason = (flip exit) reason . worker --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. +-- +-- See 'Control.Distributed.Process.kill' +cancelKill :: String -> AsyncChan a -> Process () +cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 47651f75..23c108ce 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -51,8 +51,8 @@ import GHC.Conc -- -- Handles of this type cannot cross remote boundaries. data AsyncSTM a = AsyncSTM { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , hWait :: STM a } From 6758842cda92dd92cf391ed2f188aebc8932f65e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 02:24:05 +0000 Subject: [PATCH 0588/2357] strip unused types, add documentation --- src/Control/Distributed/Platform/Async.hs | 32 +---- .../Distributed/Platform/Async/AsyncChan.hs | 111 +++++++++++------- .../Distributed/Platform/Async/AsyncSTM.hs | 4 +- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 279a982d..f00ce481 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,22 +12,18 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- The modules herein provides a set of operations for spawning Processes --- and waiting for theie results. It is a thin layer over the basic --- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. --- +-- The modules in the @Async@ package provide operations for spawning Processes, +-- waiting for their results, cancelling them and various other utilities. The +-- two primary implementation are @AsyncChan@ which provides an API which is +-- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that +-- can be used by (as in shared across) multiple processes on a local node. +-- Both abstractions can run asynchronous operations on remote node. ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncResult(..) ) where @@ -44,12 +40,6 @@ import Data.Typeable (Typeable) -- | A reference to an asynchronous action type AsyncRef = ProcessId --- | A reference to an asynchronous worker -type AsyncWorkerId = AsyncRef - --- | A reference to an asynchronous "gatherer" -type AsyncGathererId = AsyncRef - -- | A task to be performed asynchronously. This can either take the -- form of an action that runs over some type @a@ in the @Process@ monad, -- or a tuple that adds the node on which the asynchronous task should be @@ -69,13 +59,3 @@ $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) deriving instance Show a => Show (AsyncResult a) - --- | An async cancellation takes an 'AsyncRef' and does some cancellation --- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] - --- note [local cancel only] --- The cancellation is only ever sent to the insulator process, which is always --- run on the local node. That could be a limitation, as there's nothing in --- 'Async' data profile to stop it being sent remotely. At *that* point, we'd --- need to make the cancellation remote-able too however. diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1dcda7bc..7bc1403b 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,15 +19,14 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- +-- +-- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning @@ -35,8 +34,9 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel - , cancelWith , cancelWait + , cancelWith + , cancelKill -- functions to query an async-result , poll , check @@ -62,40 +62,54 @@ import Data.Maybe ( fromMaybe ) --------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- --------------------------------------------------------------------------------- - -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and +-- | A handle for an asynchronous action spawned by 'async'. +-- Asynchronous actions are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries. Furthermore, handles +-- of this type /must not/ be passed to functions in this module by processes +-- other than the caller of 'async' - that is, this module provides asynchronous +-- actions whose results are accessible *only* by the initiating process. +-- +-- See 'async' data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , channel :: (InternalChannel a) } -- | Spawns an asynchronous action in a new process. +-- We ensure that if the caller's process exits, that the worker is killed. +-- Because an @AsyncChan@ can only be used by the initial caller's process, if +-- that process dies then the result (if any) is discarded. If a process other +-- than the initial caller attempts to obtain the result of an asynchronous +-- action, the behaviour is undefined. It is /highly likely/ that such a +-- process will block indefinitely, quite possible that such behaviour could lead +-- to deadlock and almost certain that resource starvation will occur. /Do Not/ +-- share the handles returned by this function across multiple processes. -- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. +-- If you need to spawn an asynchronous operation whose handle can be shared by +-- multiple processes then use the 'AsyncSTM' module instead. +-- +-- There is currently a contract for async workers, that they should +-- exit normally (i.e., they should not call the @exit@ or @kill@ with their own +-- 'ProcessId' nor use the @terminate@ primitive to cease functining), otherwise +-- the 'AsyncResult' will end up being @AsyncFailed DiedException@ instead of +-- containing the desired result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) async = asyncDo True --- | This is a useful variant of 'async' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. +-- | For *AsyncChan*, 'async' already ensures an @AsyncChan@ is +-- never left running unintentionally. This function is provided for compatibility +-- with other /async/ implementations that may offer different semantics for +-- @async@ with regards linking. +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async @@ -112,8 +126,7 @@ asyncDo shouldLink task = do spawnWorkers :: (Serializable a) => AsyncTask a -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) + -> Process (AsyncRef, AsyncRef, InternalChannel a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -224,36 +237,52 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. Cancellation is achieved using message --- passing, and is therefore asynchronous in nature. To wait for cancellation --- to complete, use 'cancelWait' instead. The notes about the asynchronous --- nature of 'cancelWait' apply here also. +-- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. +-- To wait for cancellation to complete, use 'cancelWait' instead. The notes +-- about the asynchronous nature of 'cancelWait' apply here also. -- -- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait --- | Cancels an asynchronous operation using the supplied exit reason. --- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do --- apply here, but more importantly this function sends an /exit signal/ to the --- asynchronous worker, which leads to the following semantics: +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For +-- example, the worker may complete its task after this function is called, but +-- before the cancellation instruction is acted upon. +-- +-- If you wish to stop an asychronous operation /immediately/ (with caveats) then +-- consider using 'cancelWith' or 'cancelKill' instead. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync + +-- | Cancel an asynchronous operation immediately. +-- This operation is performed by sending an /exit signal/ to the asynchronous +-- worker, which leads to the following semantics: -- --- 1. if the worker already completed, this function does nothing +-- 1. if the worker already completed, this function has no effect -- 2. the worker might complete after this call, but before the signal arrives -- 3. the worker might ignore the exit signal using @catchExit@ -- --- In case of (3), this function will have no effect. You should use 'cancel' +-- In case of (3), this function has no effect. You should use 'cancel' -- if you need to guarantee that the asynchronous task is unable to ignore -- the cancellation instruction. -- +-- You should also consider that when sending exit signals to a process, the +-- definition of 'immediately' is somewhat vague and a scheduler might take +-- time to handle the request, which can lead to situations similar to (1) as +-- listed above, if the scheduler to which the calling process' thread is bound +-- decides to GC whilst another scheduler on which the worker is running is able +-- to continue. +-- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () cancelWith reason = (flip exit) reason . worker --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. +-- +-- See 'Control.Distributed.Process.kill' +cancelKill :: String -> AsyncChan a -> Process () +cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 47651f75..23c108ce 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -51,8 +51,8 @@ import GHC.Conc -- -- Handles of this type cannot cross remote boundaries. data AsyncSTM a = AsyncSTM { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , hWait :: STM a } From 8c3797d8be4d24bbfa9f931b92512b047cce44be Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 02:24:05 +0000 Subject: [PATCH 0589/2357] strip unused types, add documentation --- src/Control/Distributed/Platform/Async.hs | 32 +---- .../Distributed/Platform/Async/AsyncChan.hs | 111 +++++++++++------- .../Distributed/Platform/Async/AsyncSTM.hs | 4 +- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 279a982d..f00ce481 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,22 +12,18 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- The modules herein provides a set of operations for spawning Processes --- and waiting for theie results. It is a thin layer over the basic --- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. --- +-- The modules in the @Async@ package provide operations for spawning Processes, +-- waiting for their results, cancelling them and various other utilities. The +-- two primary implementation are @AsyncChan@ which provides an API which is +-- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that +-- can be used by (as in shared across) multiple processes on a local node. +-- Both abstractions can run asynchronous operations on remote node. ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncResult(..) ) where @@ -44,12 +40,6 @@ import Data.Typeable (Typeable) -- | A reference to an asynchronous action type AsyncRef = ProcessId --- | A reference to an asynchronous worker -type AsyncWorkerId = AsyncRef - --- | A reference to an asynchronous "gatherer" -type AsyncGathererId = AsyncRef - -- | A task to be performed asynchronously. This can either take the -- form of an action that runs over some type @a@ in the @Process@ monad, -- or a tuple that adds the node on which the asynchronous task should be @@ -69,13 +59,3 @@ $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) deriving instance Show a => Show (AsyncResult a) - --- | An async cancellation takes an 'AsyncRef' and does some cancellation --- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] - --- note [local cancel only] --- The cancellation is only ever sent to the insulator process, which is always --- run on the local node. That could be a limitation, as there's nothing in --- 'Async' data profile to stop it being sent remotely. At *that* point, we'd --- need to make the cancellation remote-able too however. diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1dcda7bc..7bc1403b 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,15 +19,14 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- +-- +-- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning @@ -35,8 +34,9 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel - , cancelWith , cancelWait + , cancelWith + , cancelKill -- functions to query an async-result , poll , check @@ -62,40 +62,54 @@ import Data.Maybe ( fromMaybe ) --------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- --------------------------------------------------------------------------------- - -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and +-- | A handle for an asynchronous action spawned by 'async'. +-- Asynchronous actions are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries. Furthermore, handles +-- of this type /must not/ be passed to functions in this module by processes +-- other than the caller of 'async' - that is, this module provides asynchronous +-- actions whose results are accessible *only* by the initiating process. +-- +-- See 'async' data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , channel :: (InternalChannel a) } -- | Spawns an asynchronous action in a new process. +-- We ensure that if the caller's process exits, that the worker is killed. +-- Because an @AsyncChan@ can only be used by the initial caller's process, if +-- that process dies then the result (if any) is discarded. If a process other +-- than the initial caller attempts to obtain the result of an asynchronous +-- action, the behaviour is undefined. It is /highly likely/ that such a +-- process will block indefinitely, quite possible that such behaviour could lead +-- to deadlock and almost certain that resource starvation will occur. /Do Not/ +-- share the handles returned by this function across multiple processes. -- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. +-- If you need to spawn an asynchronous operation whose handle can be shared by +-- multiple processes then use the 'AsyncSTM' module instead. +-- +-- There is currently a contract for async workers, that they should +-- exit normally (i.e., they should not call the @exit@ or @kill@ with their own +-- 'ProcessId' nor use the @terminate@ primitive to cease functining), otherwise +-- the 'AsyncResult' will end up being @AsyncFailed DiedException@ instead of +-- containing the desired result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) async = asyncDo True --- | This is a useful variant of 'async' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. +-- | For *AsyncChan*, 'async' already ensures an @AsyncChan@ is +-- never left running unintentionally. This function is provided for compatibility +-- with other /async/ implementations that may offer different semantics for +-- @async@ with regards linking. +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async @@ -112,8 +126,7 @@ asyncDo shouldLink task = do spawnWorkers :: (Serializable a) => AsyncTask a -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) + -> Process (AsyncRef, AsyncRef, InternalChannel a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -224,36 +237,52 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. Cancellation is achieved using message --- passing, and is therefore asynchronous in nature. To wait for cancellation --- to complete, use 'cancelWait' instead. The notes about the asynchronous --- nature of 'cancelWait' apply here also. +-- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. +-- To wait for cancellation to complete, use 'cancelWait' instead. The notes +-- about the asynchronous nature of 'cancelWait' apply here also. -- -- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait --- | Cancels an asynchronous operation using the supplied exit reason. --- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do --- apply here, but more importantly this function sends an /exit signal/ to the --- asynchronous worker, which leads to the following semantics: +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For +-- example, the worker may complete its task after this function is called, but +-- before the cancellation instruction is acted upon. +-- +-- If you wish to stop an asychronous operation /immediately/ (with caveats) then +-- consider using 'cancelWith' or 'cancelKill' instead. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync + +-- | Cancel an asynchronous operation immediately. +-- This operation is performed by sending an /exit signal/ to the asynchronous +-- worker, which leads to the following semantics: -- --- 1. if the worker already completed, this function does nothing +-- 1. if the worker already completed, this function has no effect -- 2. the worker might complete after this call, but before the signal arrives -- 3. the worker might ignore the exit signal using @catchExit@ -- --- In case of (3), this function will have no effect. You should use 'cancel' +-- In case of (3), this function has no effect. You should use 'cancel' -- if you need to guarantee that the asynchronous task is unable to ignore -- the cancellation instruction. -- +-- You should also consider that when sending exit signals to a process, the +-- definition of 'immediately' is somewhat vague and a scheduler might take +-- time to handle the request, which can lead to situations similar to (1) as +-- listed above, if the scheduler to which the calling process' thread is bound +-- decides to GC whilst another scheduler on which the worker is running is able +-- to continue. +-- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () cancelWith reason = (flip exit) reason . worker --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. +-- +-- See 'Control.Distributed.Process.kill' +cancelKill :: String -> AsyncChan a -> Process () +cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 47651f75..23c108ce 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -51,8 +51,8 @@ import GHC.Conc -- -- Handles of this type cannot cross remote boundaries. data AsyncSTM a = AsyncSTM { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , hWait :: STM a } From 4f3345fb418c7b6d0c207b7fb72b7995561824f8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 29 Dec 2012 02:24:05 +0000 Subject: [PATCH 0590/2357] strip unused types, add documentation --- src/Control/Distributed/Platform/Async.hs | 32 +---- .../Distributed/Platform/Async/AsyncChan.hs | 111 +++++++++++------- .../Distributed/Platform/Async/AsyncSTM.hs | 4 +- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index 279a982d..f00ce481 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -12,22 +12,18 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- The modules herein provides a set of operations for spawning Processes --- and waiting for theie results. It is a thin layer over the basic --- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. --- +-- The modules in the @Async@ package provide operations for spawning Processes, +-- waiting for their results, cancelling them and various other utilities. The +-- two primary implementation are @AsyncChan@ which provides an API which is +-- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that +-- can be used by (as in shared across) multiple processes on a local node. +-- Both abstractions can run asynchronous operations on remote node. ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncResult(..) ) where @@ -44,12 +40,6 @@ import Data.Typeable (Typeable) -- | A reference to an asynchronous action type AsyncRef = ProcessId --- | A reference to an asynchronous worker -type AsyncWorkerId = AsyncRef - --- | A reference to an asynchronous "gatherer" -type AsyncGathererId = AsyncRef - -- | A task to be performed asynchronously. This can either take the -- form of an action that runs over some type @a@ in the @Process@ monad, -- or a tuple that adds the node on which the asynchronous task should be @@ -69,13 +59,3 @@ $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) deriving instance Show a => Show (AsyncResult a) - --- | An async cancellation takes an 'AsyncRef' and does some cancellation --- operation in the @Process@ monad. -type AsyncCancel = AsyncRef -> Process () -- note [local cancel only] - --- note [local cancel only] --- The cancellation is only ever sent to the insulator process, which is always --- run on the local node. That could be a limitation, as there's nothing in --- 'Async' data profile to stop it being sent remotely. At *that* point, we'd --- need to make the cancellation remote-able too however. diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 1dcda7bc..7bc1403b 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,15 +19,14 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- +-- +-- ----------------------------------------------------------------------------- module Control.Distributed.Platform.Async.AsyncChan ( -- types/data AsyncRef - , AsyncWorkerId - , AsyncGathererId , AsyncTask - , AsyncCancel , AsyncChan(worker) , AsyncResult(..) -- functions for starting/spawning @@ -35,8 +34,9 @@ module Control.Distributed.Platform.Async.AsyncChan , asyncLinked -- and stopping/killing , cancel - , cancelWith , cancelWait + , cancelWith + , cancelKill -- functions to query an async-result , poll , check @@ -62,40 +62,54 @@ import Data.Maybe ( fromMaybe ) --------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- --------------------------------------------------------------------------------- - -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) --- | An handle for an asynchronous action spawned by 'async'. --- Asynchronous operations are run in a separate process, and +-- | A handle for an asynchronous action spawned by 'async'. +-- Asynchronous actions are run in a separate process, and -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries. Furthermore, handles +-- of this type /must not/ be passed to functions in this module by processes +-- other than the caller of 'async' - that is, this module provides asynchronous +-- actions whose results are accessible *only* by the initiating process. +-- +-- See 'async' data AsyncChan a = AsyncChan { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , channel :: (InternalChannel a) } -- | Spawns an asynchronous action in a new process. +-- We ensure that if the caller's process exits, that the worker is killed. +-- Because an @AsyncChan@ can only be used by the initial caller's process, if +-- that process dies then the result (if any) is discarded. If a process other +-- than the initial caller attempts to obtain the result of an asynchronous +-- action, the behaviour is undefined. It is /highly likely/ that such a +-- process will block indefinitely, quite possible that such behaviour could lead +-- to deadlock and almost certain that resource starvation will occur. /Do Not/ +-- share the handles returned by this function across multiple processes. -- --- There is currently a contract for async workers which is that they should --- exit normally (i.e., they should not call the @exit selfPid reason@ nor --- @terminate@ primitives), otherwise the 'AsyncResult' will end up being --- @AsyncFailed DiedException@ instead of containing the result. +-- If you need to spawn an asynchronous operation whose handle can be shared by +-- multiple processes then use the 'AsyncSTM' module instead. +-- +-- There is currently a contract for async workers, that they should +-- exit normally (i.e., they should not call the @exit@ or @kill@ with their own +-- 'ProcessId' nor use the @terminate@ primitive to cease functining), otherwise +-- the 'AsyncResult' will end up being @AsyncFailed DiedException@ instead of +-- containing the desired result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) async = asyncDo True --- | This is a useful variant of 'async' that ensures an @AsyncChan@ is --- never left running unintentionally. We ensure that if the caller's process --- exits, that the worker is killed. Because an @AsyncChan@ can only be used --- by the initial caller's process, if that process dies then the result --- (if any) is discarded. +-- | For *AsyncChan*, 'async' already ensures an @AsyncChan@ is +-- never left running unintentionally. This function is provided for compatibility +-- with other /async/ implementations that may offer different semantics for +-- @async@ with regards linking. +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async @@ -112,8 +126,7 @@ asyncDo shouldLink task = do spawnWorkers :: (Serializable a) => AsyncTask a -> Bool - -> Process (AsyncRef, AsyncRef, - (SendPort (AsyncResult a), ReceivePort (AsyncResult a))) + -> Process (AsyncRef, AsyncRef, InternalChannel a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan @@ -224,36 +237,52 @@ waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) --- | Cancel an asynchronous operation. Cancellation is achieved using message --- passing, and is therefore asynchronous in nature. To wait for cancellation --- to complete, use 'cancelWait' instead. The notes about the asynchronous --- nature of 'cancelWait' apply here also. +-- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. +-- To wait for cancellation to complete, use 'cancelWait' instead. The notes +-- about the asynchronous nature of 'cancelWait' apply here also. -- -- See 'Control.Distributed.Process' cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait --- | Cancels an asynchronous operation using the supplied exit reason. --- The notes about the asynchronous nature of 'cancel' and 'cancelWait' do --- apply here, but more importantly this function sends an /exit signal/ to the --- asynchronous worker, which leads to the following semantics: +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For +-- example, the worker may complete its task after this function is called, but +-- before the cancellation instruction is acted upon. +-- +-- If you wish to stop an asychronous operation /immediately/ (with caveats) then +-- consider using 'cancelWith' or 'cancelKill' instead. +-- +cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) +cancelWait hAsync = cancel hAsync >> wait hAsync + +-- | Cancel an asynchronous operation immediately. +-- This operation is performed by sending an /exit signal/ to the asynchronous +-- worker, which leads to the following semantics: -- --- 1. if the worker already completed, this function does nothing +-- 1. if the worker already completed, this function has no effect -- 2. the worker might complete after this call, but before the signal arrives -- 3. the worker might ignore the exit signal using @catchExit@ -- --- In case of (3), this function will have no effect. You should use 'cancel' +-- In case of (3), this function has no effect. You should use 'cancel' -- if you need to guarantee that the asynchronous task is unable to ignore -- the cancellation instruction. -- +-- You should also consider that when sending exit signals to a process, the +-- definition of 'immediately' is somewhat vague and a scheduler might take +-- time to handle the request, which can lead to situations similar to (1) as +-- listed above, if the scheduler to which the calling process' thread is bound +-- decides to GC whilst another scheduler on which the worker is running is able +-- to continue. +-- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () cancelWith reason = (flip exit) reason . worker --- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. --- -cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) -cancelWait hAsync = cancel hAsync >> wait hAsync +-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. +-- +-- See 'Control.Distributed.Process.kill' +cancelKill :: String -> AsyncChan a -> Process () +cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 47651f75..23c108ce 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -51,8 +51,8 @@ import GHC.Conc -- -- Handles of this type cannot cross remote boundaries. data AsyncSTM a = AsyncSTM { - worker :: AsyncWorkerId - , insulator :: AsyncGathererId + worker :: AsyncRef + , insulator :: AsyncRef , hWait :: STM a } From 4121f2cff546cf3a801580dfbca6afd0a823f5da Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 20:45:09 +0000 Subject: [PATCH 0591/2357] implement linkOnFailure --- distributed-process-platform.cabal | 29 ++++++++++++++++ .../Platform/Internal/Primitives.hs | 33 +++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 75c83ac2..31e70cc7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,3 +68,32 @@ test-suite TimerTests TestUtils extensions: CPP main-is: TestTimer.hs + +test-suite PrimitivesTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives + TestUtils + extensions: CPP + main-is: TestPrimitives.hs + diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs index ffe09e6b..768a3566 100644 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -1,19 +1,16 @@ --- | Common Entities used throughout -platform. +-- | Common Entities used throughout -platform. -- NB: Please DO NOT use this module as a dumping ground. -- module Control.Distributed.Platform.Internal.Primitives ( spawnLinkLocal , spawnMonitorLocal + , linkOnFailure ) where import Control.Distributed.Process - ( link - , spawnLocal - , monitor - , Process() - , ProcessId - , MonitorRef) +import Control.Concurrent (myThreadId, throwTo) +import Control.Monad (void) -- | Node local version of 'Control.Distributed.Process.spawnLink'. -- Note that this is just the sequential composition of 'spawn' and 'link'. @@ -31,3 +28,25 @@ spawnMonitorLocal p = do pid <- spawnLocal p ref <- monitor pid return (pid, ref) + +-- | CH's 'link' primitive, unlike Erlang's, will trigger when the target +-- process dies for any reason. linkOnFailure has semantics like Erlang's: +-- it will trigger only when the target function dies abnormally. +linkOnFailure :: ProcessId -> Process () +linkOnFailure them = do + us <- getSelfPid + tid <- liftIO $ myThreadId + void $ spawnLocal $ do + callerRef <- monitor us + calleeRef <- monitor them + reason <- receiveWait [ + matchIf (\(ProcessMonitorNotification mRef _ _) -> + mRef == callerRef) -- nothing left to do + (\_ -> return DiedNormal) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> + mRef' == calleeRef) + (\(ProcessMonitorNotification _ _ r') -> return r') + ] + case reason of + DiedNormal -> return () + _ -> liftIO $ throwTo tid (ProcessLinkException us reason) From 47ee12b189689f2aeca6c2ab61bbaf4d0aebe63c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 20:45:09 +0000 Subject: [PATCH 0592/2357] implement linkOnFailure --- distributed-process-platform.cabal | 29 ++++++++++++++++ .../Platform/Internal/Primitives.hs | 33 +++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 75c83ac2..31e70cc7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,3 +68,32 @@ test-suite TimerTests TestUtils extensions: CPP main-is: TestTimer.hs + +test-suite PrimitivesTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives + TestUtils + extensions: CPP + main-is: TestPrimitives.hs + diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs index ffe09e6b..768a3566 100644 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -1,19 +1,16 @@ --- | Common Entities used throughout -platform. +-- | Common Entities used throughout -platform. -- NB: Please DO NOT use this module as a dumping ground. -- module Control.Distributed.Platform.Internal.Primitives ( spawnLinkLocal , spawnMonitorLocal + , linkOnFailure ) where import Control.Distributed.Process - ( link - , spawnLocal - , monitor - , Process() - , ProcessId - , MonitorRef) +import Control.Concurrent (myThreadId, throwTo) +import Control.Monad (void) -- | Node local version of 'Control.Distributed.Process.spawnLink'. -- Note that this is just the sequential composition of 'spawn' and 'link'. @@ -31,3 +28,25 @@ spawnMonitorLocal p = do pid <- spawnLocal p ref <- monitor pid return (pid, ref) + +-- | CH's 'link' primitive, unlike Erlang's, will trigger when the target +-- process dies for any reason. linkOnFailure has semantics like Erlang's: +-- it will trigger only when the target function dies abnormally. +linkOnFailure :: ProcessId -> Process () +linkOnFailure them = do + us <- getSelfPid + tid <- liftIO $ myThreadId + void $ spawnLocal $ do + callerRef <- monitor us + calleeRef <- monitor them + reason <- receiveWait [ + matchIf (\(ProcessMonitorNotification mRef _ _) -> + mRef == callerRef) -- nothing left to do + (\_ -> return DiedNormal) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> + mRef' == calleeRef) + (\(ProcessMonitorNotification _ _ r') -> return r') + ] + case reason of + DiedNormal -> return () + _ -> liftIO $ throwTo tid (ProcessLinkException us reason) From adde3e51e36e50ae45e592902f583e9db7474f3f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 20:45:09 +0000 Subject: [PATCH 0593/2357] implement linkOnFailure --- distributed-process-platform.cabal | 29 ++++++++++++++++ .../Platform/Internal/Primitives.hs | 33 +++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 75c83ac2..31e70cc7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,3 +68,32 @@ test-suite TimerTests TestUtils extensions: CPP main-is: TestTimer.hs + +test-suite PrimitivesTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives + TestUtils + extensions: CPP + main-is: TestPrimitives.hs + diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs index ffe09e6b..768a3566 100644 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -1,19 +1,16 @@ --- | Common Entities used throughout -platform. +-- | Common Entities used throughout -platform. -- NB: Please DO NOT use this module as a dumping ground. -- module Control.Distributed.Platform.Internal.Primitives ( spawnLinkLocal , spawnMonitorLocal + , linkOnFailure ) where import Control.Distributed.Process - ( link - , spawnLocal - , monitor - , Process() - , ProcessId - , MonitorRef) +import Control.Concurrent (myThreadId, throwTo) +import Control.Monad (void) -- | Node local version of 'Control.Distributed.Process.spawnLink'. -- Note that this is just the sequential composition of 'spawn' and 'link'. @@ -31,3 +28,25 @@ spawnMonitorLocal p = do pid <- spawnLocal p ref <- monitor pid return (pid, ref) + +-- | CH's 'link' primitive, unlike Erlang's, will trigger when the target +-- process dies for any reason. linkOnFailure has semantics like Erlang's: +-- it will trigger only when the target function dies abnormally. +linkOnFailure :: ProcessId -> Process () +linkOnFailure them = do + us <- getSelfPid + tid <- liftIO $ myThreadId + void $ spawnLocal $ do + callerRef <- monitor us + calleeRef <- monitor them + reason <- receiveWait [ + matchIf (\(ProcessMonitorNotification mRef _ _) -> + mRef == callerRef) -- nothing left to do + (\_ -> return DiedNormal) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> + mRef' == calleeRef) + (\(ProcessMonitorNotification _ _ r') -> return r') + ] + case reason of + DiedNormal -> return () + _ -> liftIO $ throwTo tid (ProcessLinkException us reason) From 51ccf29aa7831d1450e9cf9aeefe796ff40030cf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 20:45:09 +0000 Subject: [PATCH 0594/2357] implement linkOnFailure --- distributed-process-platform.cabal | 29 ++++++++++++++++ .../Platform/Internal/Primitives.hs | 33 +++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 75c83ac2..31e70cc7 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,3 +68,32 @@ test-suite TimerTests TestUtils extensions: CPP main-is: TestTimer.hs + +test-suite PrimitivesTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + test-framework >= 0.6 && < 0.7, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives + TestUtils + extensions: CPP + main-is: TestPrimitives.hs + diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs index ffe09e6b..768a3566 100644 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Platform/Internal/Primitives.hs @@ -1,19 +1,16 @@ --- | Common Entities used throughout -platform. +-- | Common Entities used throughout -platform. -- NB: Please DO NOT use this module as a dumping ground. -- module Control.Distributed.Platform.Internal.Primitives ( spawnLinkLocal , spawnMonitorLocal + , linkOnFailure ) where import Control.Distributed.Process - ( link - , spawnLocal - , monitor - , Process() - , ProcessId - , MonitorRef) +import Control.Concurrent (myThreadId, throwTo) +import Control.Monad (void) -- | Node local version of 'Control.Distributed.Process.spawnLink'. -- Note that this is just the sequential composition of 'spawn' and 'link'. @@ -31,3 +28,25 @@ spawnMonitorLocal p = do pid <- spawnLocal p ref <- monitor pid return (pid, ref) + +-- | CH's 'link' primitive, unlike Erlang's, will trigger when the target +-- process dies for any reason. linkOnFailure has semantics like Erlang's: +-- it will trigger only when the target function dies abnormally. +linkOnFailure :: ProcessId -> Process () +linkOnFailure them = do + us <- getSelfPid + tid <- liftIO $ myThreadId + void $ spawnLocal $ do + callerRef <- monitor us + calleeRef <- monitor them + reason <- receiveWait [ + matchIf (\(ProcessMonitorNotification mRef _ _) -> + mRef == callerRef) -- nothing left to do + (\_ -> return DiedNormal) + , matchIf (\(ProcessMonitorNotification mRef' _ _) -> + mRef' == calleeRef) + (\(ProcessMonitorNotification _ _ r') -> return r') + ] + case reason of + DiedNormal -> return () + _ -> liftIO $ throwTo tid (ProcessLinkException us reason) From 951550e2a86a4befae29beaefd7a14017e13d822 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 21:01:44 +0000 Subject: [PATCH 0595/2357] export linkOnFailure; add test case for abnormal exits --- src/Control/Distributed/Platform.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index a5672b78..d9be491d 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -5,6 +5,7 @@ module Control.Distributed.Platform -- extra primitives spawnLinkLocal , spawnMonitorLocal + , linkOnFailure -- time interval handling , milliseconds , seconds From 0083321f4ae9276810e4678f416b79aec8538086 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 21:01:44 +0000 Subject: [PATCH 0596/2357] export linkOnFailure; add test case for abnormal exits --- src/Control/Distributed/Platform.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index a5672b78..d9be491d 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -5,6 +5,7 @@ module Control.Distributed.Platform -- extra primitives spawnLinkLocal , spawnMonitorLocal + , linkOnFailure -- time interval handling , milliseconds , seconds From cd0192f33eb69fa5fbfc5462e1066c6cd98d1fd1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 21:01:44 +0000 Subject: [PATCH 0597/2357] export linkOnFailure; add test case for abnormal exits --- src/Control/Distributed/Platform.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index a5672b78..d9be491d 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -5,6 +5,7 @@ module Control.Distributed.Platform -- extra primitives spawnLinkLocal , spawnMonitorLocal + , linkOnFailure -- time interval handling , milliseconds , seconds From e729d3851636385b89dcf33199029436eaf1c7c2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 4 Jan 2013 21:01:44 +0000 Subject: [PATCH 0598/2357] export linkOnFailure; add test case for abnormal exits --- src/Control/Distributed/Platform.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs index a5672b78..d9be491d 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Platform.hs @@ -5,6 +5,7 @@ module Control.Distributed.Platform -- extra primitives spawnLinkLocal , spawnMonitorLocal + , linkOnFailure -- time interval handling , milliseconds , seconds From fc52c3eaccbb61ef049b05037bec618fe0aa7452 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 5 Jan 2013 12:14:14 +0000 Subject: [PATCH 0599/2357] add synchronised logging support for test cases --- distributed-process-platform.cabal | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1769e7e..0ea3f227 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -70,9 +70,9 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP @@ -88,6 +88,7 @@ test-suite TimerTests derive, network-transport >= 0.3 && < 0.4, mtl, + stm >= 2.3 && < 2.5, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -103,9 +104,9 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP From bb3fc7f6374cc23bb05e98c2b7c564a6e011c808 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 5 Jan 2013 12:14:14 +0000 Subject: [PATCH 0600/2357] add synchronised logging support for test cases --- distributed-process-platform.cabal | 5 ++-- tests/TestUtils.hs | 47 ++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1769e7e..0ea3f227 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -70,9 +70,9 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP @@ -88,6 +88,7 @@ test-suite TimerTests derive, network-transport >= 0.3 && < 0.4, mtl, + stm >= 2.3 && < 2.5, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -103,9 +104,9 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index ee319330..fbe9eb02 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -18,6 +18,11 @@ module TestUtils , testProcessReport , delayedAssertion , assertComplete + -- logging + , Logger() + , newLogger + , putLogMsg + , stopLogger -- runners , testMain ) where @@ -26,18 +31,31 @@ import Prelude hiding (catch) import Data.Binary import Data.Typeable (Typeable) import Data.DeriveTH +import Control.Concurrent + ( ThreadId + , myThreadId + , forkIO + ) +import Control.Concurrent.STM + ( TQueue + , newTQueueIO + , readTQueue + , writeTQueue + ) import Control.Concurrent.MVar ( MVar , newEmptyMVar , putMVar , takeMVar ) +import Control.Exception +import Control.Monad (forever) +import Control.Monad.STM (atomically) + import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() -import Control.Monad (forever) - import Test.HUnit (Assertion) import Test.HUnit.Base (assertBool) import Test.Framework (Test, defaultMain) @@ -116,6 +134,31 @@ noop = return () stash :: TestResult a -> a -> Process () stash mvar x = liftIO $ putMVar mvar x +-- synchronised logging + +data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String } + +-- | Create a new Logger. +-- Logger uses a 'TQueue' to receive and process messages on a worker thread. +newLogger :: IO Logger +newLogger = do + tid <- liftIO $ myThreadId + q <- liftIO $ newTQueueIO + forkIO $ logger q + return $ Logger tid q + where logger q' = forever $ do + msg <- atomically $ readTQueue q' + putStrLn msg + +-- | Send a message to the Logger +putLogMsg :: Logger -> String -> Process () +putLogMsg logger msg = liftIO $ atomically $ writeTQueue (msgs logger) msg + +-- | Stop the worker thread for the given Logger +stopLogger :: Logger -> IO () +stopLogger = (flip throwTo) ThreadKilled . _tid + +-- | Given a @builder@ function, make and run a test suite on a single transport testMain :: (NT.Transport -> IO [Test]) -> IO () testMain builder = do Right (transport, _) <- createTransportExposeInternals From f864705218b1d2cf8192767ad5e516b046f3dd1e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 5 Jan 2013 12:14:14 +0000 Subject: [PATCH 0601/2357] add synchronised logging support for test cases --- distributed-process-platform.cabal | 5 ++-- tests/TestUtils.hs | 47 ++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1769e7e..0ea3f227 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -70,9 +70,9 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP @@ -88,6 +88,7 @@ test-suite TimerTests derive, network-transport >= 0.3 && < 0.4, mtl, + stm >= 2.3 && < 2.5, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -103,9 +104,9 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index ee319330..fbe9eb02 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -18,6 +18,11 @@ module TestUtils , testProcessReport , delayedAssertion , assertComplete + -- logging + , Logger() + , newLogger + , putLogMsg + , stopLogger -- runners , testMain ) where @@ -26,18 +31,31 @@ import Prelude hiding (catch) import Data.Binary import Data.Typeable (Typeable) import Data.DeriveTH +import Control.Concurrent + ( ThreadId + , myThreadId + , forkIO + ) +import Control.Concurrent.STM + ( TQueue + , newTQueueIO + , readTQueue + , writeTQueue + ) import Control.Concurrent.MVar ( MVar , newEmptyMVar , putMVar , takeMVar ) +import Control.Exception +import Control.Monad (forever) +import Control.Monad.STM (atomically) + import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() -import Control.Monad (forever) - import Test.HUnit (Assertion) import Test.HUnit.Base (assertBool) import Test.Framework (Test, defaultMain) @@ -116,6 +134,31 @@ noop = return () stash :: TestResult a -> a -> Process () stash mvar x = liftIO $ putMVar mvar x +-- synchronised logging + +data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String } + +-- | Create a new Logger. +-- Logger uses a 'TQueue' to receive and process messages on a worker thread. +newLogger :: IO Logger +newLogger = do + tid <- liftIO $ myThreadId + q <- liftIO $ newTQueueIO + forkIO $ logger q + return $ Logger tid q + where logger q' = forever $ do + msg <- atomically $ readTQueue q' + putStrLn msg + +-- | Send a message to the Logger +putLogMsg :: Logger -> String -> Process () +putLogMsg logger msg = liftIO $ atomically $ writeTQueue (msgs logger) msg + +-- | Stop the worker thread for the given Logger +stopLogger :: Logger -> IO () +stopLogger = (flip throwTo) ThreadKilled . _tid + +-- | Given a @builder@ function, make and run a test suite on a single transport testMain :: (NT.Transport -> IO [Test]) -> IO () testMain builder = do Right (transport, _) <- createTransportExposeInternals From f492bac9a5e848fed5c0390094e129407bd81bb0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 5 Jan 2013 12:14:14 +0000 Subject: [PATCH 0602/2357] add synchronised logging support for test cases --- distributed-process-platform.cabal | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d1769e7e..0ea3f227 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -70,9 +70,9 @@ test-suite AsyncTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP @@ -88,6 +88,7 @@ test-suite TimerTests derive, network-transport >= 0.3 && < 0.4, mtl, + stm >= 2.3 && < 2.5, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -103,9 +104,9 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives - TestUtils, Control.Distributed.Platform.Async, Control.Distributed.Platform.Async.AsyncSTM, + TestUtils, TestAsyncSTM, TestAsync extensions: CPP From ff61a1b826c3eb53f1a2c85c13eaa84c52d99b17 Mon Sep 17 00:00:00 2001 From: Pankaj More Date: Sat, 5 Jan 2013 23:35:39 +0530 Subject: [PATCH 0603/2357] build on ghc-7.6.1 --- network-transport-inmemory.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network-transport-inmemory.cabal b/network-transport-inmemory.cabal index ff36fa85..9f819971 100644 --- a/network-transport-inmemory.cabal +++ b/network-transport-inmemory.cabal @@ -19,8 +19,8 @@ Library Build-Depends: base >= 4.3 && < 5, network-transport >= 0.3 && < 0.4, data-accessor >= 0.2 && < 0.3, - bytestring >= 0.9 && < 0.10, - containers >= 0.4 && < 0.5 + bytestring >= 0.9 && < 0.11, + containers >= 0.4 && < 0.6 Exposed-modules: Network.Transport.Chan ghc-options: -Wall -fno-warn-unused-do-bind HS-Source-Dirs: src From d02138f17a06bd7e38380504113c4b2434c29d91 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 17:27:18 +0000 Subject: [PATCH 0604/2357] Control.Distributed.Platform => Control.Distributed.Process.Platform --- distributed-process-platform.cabal | 20 +- src/Control/Distributed/Platform.hs | 59 --- .../Distributed/Platform/GenProcess.hs | 244 ------------ src/Control/Distributed/Platform/GenServer.hs | 366 ------------------ .../Platform/Internal/Primitives.hs | 52 --- .../Distributed/Platform/Internal/Types.hs | 38 -- src/Control/Distributed/Platform/Timer.hs | 136 ------- .../{Platform => Process}/Async.hs | 4 +- tests/GenServer/Counter.hs | 2 +- tests/GenServer/Kitty.hs | 2 +- tests/TestGenServer.hs | 4 +- 11 files changed, 16 insertions(+), 911 deletions(-) delete mode 100644 src/Control/Distributed/Platform.hs delete mode 100644 src/Control/Distributed/Platform/GenProcess.hs delete mode 100644 src/Control/Distributed/Platform/GenServer.hs delete mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs delete mode 100644 src/Control/Distributed/Platform/Internal/Types.hs delete mode 100644 src/Control/Distributed/Platform/Timer.hs rename src/Control/Distributed/{Platform => Process}/Async.hs (95%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 31e70cc7..b1be4ca9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -34,11 +34,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform, - Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer, - Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Async + other-modules: Control.Distributed.Process.Platform.Internal.Primitives test-suite TimerTests type: exitcode-stdio-1.0 @@ -62,9 +62,9 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -91,8 +91,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs deleted file mode 100644 index d9be491d..00000000 --- a/src/Control/Distributed/Platform.hs +++ /dev/null @@ -1,59 +0,0 @@ --- | [Cloud Haskell Platform] --- -module Control.Distributed.Platform - ( - -- extra primitives - spawnLinkLocal - , spawnMonitorLocal - , linkOnFailure - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - , Timeout(..) - , TimeInterval - , TimeUnit - ) where - -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Internal.Primitives - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs deleted file mode 100644 index 79845f90..00000000 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ /dev/null @@ -1,244 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Control.Distributed.Platform.GenProcess where - --- TODO: define API and hide internals... - -import qualified Control.Distributed.Process as BaseProcess -import qualified Control.Monad.State as ST (StateT, get, - lift, modify, - put, runStateT) - -import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - - -type ServerName = String -type ServerPid = BaseProcess.ProcessId - -data ServerId = ServerProcess ServerPid | NamedServer ServerName - -data Recipient a = SendToPid BaseProcess.ProcessId | - SendToPort (BaseProcess.SendPort a) - --- | Initialize handler result -data InitResult = - InitOk Timeout - | InitStop String - --- | Terminate reason -data TerminateReason = - TerminateNormal - | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - -data ReplyTo = ReplyTo BaseProcess.ProcessId | None - deriving (Typeable, Show) -$(derive makeBinary ''ReplyTo) - --- | The result of a call -data ProcessAction = - ProcessContinue - | ProcessTimeout Timeout - | ProcessStop String - deriving (Typeable) -$(derive makeBinary ''ProcessAction) - -type Process s = ST.StateT s BaseProcess.Process - --- | Handlers -type InitHandler s = Process s InitResult -type TerminateHandler s = TerminateReason -> Process s () -type RequestHandler s a = Message a -> Process s ProcessAction - --- | Contains the actual payload and possibly additional routing metadata -data Message a = Message ReplyTo a - deriving (Show, Typeable) -$(derive makeBinary ''Message) - -data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) - deriving (Typeable) -$(derive makeBinary ''Rpc) - --- | Dispatcher that knows how to dispatch messages to a handler -data Dispatcher s = - forall a . (Serializable a) => - Dispatch { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction) } | - forall a . (Serializable a) => - DispatchIf { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction), - condition :: s -> Message a -> Bool } - --- dispatching to implementation callbacks - --- | Matches messages using a dispatcher -class Dispatchable d where - matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) - --- | Matches messages to a MessageDispatcher -instance Dispatchable Dispatcher where - matchMessage s (Dispatch d ) = BaseProcess.match (d s) - matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) - - -data Behaviour s = Behaviour { - initHandler :: InitHandler s -- ^ initialization handler - , dispatchers :: [Dispatcher s] - , terminateHandler :: TerminateHandler s -- ^ termination handler - } - --- | Management message --- TODO is there a std way of terminating a process from another process? -data Termination = Terminate TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''Termination) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a new server and return it's id --- start :: Behaviour s -> Process ProcessId --- start handlers = spawnLocal $ runProcess handlers - -reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () -reply (ReplyTo pid) m = BaseProcess.send pid m -reply _ _ = return () - -replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> - BaseProcess.Process () -replyVia p m = BaseProcess.sendChan p m - --- | Given a state, behaviour specificiation and spawn function, --- starts a new server and return its id. The spawn function is typically --- one taken from "Control.Distributed.Process". --- see 'Control.Distributed.Process.spawn' --- 'Control.Distributed.Process.spawnLocal' --- 'Control.Distributed.Process.spawnLink' --- 'Control.Distributed.Process.spawnMonitor' --- 'Control.Distributed.Process.spawnSupervised' -start :: - s -> Behaviour s -> - (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> - BaseProcess.Process BaseProcess.ProcessId -start state handlers spawn = spawn $ do - _ <- ST.runStateT (runProc handlers) state - return () - -send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () -send s m = do - let msg = (Message None m) - case s of - ServerProcess pid -> BaseProcess.send pid msg - NamedServer name -> BaseProcess.nsend name msg - --- process request handling - -handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s -handleRequest = handleRequestIf (const True) - -handleRequestIf :: (Serializable a) => (a -> Bool) -> - RequestHandler s a -> Dispatcher s -handleRequestIf cond handler = DispatchIf { - dispatch = (\state m@(Message _ _) -> do - (r, s') <- ST.runStateT (handler m) state - return (s', r) - ), - condition = \_ (Message _ req) -> cond req -} - --- process state management - --- | gets the process state -getState :: Process s s -getState = ST.get - --- | sets the process state -putState :: s -> Process s () -putState = ST.put - --- | modifies the server state -modifyState :: (s -> s) -> Process s () -modifyState = ST.modify - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- | server process -runProc :: Behaviour s -> Process s () -runProc s = do - ir <- init s - tr <- case ir of - InitOk t -> do - trace $ "Server ready to receive messages!" - loop s t - InitStop r -> return (TerminateReason r) - terminate s tr - --- | initialize server -init :: Behaviour s -> Process s InitResult -init s = do - trace $ "Server initializing ... " - ir <- initHandler s - return ir - -loop :: Behaviour s -> Timeout -> Process s TerminateReason -loop s t = do - s' <- processReceive (dispatchers s) t - nextAction s s' - where nextAction :: Behaviour s -> ProcessAction -> - Process s TerminateReason - nextAction b ProcessContinue = loop b t - nextAction b (ProcessTimeout t') = loop b t' - nextAction _ (ProcessStop r) = return (TerminateReason r) - -processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction -processReceive ds timeout = do - s <- getState - let ms = map (matchMessage s) ds - -- TODO: should we drain the message queue to avoid selective receive here? - case timeout of - Infinity -> do - (s', r) <- ST.lift $ BaseProcess.receiveWait ms - putState s' - return r - Timeout t -> do - result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms - case result of - Just (s', r) -> do - putState s' - return r - Nothing -> do - return $ ProcessStop "timed out" - -terminate :: Behaviour s -> TerminateReason -> Process s () -terminate s reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler s) reason - --- | Log a trace message using the underlying Process's say -trace :: String -> Process s () -trace msg = ST.lift . BaseProcess.say $ msg - --- data Upgrade = ??? --- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may handle hot server-code loading quite easily... - diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs deleted file mode 100644 index b8b91710..00000000 --- a/src/Control/Distributed/Platform/GenServer.hs +++ /dev/null @@ -1,366 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Second iteration of GenServer -module Control.Distributed.Platform.GenServer ( - ServerId, - Timeout(..), - initOk, - initStop, - ok, - forward, - stop, - InitHandler, - Handler, - TerminateHandler, - MessageDispatcher(), - handle, - handleIf, - handleAny, - putState, - getState, - modifyState, - LocalServer(..), - defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - Async(), - call, - callTimeout, - callAsync, - wait, - waitTimeout, - Process, - trace - ) where - -import qualified Control.Distributed.Process as P (forward, catch) -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) - -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, - exit, getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) - -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform -import Control.Distributed.Platform.Async - -import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) -import Data.DeriveTH -import Data.Typeable (Typeable) - --------------------------------------------------------------------------------- --- Data Types -- --------------------------------------------------------------------------------- - --- | ServerId -type ServerId = ProcessId - --- | Server monad -newtype Server s a = Server { - unServer :: ST.StateT s Process a - } - deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) - --- | Initialize handler result -data InitResult - = InitOk Timeout - | InitStop String - -initOk :: Timeout -> Server s InitResult -initOk t = return (InitOk t) - -initStop :: String -> Server s InitResult -initStop reason = return (InitStop reason) - --- | Terminate reason -data TerminateReason - = TerminateNormal - | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - --- | The result of a call -data Result a - = Ok a - | Forward ServerId - | Stop a String - deriving (Show, Typeable) - -ok :: (Serializable a, Show a) => a -> Server s (Result a) -ok resp = return (Ok resp) - -forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) -forward sid = return (Forward sid) - -stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) -stop resp reason = return (Stop resp reason) - --- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type Handler s a b = a -> Server s (Result b) - --- | Adds routing metadata to the actual payload -data Message a = - CallMessage { msgFrom :: ProcessId, msgPayload :: a } - | CastMessage { msgFrom :: ProcessId, msgPayload :: a } - deriving (Show, Typeable) -$(derive makeBinary ''Message) - --- | Dispatcher that knows how to dispatch messages to a handler --- s The server state -data MessageDispatcher s = - forall a . (Serializable a) => MessageDispatcher { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) - } - | forall a . (Serializable a) => MessageDispatcherIf { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason), - dispatchIf :: s -> Message a -> Bool - } - | MessageDispatcherAny { - dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) - } - --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - --- | Matches messages to a MessageDispatcher -instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher d) = match (d s) - matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) - matchMessage s (MessageDispatcherAny d) = matchAny (d s) - --- | Constructs a call message dispatcher --- -handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s -handle = handleIf (const True) - -handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s -handleIf cond handler = MessageDispatcherIf { - dispatcher = (\s msg -> case msg of - CallMessage cid payload -> do - --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Ok resp -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Nothing) - Forward sid -> do - --say $ "Server FORWARD to: " ++ show sid - send sid msg - return (s', Nothing) - Stop resp reason -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do - --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - send sid msg - return (s', Nothing) - ), - dispatchIf = \_ msg -> cond (msgPayload msg) -} - --- | Constructs a dispatcher for any message --- Note that since we don't know the type of this message it assumes the protocol of a cast --- i.e. no reply's -handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s -handleAny handler = MessageDispatcherAny { - dispatcherAny = (\s m -> do - (r, s') <- runServer (handler m) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - (P.forward m) sid - return (s', Nothing) - ) -} - --- | The server callbacks -data LocalServer s = LocalServer { - initHandler :: InitHandler s, -- ^ initialization handler - handlers :: [MessageDispatcher s], - terminateHandler :: TerminateHandler s -- ^ termination handler - } - ----- | Default record ----- Starting point for creating new servers -defaultServer :: LocalServer s -defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - handlers = [], - terminateHandler = \_ -> return () -} - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc - where - proc = processServer initH terminateH hs s - initH = initHandler ls - terminateH = terminateHandler ls - hs = handlers ls - --- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls - link pid - return pid - --- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls - ref <- monitor pid - return (pid, ref) - --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do - a1 <- callAsync sid rq - waitTimeout a1 timeout - --- | Async call to a server -callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = async sid $ do - cid <- getSelfPid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - --- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do - cid <- getSelfPid - --say $ "Casting server " ++ show cid - send sid (CastMessage cid msg) - --- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do - --say $ "Stop server " ++ show sid - exit sid reason - --- | Get the server state -getState :: Server s s -getState = ST.get - --- | Put the server state -putState :: s -> Server s () -putState = ST.put - --- | Modify the server state -modifyState :: (s -> s) -> Server s () -modifyState = ST.modify - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- | server process -processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () -processServer initH terminateH dispatchers s = do - (ir, s') <- runServer initH s - P.catch (proc ir s') (exitHandler s') - where - proc ir s' = do - (tr, s'') <- runServer (processLoop dispatchers ir) s' - _ <- runServer (terminateH tr) s'' - return () - exitHandler s' e = do - let tr = TerminateReason $ show (e :: SomeException) - _ <- runServer (terminateH tr) s' - return () - --- | server loop -processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason -processLoop dispatchers ir = do - case ir of - InitOk t -> loop dispatchers t - InitStop r -> return $ TerminateReason r - where - loop ds t = do - msgM <- processReceive ds t - case msgM of - Nothing -> loop ds t - Just r -> return r - --- | -processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) -processReceive ds timeout = do - s <- getState - let ms = map (matchMessage s) ds - case timeout of - Infinity -> do - (s', r) <- lift $ receiveWait ms - putState s' - return r - Timeout t -> do - mayResult <- lift $ receiveTimeout (intervalToMs t) ms - case mayResult of - Just (s', r) -> do - putState s' - return r - Nothing -> do - --trace "Receive timed out ..." - return $ Just (TerminateReason "Receive timed out") - --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = lift . say $ msg - --- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a -lift :: Process a -> Server s a -lift p = Server $ ST.lift p - --- | -runServer :: Server s a -> s -> Process (a, s) -runServer server state = ST.runStateT (unServer server) state diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs deleted file mode 100644 index 768a3566..00000000 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ /dev/null @@ -1,52 +0,0 @@ --- | Common Entities used throughout -platform. --- NB: Please DO NOT use this module as a dumping ground. --- -module Control.Distributed.Platform.Internal.Primitives - ( spawnLinkLocal - , spawnMonitorLocal - , linkOnFailure - ) -where - -import Control.Distributed.Process -import Control.Concurrent (myThreadId, throwTo) -import Control.Monad (void) - --- | Node local version of 'Control.Distributed.Process.spawnLink'. --- Note that this is just the sequential composition of 'spawn' and 'link'. --- (The "Unified" semantics that underlies Cloud Haskell does not even support --- a synchronous link operation) -spawnLinkLocal :: Process () -> Process ProcessId -spawnLinkLocal p = do - pid <- spawnLocal p - link pid - return pid - --- | Like 'spawnLinkLocal', but monitor the spawned process -spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) -spawnMonitorLocal p = do - pid <- spawnLocal p - ref <- monitor pid - return (pid, ref) - --- | CH's 'link' primitive, unlike Erlang's, will trigger when the target --- process dies for any reason. linkOnFailure has semantics like Erlang's: --- it will trigger only when the target function dies abnormally. -linkOnFailure :: ProcessId -> Process () -linkOnFailure them = do - us <- getSelfPid - tid <- liftIO $ myThreadId - void $ spawnLocal $ do - callerRef <- monitor us - calleeRef <- monitor them - reason <- receiveWait [ - matchIf (\(ProcessMonitorNotification mRef _ _) -> - mRef == callerRef) -- nothing left to do - (\_ -> return DiedNormal) - , matchIf (\(ProcessMonitorNotification mRef' _ _) -> - mRef' == calleeRef) - (\(ProcessMonitorNotification _ _ r') -> return r') - ] - case reason of - DiedNormal -> return () - _ -> liftIO $ throwTo tid (ProcessLinkException us reason) diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs deleted file mode 100644 index a0721940..00000000 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ /dev/null @@ -1,38 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Types used throughout the Cloud Haskell framework --- -module Control.Distributed.Platform.Internal.Types ( - TimeUnit(..) - , TimeInterval(..) - , Timeout(..) - ) where - -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --- | Defines the time unit for a Timeout value -data TimeUnit = Hours | Minutes | Seconds | Millis - deriving (Typeable, Show) -$(derive makeBinary ''TimeUnit) - -data TimeInterval = TimeInterval TimeUnit Int - deriving (Typeable, Show) -$(derive makeBinary ''TimeInterval) - --- | Defines a Timeout value (and unit of measure) or --- sets it to infinity (no timeout) -data Timeout = Timeout TimeInterval | Infinity - deriving (Typeable, Show) -$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs deleted file mode 100644 index 036a582c..00000000 --- a/src/Control/Distributed/Platform/Timer.hs +++ /dev/null @@ -1,136 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - -module Control.Distributed.Platform.Timer - ( - TimerRef - , Tick(Tick) - , sleep - , sendAfter - , runAfter - , startTimer - , ticker - , periodically - , resetTimer - , cancelTimer - , flushTimer - ) where - -import Control.Distributed.Process -import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --- | an opaque reference to a timer -type TimerRef = ProcessId - --- | cancellation message sent to timers -data TimerConfig = Reset | Cancel - deriving (Typeable, Show) -$(derive makeBinary ''TimerConfig) - --- | represents a 'tick' event that timers can generate -data Tick = Tick - deriving (Typeable, Eq) -$(derive makeBinary ''Tick) - -data SleepingPill = SleepingPill - deriving (Typeable) -$(derive makeBinary ''SleepingPill) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | blocks the calling Process for the specified TimeInterval. Note that this --- function assumes that a blocking receive is the most efficient approach to --- acheiving this, so expect the runtime semantics (particularly with regards --- scheduling) to differ from threadDelay and/or operating system specific --- functions that offer the same results. -sleep :: TimeInterval -> Process () -sleep t = do - let ms = intervalToMs t - _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) - (\_ -> return ())] - return () - --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. -sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -sendAfter t pid msg = runAfter t (mkSender pid msg) - --- | runs the supplied process action(s) after `t' has elapsed -runAfter :: TimeInterval -> Process () -> Process TimerRef -runAfter t p = spawnLocal $ runTimer t p True - --- | starts a timer that repeatedly sends the supplied message to the destination --- process each time the specified time interval elapses. To stop messages from --- being sent in future, cancelTimer can be called. -startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -startTimer t pid msg = periodically t (mkSender pid msg) - --- | runs the supplied process action(s) repeatedly at intervals of `t' -periodically :: TimeInterval -> Process () -> Process TimerRef -periodically t p = spawnLocal $ runTimer t p False - --- | resets a running timer. Note: Cancelling a timer does not guarantee that --- a timer's messages are prevented from being delivered to the target process. --- Also note that resetting an ongoing timer (started using the `startTimer' or --- `periodically' functions) will only cause the current elapsed period to time --- out, after which the timer will continue running. To stop a long-running --- timer, you should use `cancelTimer' instead. -resetTimer :: TimerRef -> Process () -resetTimer = (flip send) Reset - -cancelTimer :: TimerRef -> Process () -cancelTimer = (flip send) Cancel - --- | cancels a running timer and flushes any viable timer messages from the --- process' message queue. This function should only be called by the process --- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () -flushTimer ref ignore t = do - mRef <- monitor ref - cancelTimer ref - -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - performFlush mRef t - return () - where performFlush mRef Infinity = receiveWait $ filters mRef - performFlush mRef (Timeout i) = - receiveTimeout (intervalToMs i) (filters mRef) >> return () - filters mRef = [ - matchIf (\x -> x == ignore) - (\_ -> return ()) - , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') - (\_ -> return ()) ] - --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' -ticker :: TimeInterval -> ProcessId -> Process TimerRef -ticker t pid = startTimer t pid Tick - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- runs the timer process -runTimer :: TimeInterval -> Process () -> Bool -> Process () -runTimer t proc cancelOnReset = do - cancel <- expectTimeout (intervalToMs t) - -- say $ "cancel = " ++ (show cancel) ++ "\n" - case cancel of - Nothing -> runProc cancelOnReset - Just Cancel -> return () - Just Reset -> if cancelOnReset then return () - else runTimer t proc cancelOnReset - where runProc True = proc - runProc False = proc >> runTimer t proc cancelOnReset - --- create a 'sender' action for dispatching `msg' to `pid' -mkSender :: (Serializable a) => ProcessId -> a -> Process () -mkSender pid msg = do - -- say "sending\n" - send pid msg diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Process/Async.hs similarity index 95% rename from src/Control/Distributed/Platform/Async.hs rename to src/Control/Distributed/Process/Async.hs index 0beadb1c..584cb631 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -1,12 +1,12 @@ {-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( +module Control.Distributed.Process.Platform.Async ( Async(), async, wait, waitTimeout ) where import Control.Concurrent.MVar -import Control.Distributed.Platform +import Control.Distributed.Process.Platform import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), finally, liftIO, diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 4eb50cbc..9aae5ad2 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -12,7 +12,7 @@ module GenServer.Counter( Timeout(..) ) where -import Control.Distributed.Platform.GenServer +import Control.Distributed.Process.Platform.GenServer import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index de5ade6c..4a5219d4 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -18,7 +18,7 @@ module GenServer.Kitty Timeout(..) ) where -import Control.Distributed.Platform.GenServer +import Control.Distributed.Process.Platform.GenServer import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 47b19328..c477888d 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -23,8 +23,8 @@ import Test.HUnit (Assertion) import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) -import Control.Distributed.Platform.GenServer -import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Process.Platform.GenServer +import Control.Distributed.Process.Platform.Internal.Types import GenServer.Counter import GenServer.Kitty From 7ca85ba16bd1a716a80b19934c3a5c9950c2492d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 17:27:18 +0000 Subject: [PATCH 0605/2357] Control.Distributed.Platform => Control.Distributed.Process.Platform --- distributed-process-platform.cabal | 20 +-- src/Control/Distributed/Platform.hs | 59 -------- src/Control/Distributed/Platform/Async.hs | 57 -------- .../Platform/Internal/Primitives.hs | 52 ------- .../Distributed/Platform/Internal/Types.hs | 38 ----- src/Control/Distributed/Platform/Timer.hs | 136 ------------------ .../{ => Process}/Platform/GenProcess.hs | 6 +- .../{ => Process}/Platform/GenServer.hs | 6 +- tests/GenServer/Counter.hs | 2 +- tests/GenServer/Kitty.hs | 2 +- tests/TestGenServer.hs | 4 +- 11 files changed, 20 insertions(+), 362 deletions(-) delete mode 100644 src/Control/Distributed/Platform.hs delete mode 100644 src/Control/Distributed/Platform/Async.hs delete mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs delete mode 100644 src/Control/Distributed/Platform/Internal/Types.hs delete mode 100644 src/Control/Distributed/Platform/Timer.hs rename src/Control/Distributed/{ => Process}/Platform/GenProcess.hs (98%) rename src/Control/Distributed/{ => Process}/Platform/GenServer.hs (98%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 31e70cc7..b1be4ca9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -34,11 +34,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform, - Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer, - Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Async + other-modules: Control.Distributed.Process.Platform.Internal.Primitives test-suite TimerTests type: exitcode-stdio-1.0 @@ -62,9 +62,9 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -91,8 +91,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs deleted file mode 100644 index d9be491d..00000000 --- a/src/Control/Distributed/Platform.hs +++ /dev/null @@ -1,59 +0,0 @@ --- | [Cloud Haskell Platform] --- -module Control.Distributed.Platform - ( - -- extra primitives - spawnLinkLocal - , spawnMonitorLocal - , linkOnFailure - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - , Timeout(..) - , TimeInterval - , TimeUnit - ) where - -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Internal.Primitives - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs deleted file mode 100644 index 0beadb1c..00000000 --- a/src/Control/Distributed/Platform/Async.hs +++ /dev/null @@ -1,57 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM - where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs deleted file mode 100644 index 768a3566..00000000 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ /dev/null @@ -1,52 +0,0 @@ --- | Common Entities used throughout -platform. --- NB: Please DO NOT use this module as a dumping ground. --- -module Control.Distributed.Platform.Internal.Primitives - ( spawnLinkLocal - , spawnMonitorLocal - , linkOnFailure - ) -where - -import Control.Distributed.Process -import Control.Concurrent (myThreadId, throwTo) -import Control.Monad (void) - --- | Node local version of 'Control.Distributed.Process.spawnLink'. --- Note that this is just the sequential composition of 'spawn' and 'link'. --- (The "Unified" semantics that underlies Cloud Haskell does not even support --- a synchronous link operation) -spawnLinkLocal :: Process () -> Process ProcessId -spawnLinkLocal p = do - pid <- spawnLocal p - link pid - return pid - --- | Like 'spawnLinkLocal', but monitor the spawned process -spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) -spawnMonitorLocal p = do - pid <- spawnLocal p - ref <- monitor pid - return (pid, ref) - --- | CH's 'link' primitive, unlike Erlang's, will trigger when the target --- process dies for any reason. linkOnFailure has semantics like Erlang's: --- it will trigger only when the target function dies abnormally. -linkOnFailure :: ProcessId -> Process () -linkOnFailure them = do - us <- getSelfPid - tid <- liftIO $ myThreadId - void $ spawnLocal $ do - callerRef <- monitor us - calleeRef <- monitor them - reason <- receiveWait [ - matchIf (\(ProcessMonitorNotification mRef _ _) -> - mRef == callerRef) -- nothing left to do - (\_ -> return DiedNormal) - , matchIf (\(ProcessMonitorNotification mRef' _ _) -> - mRef' == calleeRef) - (\(ProcessMonitorNotification _ _ r') -> return r') - ] - case reason of - DiedNormal -> return () - _ -> liftIO $ throwTo tid (ProcessLinkException us reason) diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs deleted file mode 100644 index a0721940..00000000 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ /dev/null @@ -1,38 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Types used throughout the Cloud Haskell framework --- -module Control.Distributed.Platform.Internal.Types ( - TimeUnit(..) - , TimeInterval(..) - , Timeout(..) - ) where - -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --- | Defines the time unit for a Timeout value -data TimeUnit = Hours | Minutes | Seconds | Millis - deriving (Typeable, Show) -$(derive makeBinary ''TimeUnit) - -data TimeInterval = TimeInterval TimeUnit Int - deriving (Typeable, Show) -$(derive makeBinary ''TimeInterval) - --- | Defines a Timeout value (and unit of measure) or --- sets it to infinity (no timeout) -data Timeout = Timeout TimeInterval | Infinity - deriving (Typeable, Show) -$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs deleted file mode 100644 index 036a582c..00000000 --- a/src/Control/Distributed/Platform/Timer.hs +++ /dev/null @@ -1,136 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - -module Control.Distributed.Platform.Timer - ( - TimerRef - , Tick(Tick) - , sleep - , sendAfter - , runAfter - , startTimer - , ticker - , periodically - , resetTimer - , cancelTimer - , flushTimer - ) where - -import Control.Distributed.Process -import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --- | an opaque reference to a timer -type TimerRef = ProcessId - --- | cancellation message sent to timers -data TimerConfig = Reset | Cancel - deriving (Typeable, Show) -$(derive makeBinary ''TimerConfig) - --- | represents a 'tick' event that timers can generate -data Tick = Tick - deriving (Typeable, Eq) -$(derive makeBinary ''Tick) - -data SleepingPill = SleepingPill - deriving (Typeable) -$(derive makeBinary ''SleepingPill) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | blocks the calling Process for the specified TimeInterval. Note that this --- function assumes that a blocking receive is the most efficient approach to --- acheiving this, so expect the runtime semantics (particularly with regards --- scheduling) to differ from threadDelay and/or operating system specific --- functions that offer the same results. -sleep :: TimeInterval -> Process () -sleep t = do - let ms = intervalToMs t - _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) - (\_ -> return ())] - return () - --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. -sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -sendAfter t pid msg = runAfter t (mkSender pid msg) - --- | runs the supplied process action(s) after `t' has elapsed -runAfter :: TimeInterval -> Process () -> Process TimerRef -runAfter t p = spawnLocal $ runTimer t p True - --- | starts a timer that repeatedly sends the supplied message to the destination --- process each time the specified time interval elapses. To stop messages from --- being sent in future, cancelTimer can be called. -startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -startTimer t pid msg = periodically t (mkSender pid msg) - --- | runs the supplied process action(s) repeatedly at intervals of `t' -periodically :: TimeInterval -> Process () -> Process TimerRef -periodically t p = spawnLocal $ runTimer t p False - --- | resets a running timer. Note: Cancelling a timer does not guarantee that --- a timer's messages are prevented from being delivered to the target process. --- Also note that resetting an ongoing timer (started using the `startTimer' or --- `periodically' functions) will only cause the current elapsed period to time --- out, after which the timer will continue running. To stop a long-running --- timer, you should use `cancelTimer' instead. -resetTimer :: TimerRef -> Process () -resetTimer = (flip send) Reset - -cancelTimer :: TimerRef -> Process () -cancelTimer = (flip send) Cancel - --- | cancels a running timer and flushes any viable timer messages from the --- process' message queue. This function should only be called by the process --- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () -flushTimer ref ignore t = do - mRef <- monitor ref - cancelTimer ref - -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - performFlush mRef t - return () - where performFlush mRef Infinity = receiveWait $ filters mRef - performFlush mRef (Timeout i) = - receiveTimeout (intervalToMs i) (filters mRef) >> return () - filters mRef = [ - matchIf (\x -> x == ignore) - (\_ -> return ()) - , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') - (\_ -> return ()) ] - --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' -ticker :: TimeInterval -> ProcessId -> Process TimerRef -ticker t pid = startTimer t pid Tick - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- runs the timer process -runTimer :: TimeInterval -> Process () -> Bool -> Process () -runTimer t proc cancelOnReset = do - cancel <- expectTimeout (intervalToMs t) - -- say $ "cancel = " ++ (show cancel) ++ "\n" - case cancel of - Nothing -> runProc cancelOnReset - Just Cancel -> return () - Just Reset -> if cancelOnReset then return () - else runTimer t proc cancelOnReset - where runProc True = proc - runProc False = proc >> runTimer t proc cancelOnReset - --- create a 'sender' action for dispatching `msg' to `pid' -mkSender :: (Serializable a) => ProcessId -> a -> Process () -mkSender pid msg = do - -- say "sending\n" - send pid msg diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs similarity index 98% rename from src/Control/Distributed/Platform/GenProcess.hs rename to src/Control/Distributed/Process/Platform/GenProcess.hs index 79845f90..f285a865 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -9,7 +9,7 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Control.Distributed.Platform.GenProcess where +module Control.Distributed.Process.Platform.GenProcess where -- TODO: define API and hide internals... @@ -19,8 +19,8 @@ import qualified Control.Monad.State as ST (StateT, get, put, runStateT) import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform +import Control.Distributed.Process.Platform.Internal.Types +import Control.Distributed.Process.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs similarity index 98% rename from src/Control/Distributed/Platform/GenServer.hs rename to src/Control/Distributed/Process/Platform/GenServer.hs index b8b91710..c1890baa 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -8,7 +8,7 @@ {-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer -module Control.Distributed.Platform.GenServer ( +module Control.Distributed.Process.Platform.GenServer ( ServerId, Timeout(..), initOk, @@ -71,8 +71,8 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform -import Control.Distributed.Platform.Async +import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) import Data.Maybe (fromJust) diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 4eb50cbc..9aae5ad2 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -12,7 +12,7 @@ module GenServer.Counter( Timeout(..) ) where -import Control.Distributed.Platform.GenServer +import Control.Distributed.Process.Platform.GenServer import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index de5ade6c..4a5219d4 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -18,7 +18,7 @@ module GenServer.Kitty Timeout(..) ) where -import Control.Distributed.Platform.GenServer +import Control.Distributed.Process.Platform.GenServer import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 47b19328..c477888d 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -23,8 +23,8 @@ import Test.HUnit (Assertion) import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) -import Control.Distributed.Platform.GenServer -import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Process.Platform.GenServer +import Control.Distributed.Process.Platform.Internal.Types import GenServer.Counter import GenServer.Kitty From 8ee8229dbb4c44ebcf84a6dbb496b9f5160fc976 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 17:27:18 +0000 Subject: [PATCH 0606/2357] Control.Distributed.Platform => Control.Distributed.Process.Platform --- distributed-process-platform.cabal | 20 +- src/Control/Distributed/Platform.hs | 59 --- src/Control/Distributed/Platform/Async.hs | 57 --- .../Distributed/Platform/GenProcess.hs | 244 ------------ src/Control/Distributed/Platform/GenServer.hs | 366 ------------------ .../Platform/Internal/Primitives.hs | 52 --- .../Distributed/Platform/Internal/Types.hs | 38 -- src/Control/Distributed/Platform/Timer.hs | 136 ------- tests/GenServer/Counter.hs | 2 +- tests/GenServer/Kitty.hs | 2 +- tests/TestGenServer.hs | 4 +- 11 files changed, 14 insertions(+), 966 deletions(-) delete mode 100644 src/Control/Distributed/Platform.hs delete mode 100644 src/Control/Distributed/Platform/Async.hs delete mode 100644 src/Control/Distributed/Platform/GenProcess.hs delete mode 100644 src/Control/Distributed/Platform/GenServer.hs delete mode 100644 src/Control/Distributed/Platform/Internal/Primitives.hs delete mode 100644 src/Control/Distributed/Platform/Internal/Types.hs delete mode 100644 src/Control/Distributed/Platform/Timer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 31e70cc7..b1be4ca9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -34,11 +34,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform, - Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer, - Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Async + other-modules: Control.Distributed.Process.Platform.Internal.Primitives test-suite TimerTests type: exitcode-stdio-1.0 @@ -62,9 +62,9 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -91,8 +91,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Platform.hs deleted file mode 100644 index d9be491d..00000000 --- a/src/Control/Distributed/Platform.hs +++ /dev/null @@ -1,59 +0,0 @@ --- | [Cloud Haskell Platform] --- -module Control.Distributed.Platform - ( - -- extra primitives - spawnLinkLocal - , spawnMonitorLocal - , linkOnFailure - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - , Timeout(..) - , TimeInterval - , TimeUnit - ) where - -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Internal.Primitives - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs deleted file mode 100644 index 0beadb1c..00000000 --- a/src/Control/Distributed/Platform/Async.hs +++ /dev/null @@ -1,57 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM - where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Platform/GenProcess.hs deleted file mode 100644 index 79845f90..00000000 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ /dev/null @@ -1,244 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Control.Distributed.Platform.GenProcess where - --- TODO: define API and hide internals... - -import qualified Control.Distributed.Process as BaseProcess -import qualified Control.Monad.State as ST (StateT, get, - lift, modify, - put, runStateT) - -import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - - -type ServerName = String -type ServerPid = BaseProcess.ProcessId - -data ServerId = ServerProcess ServerPid | NamedServer ServerName - -data Recipient a = SendToPid BaseProcess.ProcessId | - SendToPort (BaseProcess.SendPort a) - --- | Initialize handler result -data InitResult = - InitOk Timeout - | InitStop String - --- | Terminate reason -data TerminateReason = - TerminateNormal - | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - -data ReplyTo = ReplyTo BaseProcess.ProcessId | None - deriving (Typeable, Show) -$(derive makeBinary ''ReplyTo) - --- | The result of a call -data ProcessAction = - ProcessContinue - | ProcessTimeout Timeout - | ProcessStop String - deriving (Typeable) -$(derive makeBinary ''ProcessAction) - -type Process s = ST.StateT s BaseProcess.Process - --- | Handlers -type InitHandler s = Process s InitResult -type TerminateHandler s = TerminateReason -> Process s () -type RequestHandler s a = Message a -> Process s ProcessAction - --- | Contains the actual payload and possibly additional routing metadata -data Message a = Message ReplyTo a - deriving (Show, Typeable) -$(derive makeBinary ''Message) - -data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) - deriving (Typeable) -$(derive makeBinary ''Rpc) - --- | Dispatcher that knows how to dispatch messages to a handler -data Dispatcher s = - forall a . (Serializable a) => - Dispatch { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction) } | - forall a . (Serializable a) => - DispatchIf { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction), - condition :: s -> Message a -> Bool } - --- dispatching to implementation callbacks - --- | Matches messages using a dispatcher -class Dispatchable d where - matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) - --- | Matches messages to a MessageDispatcher -instance Dispatchable Dispatcher where - matchMessage s (Dispatch d ) = BaseProcess.match (d s) - matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) - - -data Behaviour s = Behaviour { - initHandler :: InitHandler s -- ^ initialization handler - , dispatchers :: [Dispatcher s] - , terminateHandler :: TerminateHandler s -- ^ termination handler - } - --- | Management message --- TODO is there a std way of terminating a process from another process? -data Termination = Terminate TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''Termination) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a new server and return it's id --- start :: Behaviour s -> Process ProcessId --- start handlers = spawnLocal $ runProcess handlers - -reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () -reply (ReplyTo pid) m = BaseProcess.send pid m -reply _ _ = return () - -replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> - BaseProcess.Process () -replyVia p m = BaseProcess.sendChan p m - --- | Given a state, behaviour specificiation and spawn function, --- starts a new server and return its id. The spawn function is typically --- one taken from "Control.Distributed.Process". --- see 'Control.Distributed.Process.spawn' --- 'Control.Distributed.Process.spawnLocal' --- 'Control.Distributed.Process.spawnLink' --- 'Control.Distributed.Process.spawnMonitor' --- 'Control.Distributed.Process.spawnSupervised' -start :: - s -> Behaviour s -> - (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> - BaseProcess.Process BaseProcess.ProcessId -start state handlers spawn = spawn $ do - _ <- ST.runStateT (runProc handlers) state - return () - -send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () -send s m = do - let msg = (Message None m) - case s of - ServerProcess pid -> BaseProcess.send pid msg - NamedServer name -> BaseProcess.nsend name msg - --- process request handling - -handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s -handleRequest = handleRequestIf (const True) - -handleRequestIf :: (Serializable a) => (a -> Bool) -> - RequestHandler s a -> Dispatcher s -handleRequestIf cond handler = DispatchIf { - dispatch = (\state m@(Message _ _) -> do - (r, s') <- ST.runStateT (handler m) state - return (s', r) - ), - condition = \_ (Message _ req) -> cond req -} - --- process state management - --- | gets the process state -getState :: Process s s -getState = ST.get - --- | sets the process state -putState :: s -> Process s () -putState = ST.put - --- | modifies the server state -modifyState :: (s -> s) -> Process s () -modifyState = ST.modify - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- | server process -runProc :: Behaviour s -> Process s () -runProc s = do - ir <- init s - tr <- case ir of - InitOk t -> do - trace $ "Server ready to receive messages!" - loop s t - InitStop r -> return (TerminateReason r) - terminate s tr - --- | initialize server -init :: Behaviour s -> Process s InitResult -init s = do - trace $ "Server initializing ... " - ir <- initHandler s - return ir - -loop :: Behaviour s -> Timeout -> Process s TerminateReason -loop s t = do - s' <- processReceive (dispatchers s) t - nextAction s s' - where nextAction :: Behaviour s -> ProcessAction -> - Process s TerminateReason - nextAction b ProcessContinue = loop b t - nextAction b (ProcessTimeout t') = loop b t' - nextAction _ (ProcessStop r) = return (TerminateReason r) - -processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction -processReceive ds timeout = do - s <- getState - let ms = map (matchMessage s) ds - -- TODO: should we drain the message queue to avoid selective receive here? - case timeout of - Infinity -> do - (s', r) <- ST.lift $ BaseProcess.receiveWait ms - putState s' - return r - Timeout t -> do - result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms - case result of - Just (s', r) -> do - putState s' - return r - Nothing -> do - return $ ProcessStop "timed out" - -terminate :: Behaviour s -> TerminateReason -> Process s () -terminate s reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler s) reason - --- | Log a trace message using the underlying Process's say -trace :: String -> Process s () -trace msg = ST.lift . BaseProcess.say $ msg - --- data Upgrade = ??? --- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may handle hot server-code loading quite easily... - diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Platform/GenServer.hs deleted file mode 100644 index b8b91710..00000000 --- a/src/Control/Distributed/Platform/GenServer.hs +++ /dev/null @@ -1,366 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Second iteration of GenServer -module Control.Distributed.Platform.GenServer ( - ServerId, - Timeout(..), - initOk, - initStop, - ok, - forward, - stop, - InitHandler, - Handler, - TerminateHandler, - MessageDispatcher(), - handle, - handleIf, - handleAny, - putState, - getState, - modifyState, - LocalServer(..), - defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - Async(), - call, - callTimeout, - callAsync, - wait, - waitTimeout, - Process, - trace - ) where - -import qualified Control.Distributed.Process as P (forward, catch) -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) - -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, - exit, getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) - -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform -import Control.Distributed.Platform.Async - -import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) -import Data.DeriveTH -import Data.Typeable (Typeable) - --------------------------------------------------------------------------------- --- Data Types -- --------------------------------------------------------------------------------- - --- | ServerId -type ServerId = ProcessId - --- | Server monad -newtype Server s a = Server { - unServer :: ST.StateT s Process a - } - deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) - --- | Initialize handler result -data InitResult - = InitOk Timeout - | InitStop String - -initOk :: Timeout -> Server s InitResult -initOk t = return (InitOk t) - -initStop :: String -> Server s InitResult -initStop reason = return (InitStop reason) - --- | Terminate reason -data TerminateReason - = TerminateNormal - | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - --- | The result of a call -data Result a - = Ok a - | Forward ServerId - | Stop a String - deriving (Show, Typeable) - -ok :: (Serializable a, Show a) => a -> Server s (Result a) -ok resp = return (Ok resp) - -forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) -forward sid = return (Forward sid) - -stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) -stop resp reason = return (Stop resp reason) - --- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type Handler s a b = a -> Server s (Result b) - --- | Adds routing metadata to the actual payload -data Message a = - CallMessage { msgFrom :: ProcessId, msgPayload :: a } - | CastMessage { msgFrom :: ProcessId, msgPayload :: a } - deriving (Show, Typeable) -$(derive makeBinary ''Message) - --- | Dispatcher that knows how to dispatch messages to a handler --- s The server state -data MessageDispatcher s = - forall a . (Serializable a) => MessageDispatcher { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) - } - | forall a . (Serializable a) => MessageDispatcherIf { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason), - dispatchIf :: s -> Message a -> Bool - } - | MessageDispatcherAny { - dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) - } - --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - --- | Matches messages to a MessageDispatcher -instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher d) = match (d s) - matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) - matchMessage s (MessageDispatcherAny d) = matchAny (d s) - --- | Constructs a call message dispatcher --- -handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s -handle = handleIf (const True) - -handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s -handleIf cond handler = MessageDispatcherIf { - dispatcher = (\s msg -> case msg of - CallMessage cid payload -> do - --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Ok resp -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Nothing) - Forward sid -> do - --say $ "Server FORWARD to: " ++ show sid - send sid msg - return (s', Nothing) - Stop resp reason -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do - --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - send sid msg - return (s', Nothing) - ), - dispatchIf = \_ msg -> cond (msgPayload msg) -} - --- | Constructs a dispatcher for any message --- Note that since we don't know the type of this message it assumes the protocol of a cast --- i.e. no reply's -handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s -handleAny handler = MessageDispatcherAny { - dispatcherAny = (\s m -> do - (r, s') <- runServer (handler m) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - (P.forward m) sid - return (s', Nothing) - ) -} - --- | The server callbacks -data LocalServer s = LocalServer { - initHandler :: InitHandler s, -- ^ initialization handler - handlers :: [MessageDispatcher s], - terminateHandler :: TerminateHandler s -- ^ termination handler - } - ----- | Default record ----- Starting point for creating new servers -defaultServer :: LocalServer s -defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - handlers = [], - terminateHandler = \_ -> return () -} - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc - where - proc = processServer initH terminateH hs s - initH = initHandler ls - terminateH = terminateHandler ls - hs = handlers ls - --- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls - link pid - return pid - --- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls - ref <- monitor pid - return (pid, ref) - --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do - a1 <- callAsync sid rq - waitTimeout a1 timeout - --- | Async call to a server -callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) -callAsync sid rq = async sid $ do - cid <- getSelfPid - --say $ "Calling server " ++ show cid ++ " - " ++ show rq - send sid (CallMessage cid rq) - --- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do - cid <- getSelfPid - --say $ "Casting server " ++ show cid - send sid (CastMessage cid msg) - --- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do - --say $ "Stop server " ++ show sid - exit sid reason - --- | Get the server state -getState :: Server s s -getState = ST.get - --- | Put the server state -putState :: s -> Server s () -putState = ST.put - --- | Modify the server state -modifyState :: (s -> s) -> Server s () -modifyState = ST.modify - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- | server process -processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () -processServer initH terminateH dispatchers s = do - (ir, s') <- runServer initH s - P.catch (proc ir s') (exitHandler s') - where - proc ir s' = do - (tr, s'') <- runServer (processLoop dispatchers ir) s' - _ <- runServer (terminateH tr) s'' - return () - exitHandler s' e = do - let tr = TerminateReason $ show (e :: SomeException) - _ <- runServer (terminateH tr) s' - return () - --- | server loop -processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason -processLoop dispatchers ir = do - case ir of - InitOk t -> loop dispatchers t - InitStop r -> return $ TerminateReason r - where - loop ds t = do - msgM <- processReceive ds t - case msgM of - Nothing -> loop ds t - Just r -> return r - --- | -processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) -processReceive ds timeout = do - s <- getState - let ms = map (matchMessage s) ds - case timeout of - Infinity -> do - (s', r) <- lift $ receiveWait ms - putState s' - return r - Timeout t -> do - mayResult <- lift $ receiveTimeout (intervalToMs t) ms - case mayResult of - Just (s', r) -> do - putState s' - return r - Nothing -> do - --trace "Receive timed out ..." - return $ Just (TerminateReason "Receive timed out") - --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = lift . say $ msg - --- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a -lift :: Process a -> Server s a -lift p = Server $ ST.lift p - --- | -runServer :: Server s a -> s -> Process (a, s) -runServer server state = ST.runStateT (unServer server) state diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Platform/Internal/Primitives.hs deleted file mode 100644 index 768a3566..00000000 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ /dev/null @@ -1,52 +0,0 @@ --- | Common Entities used throughout -platform. --- NB: Please DO NOT use this module as a dumping ground. --- -module Control.Distributed.Platform.Internal.Primitives - ( spawnLinkLocal - , spawnMonitorLocal - , linkOnFailure - ) -where - -import Control.Distributed.Process -import Control.Concurrent (myThreadId, throwTo) -import Control.Monad (void) - --- | Node local version of 'Control.Distributed.Process.spawnLink'. --- Note that this is just the sequential composition of 'spawn' and 'link'. --- (The "Unified" semantics that underlies Cloud Haskell does not even support --- a synchronous link operation) -spawnLinkLocal :: Process () -> Process ProcessId -spawnLinkLocal p = do - pid <- spawnLocal p - link pid - return pid - --- | Like 'spawnLinkLocal', but monitor the spawned process -spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef) -spawnMonitorLocal p = do - pid <- spawnLocal p - ref <- monitor pid - return (pid, ref) - --- | CH's 'link' primitive, unlike Erlang's, will trigger when the target --- process dies for any reason. linkOnFailure has semantics like Erlang's: --- it will trigger only when the target function dies abnormally. -linkOnFailure :: ProcessId -> Process () -linkOnFailure them = do - us <- getSelfPid - tid <- liftIO $ myThreadId - void $ spawnLocal $ do - callerRef <- monitor us - calleeRef <- monitor them - reason <- receiveWait [ - matchIf (\(ProcessMonitorNotification mRef _ _) -> - mRef == callerRef) -- nothing left to do - (\_ -> return DiedNormal) - , matchIf (\(ProcessMonitorNotification mRef' _ _) -> - mRef' == calleeRef) - (\(ProcessMonitorNotification _ _ r') -> return r') - ] - case reason of - DiedNormal -> return () - _ -> liftIO $ throwTo tid (ProcessLinkException us reason) diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Platform/Internal/Types.hs deleted file mode 100644 index a0721940..00000000 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ /dev/null @@ -1,38 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Types used throughout the Cloud Haskell framework --- -module Control.Distributed.Platform.Internal.Types ( - TimeUnit(..) - , TimeInterval(..) - , Timeout(..) - ) where - -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --- | Defines the time unit for a Timeout value -data TimeUnit = Hours | Minutes | Seconds | Millis - deriving (Typeable, Show) -$(derive makeBinary ''TimeUnit) - -data TimeInterval = TimeInterval TimeUnit Int - deriving (Typeable, Show) -$(derive makeBinary ''TimeInterval) - --- | Defines a Timeout value (and unit of measure) or --- sets it to infinity (no timeout) -data Timeout = Timeout TimeInterval | Infinity - deriving (Typeable, Show) -$(derive makeBinary ''Timeout) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Platform/Timer.hs deleted file mode 100644 index 036a582c..00000000 --- a/src/Control/Distributed/Platform/Timer.hs +++ /dev/null @@ -1,136 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} - -module Control.Distributed.Platform.Timer - ( - TimerRef - , Tick(Tick) - , sleep - , sendAfter - , runAfter - , startTimer - , ticker - , periodically - , resetTimer - , cancelTimer - , flushTimer - ) where - -import Control.Distributed.Process -import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --- | an opaque reference to a timer -type TimerRef = ProcessId - --- | cancellation message sent to timers -data TimerConfig = Reset | Cancel - deriving (Typeable, Show) -$(derive makeBinary ''TimerConfig) - --- | represents a 'tick' event that timers can generate -data Tick = Tick - deriving (Typeable, Eq) -$(derive makeBinary ''Tick) - -data SleepingPill = SleepingPill - deriving (Typeable) -$(derive makeBinary ''SleepingPill) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | blocks the calling Process for the specified TimeInterval. Note that this --- function assumes that a blocking receive is the most efficient approach to --- acheiving this, so expect the runtime semantics (particularly with regards --- scheduling) to differ from threadDelay and/or operating system specific --- functions that offer the same results. -sleep :: TimeInterval -> Process () -sleep t = do - let ms = intervalToMs t - _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) - (\_ -> return ())] - return () - --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. -sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -sendAfter t pid msg = runAfter t (mkSender pid msg) - --- | runs the supplied process action(s) after `t' has elapsed -runAfter :: TimeInterval -> Process () -> Process TimerRef -runAfter t p = spawnLocal $ runTimer t p True - --- | starts a timer that repeatedly sends the supplied message to the destination --- process each time the specified time interval elapses. To stop messages from --- being sent in future, cancelTimer can be called. -startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -startTimer t pid msg = periodically t (mkSender pid msg) - --- | runs the supplied process action(s) repeatedly at intervals of `t' -periodically :: TimeInterval -> Process () -> Process TimerRef -periodically t p = spawnLocal $ runTimer t p False - --- | resets a running timer. Note: Cancelling a timer does not guarantee that --- a timer's messages are prevented from being delivered to the target process. --- Also note that resetting an ongoing timer (started using the `startTimer' or --- `periodically' functions) will only cause the current elapsed period to time --- out, after which the timer will continue running. To stop a long-running --- timer, you should use `cancelTimer' instead. -resetTimer :: TimerRef -> Process () -resetTimer = (flip send) Reset - -cancelTimer :: TimerRef -> Process () -cancelTimer = (flip send) Cancel - --- | cancels a running timer and flushes any viable timer messages from the --- process' message queue. This function should only be called by the process --- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () -flushTimer ref ignore t = do - mRef <- monitor ref - cancelTimer ref - -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing - performFlush mRef t - return () - where performFlush mRef Infinity = receiveWait $ filters mRef - performFlush mRef (Timeout i) = - receiveTimeout (intervalToMs i) (filters mRef) >> return () - filters mRef = [ - matchIf (\x -> x == ignore) - (\_ -> return ()) - , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') - (\_ -> return ()) ] - --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' -ticker :: TimeInterval -> ProcessId -> Process TimerRef -ticker t pid = startTimer t pid Tick - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- runs the timer process -runTimer :: TimeInterval -> Process () -> Bool -> Process () -runTimer t proc cancelOnReset = do - cancel <- expectTimeout (intervalToMs t) - -- say $ "cancel = " ++ (show cancel) ++ "\n" - case cancel of - Nothing -> runProc cancelOnReset - Just Cancel -> return () - Just Reset -> if cancelOnReset then return () - else runTimer t proc cancelOnReset - where runProc True = proc - runProc False = proc >> runTimer t proc cancelOnReset - --- create a 'sender' action for dispatching `msg' to `pid' -mkSender :: (Serializable a) => ProcessId -> a -> Process () -mkSender pid msg = do - -- say "sending\n" - send pid msg diff --git a/tests/GenServer/Counter.hs b/tests/GenServer/Counter.hs index 4eb50cbc..9aae5ad2 100644 --- a/tests/GenServer/Counter.hs +++ b/tests/GenServer/Counter.hs @@ -12,7 +12,7 @@ module GenServer.Counter( Timeout(..) ) where -import Control.Distributed.Platform.GenServer +import Control.Distributed.Process.Platform.GenServer import Data.Binary (Binary (..), getWord8, putWord8) diff --git a/tests/GenServer/Kitty.hs b/tests/GenServer/Kitty.hs index de5ade6c..4a5219d4 100644 --- a/tests/GenServer/Kitty.hs +++ b/tests/GenServer/Kitty.hs @@ -18,7 +18,7 @@ module GenServer.Kitty Timeout(..) ) where -import Control.Distributed.Platform.GenServer +import Control.Distributed.Process.Platform.GenServer import Data.Binary (Binary (..), getWord8, putWord8) import Data.DeriveTH diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 47b19328..c477888d 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -23,8 +23,8 @@ import Test.HUnit (Assertion) import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) -import Control.Distributed.Platform.GenServer -import Control.Distributed.Platform.Internal.Types +import Control.Distributed.Process.Platform.GenServer +import Control.Distributed.Process.Platform.Internal.Types import GenServer.Counter import GenServer.Kitty From c33b77af187850f2a150524af1af5f1ccdb9fe3c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 17:27:18 +0000 Subject: [PATCH 0607/2357] Control.Distributed.Platform => Control.Distributed.Process.Platform --- distributed-process-platform.cabal | 20 +++---- src/Control/Distributed/Platform/Async.hs | 57 ------------------- .../Distributed/{ => Process}/Platform.hs | 6 +- .../{ => Process}/Platform/GenProcess.hs | 6 +- .../{ => Process}/Platform/GenServer.hs | 6 +- .../Platform/Internal/Primitives.hs | 2 +- .../{ => Process}/Platform/Internal/Types.hs | 2 +- .../{ => Process}/Platform/Timer.hs | 6 +- 8 files changed, 24 insertions(+), 81 deletions(-) delete mode 100644 src/Control/Distributed/Platform/Async.hs rename src/Control/Distributed/{ => Process}/Platform.hs (90%) rename src/Control/Distributed/{ => Process}/Platform/GenProcess.hs (98%) rename src/Control/Distributed/{ => Process}/Platform/GenServer.hs (98%) rename src/Control/Distributed/{ => Process}/Platform/Internal/Primitives.hs (96%) rename src/Control/Distributed/{ => Process}/Platform/Internal/Types.hs (94%) rename src/Control/Distributed/{ => Process}/Platform/Timer.hs (97%) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 31e70cc7..b1be4ca9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -34,11 +34,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Control.Distributed.Platform, - Control.Distributed.Platform.GenServer, - Control.Distributed.Platform.Timer, - Control.Distributed.Platform.Async - other-modules: Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Async + other-modules: Control.Distributed.Process.Platform.Internal.Primitives test-suite TimerTests type: exitcode-stdio-1.0 @@ -62,9 +62,9 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -91,8 +91,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs deleted file mode 100644 index 0beadb1c..00000000 --- a/src/Control/Distributed/Platform/Async.hs +++ /dev/null @@ -1,57 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Control.Distributed.Platform.Async ( - Async(), - async, - wait, - waitTimeout - ) where -import Control.Concurrent.MVar -import Control.Distributed.Platform -import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), - finally, liftIO, - match, monitor, - receiveTimeout, - receiveWait, - unmonitor) -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Data.Maybe (fromMaybe) - - --- | Async data type -data Async a = Async MonitorRef (MVar a) - --- | -async :: (Serializable b) => ProcessId -> Process () -> Process (Async b) -async sid proc = do - ref <- monitor sid - proc - mvar <- liftIO newEmptyMVar - return $ Async ref mvar - --- | Wait for the call response -wait :: (Serializable a, Show a) => Async a -> Process a -wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") - --- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do - respM <- liftIO $ tryTakeMVar respMVar - case respM of - Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) - case respM' of - Just resp -> do - liftIO $ putMVar respMVar resp - return respM' - _ -> return respM' - _ -> return respM - where - receive to = case to of - Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches - matches = [ - match return, - match (\(ProcessMonitorNotification _ _ reason) -> - receiveTimeout 0 [match return] >>= return . fromMaybe (error $ "Server died: " ++ show reason))] diff --git a/src/Control/Distributed/Platform.hs b/src/Control/Distributed/Process/Platform.hs similarity index 90% rename from src/Control/Distributed/Platform.hs rename to src/Control/Distributed/Process/Platform.hs index d9be491d..7820ff86 100644 --- a/src/Control/Distributed/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -1,6 +1,6 @@ -- | [Cloud Haskell Platform] -- -module Control.Distributed.Platform +module Control.Distributed.Process.Platform ( -- extra primitives spawnLinkLocal @@ -18,8 +18,8 @@ module Control.Distributed.Platform , TimeUnit ) where -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform.Internal.Primitives +import Control.Distributed.Process.Platform.Internal.Types +import Control.Distributed.Process.Platform.Internal.Primitives -------------------------------------------------------------------------------- -- API -- diff --git a/src/Control/Distributed/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs similarity index 98% rename from src/Control/Distributed/Platform/GenProcess.hs rename to src/Control/Distributed/Process/Platform/GenProcess.hs index 79845f90..f285a865 100644 --- a/src/Control/Distributed/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -9,7 +9,7 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Control.Distributed.Platform.GenProcess where +module Control.Distributed.Process.Platform.GenProcess where -- TODO: define API and hide internals... @@ -19,8 +19,8 @@ import qualified Control.Monad.State as ST (StateT, get, put, runStateT) import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform +import Control.Distributed.Process.Platform.Internal.Types +import Control.Distributed.Process.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) diff --git a/src/Control/Distributed/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs similarity index 98% rename from src/Control/Distributed/Platform/GenServer.hs rename to src/Control/Distributed/Process/Platform/GenServer.hs index b8b91710..c1890baa 100644 --- a/src/Control/Distributed/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -8,7 +8,7 @@ {-# LANGUAGE TypeFamilies #-} -- | Second iteration of GenServer -module Control.Distributed.Platform.GenServer ( +module Control.Distributed.Process.Platform.GenServer ( ServerId, Timeout(..), initOk, @@ -71,8 +71,8 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Platform -import Control.Distributed.Platform.Async +import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) import Data.Maybe (fromJust) diff --git a/src/Control/Distributed/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs similarity index 96% rename from src/Control/Distributed/Platform/Internal/Primitives.hs rename to src/Control/Distributed/Process/Platform/Internal/Primitives.hs index 768a3566..940c3bfe 100644 --- a/src/Control/Distributed/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -1,7 +1,7 @@ -- | Common Entities used throughout -platform. -- NB: Please DO NOT use this module as a dumping ground. -- -module Control.Distributed.Platform.Internal.Primitives +module Control.Distributed.Process.Platform.Internal.Primitives ( spawnLinkLocal , spawnMonitorLocal , linkOnFailure diff --git a/src/Control/Distributed/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs similarity index 94% rename from src/Control/Distributed/Platform/Internal/Types.hs rename to src/Control/Distributed/Process/Platform/Internal/Types.hs index a0721940..26ce99b1 100644 --- a/src/Control/Distributed/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -11,7 +11,7 @@ -- | Types used throughout the Cloud Haskell framework -- -module Control.Distributed.Platform.Internal.Types ( +module Control.Distributed.Process.Platform.Internal.Types ( TimeUnit(..) , TimeInterval(..) , Timeout(..) diff --git a/src/Control/Distributed/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs similarity index 97% rename from src/Control/Distributed/Platform/Timer.hs rename to src/Control/Distributed/Process/Platform/Timer.hs index 036a582c..ab62e4ea 100644 --- a/src/Control/Distributed/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -1,7 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Platform.Timer +module Control.Distributed.Process.Platform.Timer ( TimerRef , Tick(Tick) @@ -18,8 +18,8 @@ module Control.Distributed.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable -import Control.Distributed.Platform.Internal.Types -import Control.Distributed.Platform +import Control.Distributed.Process.Platform.Internal.Types +import Control.Distributed.Process.Platform import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) From 7837ff2abf7441d64db7a5be0d6753b0ea4e7e62 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:04:11 +0000 Subject: [PATCH 0608/2357] synch license wording with d-p-global --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index bee23260..005cb4be 100644 --- a/LICENCE +++ b/LICENCE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the owner nor the names of other + * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. From d545db906af89d6991ac5d79c7e62d0e5518a875 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:04:11 +0000 Subject: [PATCH 0609/2357] synch license wording with d-p-global --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index bee23260..005cb4be 100644 --- a/LICENCE +++ b/LICENCE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the owner nor the names of other + * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. From b5c4757b077e8e31f5baf7dec5843aa702a4a851 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:04:11 +0000 Subject: [PATCH 0610/2357] synch license wording with d-p-global --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index bee23260..005cb4be 100644 --- a/LICENCE +++ b/LICENCE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the owner nor the names of other + * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. From df8a34e75730563cdb71b598dfa22a4fd4b28d70 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:04:11 +0000 Subject: [PATCH 0611/2357] synch license wording with d-p-global --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index bee23260..005cb4be 100644 --- a/LICENCE +++ b/LICENCE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the owner nor the names of other + * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. From a3313fa66f33a3f87d9fc0dbcd227b8cd89bfc95 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:05:08 +0000 Subject: [PATCH 0612/2357] migrate Call.hs from d-p-global; introduces API changes to d-p-platform --- distributed-process-platform.cabal | 20 +- src/Control/Distributed/Process/Platform.hs | 54 +---- .../Distributed/Process/Platform/Call.hs | 211 ++++++++++++++++++ .../Distributed/Process/Platform/GenServer.hs | 15 +- .../Process/Platform/Internal/Types.hs | 55 ++--- .../Distributed/Process/Platform/Time.hs | 126 +++++++++++ .../Distributed/Process/Platform/Timer.hs | 7 +- 7 files changed, 392 insertions(+), 96 deletions(-) create mode 100644 src/Control/Distributed/Process/Platform/Call.hs create mode 100644 src/Control/Distributed/Process/Platform/Time.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..79298d1c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,6 +29,7 @@ library distributed-static, binary, mtl, + containers >= 0.4 && < 0.6, stm >= 2.3 && < 2.5, transformers hs-source-dirs: src @@ -38,7 +39,10 @@ library Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async - other-modules: Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time test-suite TimerTests type: exitcode-stdio-1.0 @@ -63,9 +67,10 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestTimer.hs @@ -91,9 +96,10 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 7820ff86..029c3e69 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -6,54 +6,14 @@ module Control.Distributed.Process.Platform spawnLinkLocal , spawnMonitorLocal , linkOnFailure - -- time interval handling - , milliseconds - , seconds - , minutes - , hours - , intervalToMs - , timeToMs - , Timeout(..) - , TimeInterval - , TimeUnit + + -- tags + , Tag + , TagPool + , newTagPool + , getTag + ) where import Control.Distributed.Process.Platform.Internal.Types import Control.Distributed.Process.Platform.Internal.Primitives - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- time interval/unit handling - --- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v - --- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis - --- | given a number, produces a @TimeInterval@ of seconds -seconds :: Int -> TimeInterval -seconds = TimeInterval Seconds - --- | given a number, produces a @TimeInterval@ of minutes -minutes :: Int -> TimeInterval -minutes = TimeInterval Minutes - --- | given a number, produces a @TimeInterval@ of hours -hours :: Int -> TimeInterval -hours = TimeInterval Hours - --- TODO: timeToMs is not exactly efficient and we need to scale it up to --- deal with days, months, years, etc - --- | converts the supplied @TimeUnit@ to milliseconds -timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - diff --git a/src/Control/Distributed/Process/Platform/Call.hs b/src/Control/Distributed/Process/Platform/Call.hs new file mode 100644 index 00000000..b98ed083 --- /dev/null +++ b/src/Control/Distributed/Process/Platform/Call.hs @@ -0,0 +1,211 @@ +{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.Call +-- Copyright : (c) Parallel Scientific (Jeff Epstein) 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainers : Jeff Epstein, Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a Remote Procedure Call (rpc) style facility. +-- Clients make synchronous calls to a running process (i.e., server) using the +-- 'callAt', 'callTimeout' and 'multicall' functions. Processes acting as the +-- server are constructed using Cloud Haskell's 'receive' family of primitives +-- and the 'callResponse' family of functions in this module. +----------------------------------------------------------------------------- + +module Control.Distributed.Process.Platform.Call + ( -- client API + callAt + , callTimeout + , multicall + -- server API + , callResponse + , callResponseIf + , callResponseDefer + , callResponseDeferIf + , callForward + , callResponseAsync + ) where + +import Control.Distributed.Process +import Control.Distributed.Process.Serializable (Serializable) +import Control.Monad (forM, forM_, join) +import Data.List (delete) +import qualified Data.Map as M +import Data.Maybe (listToMaybe) +import Data.Binary (Binary,get,put) +import Data.Typeable (Typeable) + +import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Time + +---------------------------------------------- +-- * Multicall +---------------------------------------------- + +-- | Sends a message of type a to the given process, to be handled by a corresponding +-- callResponse... function, which will send back a message of type b. +-- The tag is per-process unique identifier of the transaction. If the timeout expires +-- or the target process dies, Nothing will be returned. +callTimeout :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Timeout -> Process (Maybe b) +callTimeout pid msg tag time = + do res <- multicall [pid] msg tag time + return $ join (listToMaybe res) + +-- | Like 'callTimeout', but with no timeout. Returns Nothing if the target process dies. +callAt :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Process (Maybe b) +callAt pid msg tag = callTimeout pid msg tag infiniteWait + +-- | Like 'callTimeout', but sends the message to multiple recipients and collects the results. +multicall :: forall a b.(Serializable a, Serializable b) => [ProcessId] -> a -> Tag -> Timeout -> Process [Maybe b] +multicall nodes msg tag time = + do caller <- getSelfPid + reciever <- spawnLocal $ + do reciever_pid <- getSelfPid + mon_caller <- monitor caller + () <- expect + monitortags <- forM nodes monitor + forM_ nodes $ \node -> send node (Multicall, node, reciever_pid, tag, msg) + maybeTimeout time tag reciever_pid + results <- recv nodes monitortags mon_caller + send caller (MulticallResponse,tag,results) + mon_reciever <- monitor reciever + send reciever () + receiveWait + [ + matchIf (\(MulticallResponse,mtag,_) -> mtag == tag) + (\(MulticallResponse,_,val) -> return val), + matchIf (\(ProcessMonitorNotification ref _pid reason) -> ref == mon_reciever && reason /= DiedNormal) + (\_ -> error "multicall: unexpected termination of worker process") + ] + where + recv nodes monitortags mon_caller = + do + let + ordered [] _ = [] + ordered (x:xs) m = + M.lookup x m : ordered xs m + recv1 ([],_,results) = return results + recv1 (_,[],results) = return results + recv1 (nodesleft,monitortagsleft,results) = + receiveWait + [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mon_caller) + (\_ -> return Nothing), + matchIf (\(ProcessMonitorNotification ref pid reason) -> + ref `elem` monitortagsleft && pid `elem` nodesleft && reason /= DiedNormal) + (\(ProcessMonitorNotification ref pid _reason) -> + return $ Just (delete pid nodesleft, delete ref monitortagsleft, results)), + matchIf (\(MulticallResponse,mtag,_,_) -> mtag == tag) + (\(MulticallResponse,_,responder,msgx) -> + return $ Just (delete responder nodesleft, monitortagsleft, M.insert responder (msgx::b) results)), + matchIf (\(TimeoutNotification mtag) -> mtag == tag ) + (\_ -> return Nothing) + ] + >>= maybe (return results) recv1 + resultmap <- recv1 (nodes, monitortags, M.empty) :: Process (M.Map ProcessId b) + return $ ordered nodes resultmap + +data MulticallResponseType a = + MulticallAccept + | MulticallForward ProcessId a + | MulticallReject deriving Eq + +callResponseImpl :: (Serializable a,Serializable b) => (a -> MulticallResponseType c) -> + (a -> (b -> Process())-> Process c) -> Match c +callResponseImpl cond proc = + matchIf (\(Multicall,_responder,_,_,msg) -> + case cond msg of + MulticallReject -> False + _ -> True) + (\wholemsg@(Multicall,responder,sender,tag,msg) -> + case cond msg of + MulticallForward target ret -> -- TODO sender should get a ProcessMonitorNotification if target dies, or we should link target + do send target wholemsg + return ret + MulticallReject -> error "multicallResponseImpl: Indecisive condition" + MulticallAccept -> + let resultSender tosend = send sender (MulticallResponse,tag::Tag,responder::ProcessId, tosend) + in proc msg resultSender) + +-- | Produces a Match that can be used with the 'receiveWait' family of message-receiving functions. +-- callResponse will respond to a message of type a sent by 'callTimeout', and will respond with +-- a value of type b. +callResponse :: (Serializable a,Serializable b) => (a -> Process (b,c)) -> Match c +callResponse = + callResponseIf (const True) + +callResponseDeferIf :: (Serializable a,Serializable b) => (a -> Bool) -> (a -> (b -> Process())-> Process c) -> Match c +callResponseDeferIf cond = callResponseImpl (\msg -> if cond msg then MulticallAccept else MulticallReject) + +callResponseDefer :: (Serializable a,Serializable b) => (a -> (b -> Process())-> Process c) -> Match c +callResponseDefer = callResponseDeferIf (const True) + + +-- | Produces a Match that can be used with the 'receiveWait' family of message-receiving functions. +-- When calllForward receives a message of type from from 'callTimeout' (and similar), it will forward +-- the message to another process, who will be responsible for responding to it. It is the user's +-- responsibility to ensure that the forwarding process is linked to the destination process, so that if +-- it fails, the sender will be notified. +callForward :: Serializable a => (a -> (ProcessId, c)) -> Match c +callForward proc = + callResponseImpl + (\msg -> let (pid, ret) = proc msg + in MulticallForward pid ret ) + (\_ sender -> (sender::(() -> Process ())) `mention` error "multicallForward: Indecisive condition") + +-- | The message handling code is started in a separate thread. It's not automatically +-- linked to the calling thread, so if you want it to be terminated when the message +-- handling thread dies, you'll need to call link yourself. +callResponseAsync :: (Serializable a,Serializable b) => (a -> Maybe c) -> (a -> Process b) -> Match c +callResponseAsync cond proc = + callResponseImpl + (\msg -> + case cond msg of + Nothing -> MulticallReject + Just _ -> MulticallAccept) + (\msg sender -> + do _ <- spawnLocal $ -- TODO linkOnFailure to spawned procss + do val <- proc msg + sender val + case cond msg of + Nothing -> error "multicallResponseAsync: Indecisive condition" + Just ret -> return ret ) + +callResponseIf :: (Serializable a,Serializable b) => (a -> Bool) -> (a -> Process (b,c)) -> Match c +callResponseIf cond proc = + callResponseImpl + (\msg -> + case cond msg of + True -> MulticallAccept + False -> MulticallReject) + (\msg sender -> + do (tosend,toreturn) <- proc msg + sender tosend + return toreturn) + +maybeTimeout :: Timeout -> Tag -> ProcessId -> Process () +maybeTimeout Nothing _ _ = return () +maybeTimeout (Just time) tag p = timeout time tag p + +---------------------------------------------- +-- * Private types +---------------------------------------------- + +mention :: a -> b -> b +mention _a b = b + +data Multicall = Multicall + deriving (Typeable) +instance Binary Multicall where + get = return Multicall + put _ = return () +data MulticallResponse = MulticallResponse + deriving (Typeable) +instance Binary MulticallResponse where + get = return MulticallResponse + put _ = return () + diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index c1890baa..9ede0c5c 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -72,6 +72,7 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) @@ -94,10 +95,10 @@ newtype Server s a = Server { -- | Initialize handler result data InitResult - = InitOk Timeout + = InitOk Delay | InitStop String -initOk :: Timeout -> Server s InitResult +initOk :: Delay -> Server s InitResult initOk t = return (InitOk t) initStop :: String -> Server s InitResult @@ -264,10 +265,10 @@ call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq - call sid rq = callTimeout sid Infinity rq >>= return . fromJust -- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do +callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Delay -> rq -> Process (Maybe rs) +callTimeout sid t rq = do a1 <- callAsync sid rq - waitTimeout a1 timeout + waitTimeout a1 t -- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) @@ -334,7 +335,7 @@ processLoop dispatchers ir = do Just r -> return r -- | -processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) +processReceive :: [MessageDispatcher s] -> Delay -> Server s (Maybe TerminateReason) processReceive ds timeout = do s <- getState let ms = map (matchMessage s) ds @@ -343,7 +344,7 @@ processReceive ds timeout = do (s', r) <- lift $ receiveWait ms putState s' return r - Timeout t -> do + Delay t -> do mayResult <- lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index 26ce99b1..fe1eb494 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -1,38 +1,31 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -- | Types used throughout the Cloud Haskell framework -- -module Control.Distributed.Process.Platform.Internal.Types ( - TimeUnit(..) - , TimeInterval(..) - , Timeout(..) +module Control.Distributed.Process.Platform.Internal.Types + ( Tag + , TagPool + , newTagPool + , getTag ) where -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) +import Control.Distributed.Process +import Control.Concurrent.MVar (MVar, newMVar, modifyMVar) + +-- | Tags provide uniqueness for messages, so that they can be +-- matched with their response. +type Tag = Int --- | Defines the time unit for a Timeout value -data TimeUnit = Hours | Minutes | Seconds | Millis - deriving (Typeable, Show) -$(derive makeBinary ''TimeUnit) +-- | Generates unique 'Tag' for messages and response pairs. +-- Each process that depends, directly or indirectly, on +-- the call mechanisms in "Control.Distributed.Process.Global.Call" +-- should have at most one TagPool on which to draw unique message +-- tags. +type TagPool = MVar Tag -data TimeInterval = TimeInterval TimeUnit Int - deriving (Typeable, Show) -$(derive makeBinary ''TimeInterval) +-- | Create a new per-process source of unique +-- message identifiers. +newTagPool :: Process TagPool +newTagPool = liftIO $ newMVar 0 --- | Defines a Timeout value (and unit of measure) or --- sets it to infinity (no timeout) -data Timeout = Timeout TimeInterval | Infinity - deriving (Typeable, Show) -$(derive makeBinary ''Timeout) +-- | Extract a new identifier from a 'TagPool'. +getTag :: TagPool -> Process Tag +getTag tp = liftIO $ modifyMVar tp (\tag -> return (tag+1,tag)) diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs new file mode 100644 index 00000000..b14df6f8 --- /dev/null +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -0,0 +1,126 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.Time +-- Copyright : (c) Tim Watson, Jeff Epstein +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainers : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides facilities for working with time delays and timeouts. +-- The type 'Timeout' and the 'timeout' family of functions provide mechanisms +-- for working with @threadDelay@-like behaviour operates on microsecond values. +-- +-- The 'TimeInterval' and 'TimeUnit' related functions provide an abstraction +-- for working with various time intervals and the 'Delay' type provides a +-- corrolary to 'timeout' that works with these. +----------------------------------------------------------------------------- + +module Control.Distributed.Process.Platform.Time + ( -- time interval handling + milliseconds + , seconds + , minutes + , hours + , intervalToMs + , timeToMs + , TimeInterval + , TimeUnit + , Delay(..) + + -- timeouts + , Timeout + , TimeoutNotification(..) + , timeout + , infiniteWait + , noWait + ) where + +import Control.Concurrent (threadDelay) +import Control.Distributed.Process +import Control.Distributed.Process.Platform.Internal.Types +import Control.Monad (void) +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Defines the time unit for a Timeout value +data TimeUnit = Days | Hours | Minutes | Seconds | Millis + deriving (Typeable, Show) +$(derive makeBinary ''TimeUnit) + +data TimeInterval = TimeInterval TimeUnit Int + deriving (Typeable, Show) +$(derive makeBinary ''TimeInterval) + +data Delay = Delay TimeInterval | Infinity + +-- | Represents a /timeout/ in terms of microseconds, where 'Nothing' stands for +-- infinity and @Just 0@, no-delay. +type Timeout = Maybe Int + +-- | Send to a process when a timeout expires. +data TimeoutNotification = TimeoutNotification Tag + deriving (Typeable) + +instance Binary TimeoutNotification where + get = fmap TimeoutNotification $ get + put (TimeoutNotification n) = put n + +-- time interval/unit handling + +-- | converts the supplied @TimeInterval@ to milliseconds +intervalToMs :: TimeInterval -> Int +intervalToMs (TimeInterval u v) = timeToMs u v + +-- | given a number, produces a @TimeInterval@ of milliseconds +milliseconds :: Int -> TimeInterval +milliseconds = TimeInterval Millis + +-- | given a number, produces a @TimeInterval@ of seconds +seconds :: Int -> TimeInterval +seconds = TimeInterval Seconds + +-- | given a number, produces a @TimeInterval@ of minutes +minutes :: Int -> TimeInterval +minutes = TimeInterval Minutes + +-- | given a number, produces a @TimeInterval@ of hours +hours :: Int -> TimeInterval +hours = TimeInterval Hours + +-- TODO: timeToMs is not exactly efficient and we may want to scale it up + +-- | converts the supplied @TimeUnit@ to milliseconds +timeToMs :: TimeUnit -> Int -> Int +timeToMs Millis ms = ms +timeToMs Seconds sec = sec * 1000 +timeToMs Minutes mins = (mins * 60) * 1000 +timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 +timeToMs Days days = (((days * 24) * 60) * 60) * 1000 + +-- timeouts/delays + +-- | Constructs an inifinite 'Timeout'. +infiniteWait :: Timeout +infiniteWait = Nothing + +-- | Constructs a no-wait 'Timeout' +noWait :: Timeout +noWait = Just 0 + +-- | Sends the calling process @TimeoutNotification tag@ after @time@ microseconds +timeout :: Int -> Tag -> ProcessId -> Process () +timeout time tag p = + void $ spawnLocal $ + do liftIO $ threadDelay time + send p (TimeoutNotification tag) + \ No newline at end of file diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index ab62e4ea..634f9f30 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -18,8 +18,7 @@ module Control.Distributed.Process.Platform.Timer import Control.Distributed.Process import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Internal.Types -import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -92,7 +91,7 @@ cancelTimer = (flip send) Cancel -- | cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Delay -> Process () flushTimer ref ignore t = do mRef <- monitor ref cancelTimer ref @@ -100,7 +99,7 @@ flushTimer ref ignore t = do performFlush mRef t return () where performFlush mRef Infinity = receiveWait $ filters mRef - performFlush mRef (Timeout i) = + performFlush mRef (Delay i) = receiveTimeout (intervalToMs i) (filters mRef) >> return () filters mRef = [ matchIf (\x -> x == ignore) From 307f0e42b847908e8a305c8b5fea26f60b63b76d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:05:08 +0000 Subject: [PATCH 0613/2357] migrate Call.hs from d-p-global; introduces API changes to d-p-platform --- distributed-process-platform.cabal | 20 +++++++++++++------- src/Control/Distributed/Process/Async.hs | 9 +++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..79298d1c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,6 +29,7 @@ library distributed-static, binary, mtl, + containers >= 0.4 && < 0.6, stm >= 2.3 && < 2.5, transformers hs-source-dirs: src @@ -38,7 +39,10 @@ library Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async - other-modules: Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time test-suite TimerTests type: exitcode-stdio-1.0 @@ -63,9 +67,10 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestTimer.hs @@ -91,9 +96,10 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 584cb631..ebf978d5 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -7,6 +7,7 @@ module Control.Distributed.Process.Platform.Async ( ) where import Control.Concurrent.MVar import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Time import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), finally, liftIO, @@ -35,12 +36,12 @@ wait :: (Serializable a, Show a) => Async a -> Process a wait a = waitTimeout a Infinity >>= return . fromMaybe (error "Receive wait timeout") -- | Wait for the call response given a timeout -waitTimeout :: (Serializable a, Show a) => Async a -> Timeout -> Process (Maybe a) -waitTimeout (Async ref respMVar) timeout = do +waitTimeout :: (Serializable a, Show a) => Async a -> Delay -> Process (Maybe a) +waitTimeout (Async ref respMVar) t = do respM <- liftIO $ tryTakeMVar respMVar case respM of Nothing -> do - respM' <- finally (receive timeout) (unmonitor ref) + respM' <- finally (receive t) (unmonitor ref) case respM' of Just resp -> do liftIO $ putMVar respMVar resp @@ -50,7 +51,7 @@ waitTimeout (Async ref respMVar) timeout = do where receive to = case to of Infinity -> receiveWait matches >>= return . Just - Timeout t -> receiveTimeout (intervalToMs t) matches + Delay t' -> receiveTimeout (intervalToMs t') matches matches = [ match return, match (\(ProcessMonitorNotification _ _ reason) -> From 635db112731678c7c9a5e6ff282946a37e335ab3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:05:08 +0000 Subject: [PATCH 0614/2357] migrate Call.hs from d-p-global; introduces API changes to d-p-platform --- distributed-process-platform.cabal | 20 ++++++++++++------- .../Distributed/Process/Platform/GenServer.hs | 15 +++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..79298d1c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,6 +29,7 @@ library distributed-static, binary, mtl, + containers >= 0.4 && < 0.6, stm >= 2.3 && < 2.5, transformers hs-source-dirs: src @@ -38,7 +39,10 @@ library Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async - other-modules: Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time test-suite TimerTests type: exitcode-stdio-1.0 @@ -63,9 +67,10 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestTimer.hs @@ -91,9 +96,10 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index c1890baa..9ede0c5c 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -72,6 +72,7 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Async import Data.Binary (Binary (..), getWord8, putWord8) @@ -94,10 +95,10 @@ newtype Server s a = Server { -- | Initialize handler result data InitResult - = InitOk Timeout + = InitOk Delay | InitStop String -initOk :: Timeout -> Server s InitResult +initOk :: Delay -> Server s InitResult initOk t = return (InitOk t) initStop :: String -> Server s InitResult @@ -264,10 +265,10 @@ call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq - call sid rq = callTimeout sid Infinity rq >>= return . fromJust -- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Timeout -> rq -> Process (Maybe rs) -callTimeout sid timeout rq = do +callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Delay -> rq -> Process (Maybe rs) +callTimeout sid t rq = do a1 <- callAsync sid rq - waitTimeout a1 timeout + waitTimeout a1 t -- | Async call to a server callAsync :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process (Async rs) @@ -334,7 +335,7 @@ processLoop dispatchers ir = do Just r -> return r -- | -processReceive :: [MessageDispatcher s] -> Timeout -> Server s (Maybe TerminateReason) +processReceive :: [MessageDispatcher s] -> Delay -> Server s (Maybe TerminateReason) processReceive ds timeout = do s <- getState let ms = map (matchMessage s) ds @@ -343,7 +344,7 @@ processReceive ds timeout = do (s', r) <- lift $ receiveWait ms putState s' return r - Timeout t -> do + Delay t -> do mayResult <- lift $ receiveTimeout (intervalToMs t) ms case mayResult of Just (s', r) -> do From 78f37fdffac8ae8a99c861500c2396133772ecee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:05:08 +0000 Subject: [PATCH 0615/2357] migrate Call.hs from d-p-global; introduces API changes to d-p-platform --- distributed-process-platform.cabal | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..79298d1c 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -29,6 +29,7 @@ library distributed-static, binary, mtl, + containers >= 0.4 && < 0.6, stm >= 2.3 && < 2.5, transformers hs-source-dirs: src @@ -38,7 +39,10 @@ library Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async - other-modules: Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time test-suite TimerTests type: exitcode-stdio-1.0 @@ -63,9 +67,10 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestTimer.hs @@ -91,9 +96,10 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives - TestUtils + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Internal.Primitives, + TestUtils, + Control.Distributed.Process.Platform.Time extensions: CPP main-is: TestPrimitives.hs From 1da7843f2042c1011e44279db6a8ec5ab6bf6eb2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:10:01 +0000 Subject: [PATCH 0616/2357] reduce compiler warnings --- .../Distributed/Process/Platform/GenServer.hs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index 9ede0c5c..3cd2e46a 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -10,7 +10,6 @@ -- | Second iteration of GenServer module Control.Distributed.Process.Platform.GenServer ( ServerId, - Timeout(..), initOk, initStop, ok, @@ -59,19 +58,16 @@ import Control.Distributed.Process (AbstractMessage, Match, Process, ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, + monitor, + link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) + send, spawnLocal) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Async @@ -336,16 +332,16 @@ processLoop dispatchers ir = do -- | processReceive :: [MessageDispatcher s] -> Delay -> Server s (Maybe TerminateReason) -processReceive ds timeout = do +processReceive ds t = do s <- getState let ms = map (matchMessage s) ds - case timeout of + case t of Infinity -> do (s', r) <- lift $ receiveWait ms putState s' return r - Delay t -> do - mayResult <- lift $ receiveTimeout (intervalToMs t) ms + Delay t' -> do + mayResult <- lift $ receiveTimeout (intervalToMs t') ms case mayResult of Just (s', r) -> do putState s' From f33607dea0b07d129b532d4c195f08145cfeaaa0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:10:01 +0000 Subject: [PATCH 0617/2357] reduce compiler warnings --- src/Control/Distributed/Process/Async.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index ebf978d5..3cb5acc7 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -6,7 +6,6 @@ module Control.Distributed.Process.Platform.Async ( waitTimeout ) where import Control.Concurrent.MVar -import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Time import Control.Distributed.Process (Process, ProcessId, ProcessMonitorNotification (..), From cb1e5e91a94905c284c1602792dfbdc37b596e74 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:10:01 +0000 Subject: [PATCH 0618/2357] reduce compiler warnings --- .../Distributed/Process/Platform/GenServer.hs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index 9ede0c5c..3cd2e46a 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -10,7 +10,6 @@ -- | Second iteration of GenServer module Control.Distributed.Process.Platform.GenServer ( ServerId, - Timeout(..), initOk, initStop, ok, @@ -59,19 +58,16 @@ import Control.Distributed.Process (AbstractMessage, Match, Process, ProcessId, - expectTimeout, - monitor, unmonitor, - link, finally, + monitor, + link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, receiveWait, say, - send, spawnLocal, - ProcessMonitorNotification(..)) + send, spawnLocal) import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Async @@ -336,16 +332,16 @@ processLoop dispatchers ir = do -- | processReceive :: [MessageDispatcher s] -> Delay -> Server s (Maybe TerminateReason) -processReceive ds timeout = do +processReceive ds t = do s <- getState let ms = map (matchMessage s) ds - case timeout of + case t of Infinity -> do (s', r) <- lift $ receiveWait ms putState s' return r - Delay t -> do - mayResult <- lift $ receiveTimeout (intervalToMs t) ms + Delay t' -> do + mayResult <- lift $ receiveTimeout (intervalToMs t') ms case mayResult of Just (s', r) -> do putState s' From 35525ba2a756abdc69642413c2d41554069245ce Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:38:20 +0000 Subject: [PATCH 0619/2357] migrati primitives from d-p-global --- src/Control/Distributed/Process/Platform.hs | 12 +- .../Process/Platform/Internal/Primitives.hs | 134 +++++++++++++++++- .../Process/Platform/Internal/Types.hs | 11 ++ 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 029c3e69..5d7afd7c 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -13,7 +13,17 @@ module Control.Distributed.Process.Platform , newTagPool , getTag + -- * Remote call table + , __remoteTable ) where +import Control.Distributed.Process import Control.Distributed.Process.Platform.Internal.Types -import Control.Distributed.Process.Platform.Internal.Primitives +import Control.Distributed.Process.Platform.Internal.Primitives hiding (__remoteTable) +import qualified Control.Distributed.Process.Platform.Internal.Primitives (__remoteTable) + +-- remote table + +__remoteTable :: RemoteTable -> RemoteTable +__remoteTable = + Control.Distributed.Process.Platform.Internal.Primitives.__remoteTable diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index 940c3bfe..39cd060f 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -1,16 +1,51 @@ --- | Common Entities used throughout -platform. +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +-- Common Entities used throughout -platform. -- NB: Please DO NOT use this module as a dumping ground. + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.Internal.Primitives +-- Copyright : (c) Tim Watson 2013, Parallel Scientific (Jeff Epstein) 2012 +-- License : BSD3 (see the file LICENSE) -- +-- Maintainers : Jeff Epstein, Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a set of additional primitives that add functionality +-- to the basic Cloud Haskell APIs. +----------------------------------------------------------------------------- + module Control.Distributed.Process.Platform.Internal.Primitives - ( spawnLinkLocal + ( -- spawning/linking + spawnLinkLocal , spawnMonitorLocal , linkOnFailure + + -- registration/start + , whereisOrStart + , whereisOrStartRemote + + -- matching + , matchCond + + -- remote table + , __remoteTable ) where -import Control.Distributed.Process import Control.Concurrent (myThreadId, throwTo) +import Control.Distributed.Process +import Control.Distributed.Process.Internal.Closure.BuiltIn (seqCP) +import Control.Distributed.Process.Closure (remotable, mkClosure) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Platform.Internal.Types import Control.Monad (void) +import Data.Maybe (isJust, fromJust) + +-- spawning, linking and generic server startup -- | Node local version of 'Control.Distributed.Process.spawnLink'. -- Note that this is just the sequential composition of 'spawn' and 'link'. @@ -50,3 +85,96 @@ linkOnFailure them = do case reason of DiedNormal -> return () _ -> liftIO $ throwTo tid (ProcessLinkException us reason) + +-- | Returns the pid of the process that has been registered +-- under the given name. This refers to a local, per-node registration, +-- not @global@ registration. If that name is unregistered, a process +-- is started. This is a handy way to start per-node named servers. +whereisOrStart :: String -> Process () -> Process ProcessId +whereisOrStart name proc = + do mpid <- whereis name + case mpid of + Just pid -> return pid + Nothing -> + do caller <- getSelfPid + pid <- spawnLocal $ + do self <- getSelfPid + register name self + send caller (RegisterSelf,self) + () <- expect + proc + ref <- monitor pid + ret <- receiveWait + [ matchIf (\(ProcessMonitorNotification aref _ _) -> ref == aref) + (\(ProcessMonitorNotification _ _ _) -> return Nothing), + matchIf (\(RegisterSelf,apid) -> apid == pid) + (\(RegisterSelf,_) -> return $ Just pid) + ] + case ret of + Nothing -> whereisOrStart name proc + Just somepid -> + do unmonitor ref + send somepid () + return somepid + +registerSelf :: (String, ProcessId) -> Process () +registerSelf (name,target) = + do self <- getSelfPid + register name self + send target (RegisterSelf, self) + () <- expect + return () + +$(remotable ['registerSelf]) + +-- | A remote equivalent of 'whereisOrStart'. It deals with the +-- node registry on the given node, and the process, if it needs to be started, +-- will run on that node. If the node is inaccessible, Nothing will be returned. +whereisOrStartRemote :: NodeId -> String -> Closure (Process ()) -> Process (Maybe ProcessId) +whereisOrStartRemote nid name proc = + do mRef <- monitorNode nid + whereisRemoteAsync nid name + res <- receiveWait + [ matchIf (\(WhereIsReply label _) -> label == name) + (\(WhereIsReply _ mPid) -> return (Just mPid)), + matchIf (\(NodeMonitorNotification aref _ _) -> aref == mRef) + (\(NodeMonitorNotification _ _ _) -> return Nothing) + ] + case res of + Nothing -> return Nothing + Just (Just pid) -> unmonitor mRef >> return (Just pid) + Just Nothing -> + do self <- getSelfPid + sRef <- spawnAsync nid ($(mkClosure 'registerSelf) (name,self) `seqCP` proc) + ret <- receiveWait [ + matchIf (\(NodeMonitorNotification ref _ _) -> ref == mRef) + (\(NodeMonitorNotification _ _ _) -> return Nothing), + matchIf (\(DidSpawn ref _) -> ref==sRef ) + (\(DidSpawn _ pid) -> + do pRef <- monitor pid + receiveWait + [ matchIf (\(RegisterSelf, apid) -> apid == pid) + (\(RegisterSelf, _) -> do unmonitor pRef + send pid () + return $ Just pid), + matchIf (\(NodeMonitorNotification aref _ _) -> aref == mRef) + (\(NodeMonitorNotification _aref _ _) -> return Nothing), + matchIf (\(ProcessMonitorNotification ref _ _) -> ref==pRef) + (\(ProcessMonitorNotification _ _ _) -> return Nothing) + ] ) + ] + unmonitor mRef + case ret of + Nothing -> whereisOrStartRemote nid name proc + Just pid -> return $ Just pid + +-- advanced messaging/matching + +-- | An alternative to 'matchIf' that allows both predicate and action +-- to be expressed in one parameter. +matchCond :: (Serializable a) => (a -> Maybe (Process b)) -> Match b +matchCond cond = + let v n = (isJust n, fromJust n) + res = v . cond + in matchIf (fst . res) (snd . res) + diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index fe1eb494..eb23e6ce 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE DeriveDataTypeable #-} + -- | Types used throughout the Cloud Haskell framework -- module Control.Distributed.Process.Platform.Internal.Types @@ -5,10 +7,19 @@ module Control.Distributed.Process.Platform.Internal.Types , TagPool , newTagPool , getTag + , RegisterSelf(..) ) where import Control.Distributed.Process import Control.Concurrent.MVar (MVar, newMVar, modifyMVar) +import Data.Binary (Binary,get,put) +import Data.Typeable (Typeable) + +-- | Used internally in whereisOrStart. Send as (RegisterSelf,ProcessId). +data RegisterSelf = RegisterSelf deriving Typeable +instance Binary RegisterSelf where + put _ = return () + get = return RegisterSelf -- | Tags provide uniqueness for messages, so that they can be -- matched with their response. From 601f65e995b15554e797fb5e4ec2e891d66dd680 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:48:41 +0000 Subject: [PATCH 0620/2357] be a bit clearer with comments/docs --- src/Control/Distributed/Process/Platform/Call.hs | 5 ++++- src/Control/Distributed/Process/Platform/Time.hs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Call.hs b/src/Control/Distributed/Process/Platform/Call.hs index b98ed083..476fc4d2 100644 --- a/src/Control/Distributed/Process/Platform/Call.hs +++ b/src/Control/Distributed/Process/Platform/Call.hs @@ -1,4 +1,5 @@ {-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} + ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.Platform.Call @@ -9,7 +10,9 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- This module provides a Remote Procedure Call (rpc) style facility. +-- This module provides a facility for Remote Procedure Call (rpc) style +-- interactions with Cloud Haskell processes. +-- -- Clients make synchronous calls to a running process (i.e., server) using the -- 'callAt', 'callTimeout' and 'multicall' functions. Processes acting as the -- server are constructed using Cloud Haskell's 'receive' family of primitives diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index b14df6f8..d7d7cea1 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -75,7 +75,7 @@ instance Binary TimeoutNotification where get = fmap TimeoutNotification $ get put (TimeoutNotification n) = put n --- time interval/unit handling +-- time interval/unit handling (milliseconds) -- | converts the supplied @TimeInterval@ to milliseconds intervalToMs :: TimeInterval -> Int @@ -107,7 +107,7 @@ timeToMs Minutes mins = (mins * 60) * 1000 timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 timeToMs Days days = (((days * 24) * 60) * 60) * 1000 --- timeouts/delays +-- timeouts/delays (microseconds) -- | Constructs an inifinite 'Timeout'. infiniteWait :: Timeout From 50e064e19c74e809f120ce565272e0ad7ed0ca6f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:49:05 +0000 Subject: [PATCH 0621/2357] that was nonsense - we don't need to refer to d-static explicitly --- distributed-process-platform.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 79298d1c..64c1844e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -26,7 +26,6 @@ library base >= 4, distributed-process, derive, - distributed-static, binary, mtl, containers >= 0.4 && < 0.6, From 865d8b635e3f0a6bc6d0bffb137e167aba75d3b8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:49:05 +0000 Subject: [PATCH 0622/2357] that was nonsense - we don't need to refer to d-static explicitly --- distributed-process-platform.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 79298d1c..64c1844e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -26,7 +26,6 @@ library base >= 4, distributed-process, derive, - distributed-static, binary, mtl, containers >= 0.4 && < 0.6, From d9240717d272247bb3852e4345b3cd14ca6cc7ce Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:49:05 +0000 Subject: [PATCH 0623/2357] that was nonsense - we don't need to refer to d-static explicitly --- distributed-process-platform.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 79298d1c..64c1844e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -26,7 +26,6 @@ library base >= 4, distributed-process, derive, - distributed-static, binary, mtl, containers >= 0.4 && < 0.6, From 02b22df73bf3fdb957a469ab441b0687e8ebd753 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 8 Jan 2013 23:49:05 +0000 Subject: [PATCH 0624/2357] that was nonsense - we don't need to refer to d-static explicitly --- distributed-process-platform.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 79298d1c..64c1844e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -26,7 +26,6 @@ library base >= 4, distributed-process, derive, - distributed-static, binary, mtl, containers >= 0.4 && < 0.6, From 64221cde8044c550afb22afd6ed1d147aff7906b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 00:22:43 +0000 Subject: [PATCH 0625/2357] integrate the tests for Call/Multicall --- distributed-process-platform.cabal | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64c1844e..3c85b83d 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -19,7 +19,7 @@ data-dir: "" source-repository head type: git - location: https://github.com/hyperthunk/distributed-process-platform + location: https://github.com/haskell-distributed/distributed-process-platform library build-depends: @@ -41,7 +41,8 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test test-suite TimerTests type: exitcode-stdio-1.0 @@ -51,6 +52,7 @@ test-suite TimerTests ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, + containers >= 0.4 && < 0.6, network-transport >= 0.3 && < 0.4, mtl, network-transport-tcp >= 0.3 && < 0.4, @@ -68,8 +70,9 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestTimer.hs @@ -83,6 +86,7 @@ test-suite PrimitivesTests derive, network-transport >= 0.3 && < 0.4, mtl, + containers >= 0.4 && < 0.6, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -97,8 +101,9 @@ test-suite PrimitivesTests other-modules: Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestPrimitives.hs From 03d75393ad9f27f06b54d5aeb06de625822a8580 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 00:22:43 +0000 Subject: [PATCH 0626/2357] integrate the tests for Call/Multicall --- distributed-process-platform.cabal | 13 +++++-- tests/TestUtils.hs | 61 +----------------------------- 2 files changed, 11 insertions(+), 63 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64c1844e..3c85b83d 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -19,7 +19,7 @@ data-dir: "" source-repository head type: git - location: https://github.com/hyperthunk/distributed-process-platform + location: https://github.com/haskell-distributed/distributed-process-platform library build-depends: @@ -41,7 +41,8 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test test-suite TimerTests type: exitcode-stdio-1.0 @@ -51,6 +52,7 @@ test-suite TimerTests ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, + containers >= 0.4 && < 0.6, network-transport >= 0.3 && < 0.4, mtl, network-transport-tcp >= 0.3 && < 0.4, @@ -68,8 +70,9 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestTimer.hs @@ -83,6 +86,7 @@ test-suite PrimitivesTests derive, network-transport >= 0.3 && < 0.4, mtl, + containers >= 0.4 && < 0.6, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -97,8 +101,9 @@ test-suite PrimitivesTests other-modules: Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestPrimitives.hs diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 17022abb..69b93f34 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -18,24 +18,20 @@ module TestUtils , delayedAssertion , assertComplete -- runners + , tryRunProcess , testMain ) where import Prelude hiding (catch) -import Data.Binary -import Data.Typeable (Typeable) -import Data.DeriveTH import Control.Concurrent.MVar ( MVar , newEmptyMVar - , putMVar , takeMVar ) import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() - -import Control.Monad (forever) +import Control.Distributed.Process.Platform.Test import Test.HUnit (Assertion) import Test.HUnit.Base (assertBool) @@ -43,51 +39,6 @@ import Test.Framework (Test, defaultMain) import Network.Transport.TCP import qualified Network.Transport as NT - --- | A mutable cell containing a test result. -type TestResult a = MVar a - --- | A simple @Ping@ signal -data Ping = Ping - deriving (Typeable, Eq, Show) -$(derive makeBinary ''Ping) - -ping :: ProcessId -> Process () -ping pid = send pid Ping - --- | Control signals used to manage /test processes/ -data TestProcessControl = Stop | Go | Report ProcessId - deriving (Typeable) -$(derive makeBinary ''TestProcessControl) - --- | Starts a test process on the local node. -startTestProcess :: Process () -> Process ProcessId -startTestProcess proc = spawnLocal $ runTestProcess proc - --- | Runs a /test process/ around the supplied @proc@, which is executed --- whenever the outer process loop receives a 'Go' signal. -runTestProcess :: Process () -> Process () -runTestProcess proc = forever $ do - ctl <- expect - case ctl of - Stop -> terminate - Go -> proc - Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () - --- | Tell a /test process/ to continue executing -testProcessGo :: ProcessId -> Process () -testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go - --- | Tell a /test process/ to stop (i.e., 'terminate') -testProcessStop :: ProcessId -> Process () -testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop - --- | Tell a /test process/ to send a report (message) --- back to the calling process -testProcessReport :: ProcessId -> Process () -testProcessReport pid = do - self <- getSelfPid - send pid $ Report self -- | Run the supplied @testProc@ using an @MVar@ to collect and assert -- against its result. Uses the supplied @note@ if the assertion fails. @@ -104,14 +55,6 @@ assertComplete msg mv a = do b <- takeMVar mv assertBool msg (a == b) --- | Does exactly what it says on the tin, doing so in the @Process@ monad. -noop :: Process () -noop = return () - --- | Stashes a value in our 'TestResult' using @putMVar@ -stash :: TestResult a -> a -> Process () -stash mvar x = liftIO $ putMVar mvar x - testMain :: (NT.Transport -> IO [Test]) -> IO () testMain builder = do Right (transport, _) <- createTransportExposeInternals From ab1c285f02bacf2181b6770092ad45b032c6d8a2 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 00:22:43 +0000 Subject: [PATCH 0627/2357] integrate the tests for Call/Multicall --- distributed-process-platform.cabal | 13 +++++-- tests/TestUtils.hs | 61 +----------------------------- 2 files changed, 11 insertions(+), 63 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64c1844e..3c85b83d 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -19,7 +19,7 @@ data-dir: "" source-repository head type: git - location: https://github.com/hyperthunk/distributed-process-platform + location: https://github.com/haskell-distributed/distributed-process-platform library build-depends: @@ -41,7 +41,8 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test test-suite TimerTests type: exitcode-stdio-1.0 @@ -51,6 +52,7 @@ test-suite TimerTests ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, + containers >= 0.4 && < 0.6, network-transport >= 0.3 && < 0.4, mtl, network-transport-tcp >= 0.3 && < 0.4, @@ -68,8 +70,9 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestTimer.hs @@ -83,6 +86,7 @@ test-suite PrimitivesTests derive, network-transport >= 0.3 && < 0.4, mtl, + containers >= 0.4 && < 0.6, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -97,8 +101,9 @@ test-suite PrimitivesTests other-modules: Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestPrimitives.hs diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 17022abb..69b93f34 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -18,24 +18,20 @@ module TestUtils , delayedAssertion , assertComplete -- runners + , tryRunProcess , testMain ) where import Prelude hiding (catch) -import Data.Binary -import Data.Typeable (Typeable) -import Data.DeriveTH import Control.Concurrent.MVar ( MVar , newEmptyMVar - , putMVar , takeMVar ) import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() - -import Control.Monad (forever) +import Control.Distributed.Process.Platform.Test import Test.HUnit (Assertion) import Test.HUnit.Base (assertBool) @@ -43,51 +39,6 @@ import Test.Framework (Test, defaultMain) import Network.Transport.TCP import qualified Network.Transport as NT - --- | A mutable cell containing a test result. -type TestResult a = MVar a - --- | A simple @Ping@ signal -data Ping = Ping - deriving (Typeable, Eq, Show) -$(derive makeBinary ''Ping) - -ping :: ProcessId -> Process () -ping pid = send pid Ping - --- | Control signals used to manage /test processes/ -data TestProcessControl = Stop | Go | Report ProcessId - deriving (Typeable) -$(derive makeBinary ''TestProcessControl) - --- | Starts a test process on the local node. -startTestProcess :: Process () -> Process ProcessId -startTestProcess proc = spawnLocal $ runTestProcess proc - --- | Runs a /test process/ around the supplied @proc@, which is executed --- whenever the outer process loop receives a 'Go' signal. -runTestProcess :: Process () -> Process () -runTestProcess proc = forever $ do - ctl <- expect - case ctl of - Stop -> terminate - Go -> proc - Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () - --- | Tell a /test process/ to continue executing -testProcessGo :: ProcessId -> Process () -testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go - --- | Tell a /test process/ to stop (i.e., 'terminate') -testProcessStop :: ProcessId -> Process () -testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop - --- | Tell a /test process/ to send a report (message) --- back to the calling process -testProcessReport :: ProcessId -> Process () -testProcessReport pid = do - self <- getSelfPid - send pid $ Report self -- | Run the supplied @testProc@ using an @MVar@ to collect and assert -- against its result. Uses the supplied @note@ if the assertion fails. @@ -104,14 +55,6 @@ assertComplete msg mv a = do b <- takeMVar mv assertBool msg (a == b) --- | Does exactly what it says on the tin, doing so in the @Process@ monad. -noop :: Process () -noop = return () - --- | Stashes a value in our 'TestResult' using @putMVar@ -stash :: TestResult a -> a -> Process () -stash mvar x = liftIO $ putMVar mvar x - testMain :: (NT.Transport -> IO [Test]) -> IO () testMain builder = do Right (transport, _) <- createTransportExposeInternals From b8b9dcb37e548055218d75df800762de0193b36a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 00:22:43 +0000 Subject: [PATCH 0628/2357] integrate the tests for Call/Multicall --- distributed-process-platform.cabal | 13 +- src/Control/Distributed/Process/Platform.hs | 9 +- .../Distributed/Process/Platform/Test.hs | 116 ++++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 src/Control/Distributed/Process/Platform/Test.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64c1844e..3c85b83d 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -19,7 +19,7 @@ data-dir: "" source-repository head type: git - location: https://github.com/hyperthunk/distributed-process-platform + location: https://github.com/haskell-distributed/distributed-process-platform library build-depends: @@ -41,7 +41,8 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test test-suite TimerTests type: exitcode-stdio-1.0 @@ -51,6 +52,7 @@ test-suite TimerTests ansi-terminal >= 0.5 && < 0.6, distributed-process, derive, + containers >= 0.4 && < 0.6, network-transport >= 0.3 && < 0.4, mtl, network-transport-tcp >= 0.3 && < 0.4, @@ -68,8 +70,9 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestTimer.hs @@ -83,6 +86,7 @@ test-suite PrimitivesTests derive, network-transport >= 0.3 && < 0.4, mtl, + containers >= 0.4 && < 0.6, network-transport-tcp >= 0.3 && < 0.4, binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, @@ -97,8 +101,9 @@ test-suite PrimitivesTests other-modules: Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Time + Control.Distributed.Process.Platform.Test extensions: CPP main-is: TestPrimitives.hs diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 5d7afd7c..78d97945 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -7,13 +7,20 @@ module Control.Distributed.Process.Platform , spawnMonitorLocal , linkOnFailure + -- registration/start + , whereisOrStart + , whereisOrStartRemote + + -- matching + , matchCond + -- tags , Tag , TagPool , newTagPool , getTag - -- * Remote call table + -- remote call table , __remoteTable ) where diff --git a/src/Control/Distributed/Process/Platform/Test.hs b/src/Control/Distributed/Process/Platform/Test.hs new file mode 100644 index 00000000..ec8f7c95 --- /dev/null +++ b/src/Control/Distributed/Process/Platform/Test.hs @@ -0,0 +1,116 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.Test +-- Copyright : (c) Tim Watson, Jeff Epstein 2013 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides basic building blocks for testing Cloud Haskell programs. +----------------------------------------------------------------------------- + +module Control.Distributed.Process.Platform.Test + ( TestResult + , noop + , stash + -- ping ! + , Ping(Ping) + , ping + -- test process utilities + , TestProcessControl + , startTestProcess + , runTestProcess + , testProcessGo + , testProcessStop + , testProcessReport + -- runners + , tryRunProcess + , tryForkProcess + ) where + +import Control.Concurrent + ( myThreadId + , throwTo + ) +import Control.Concurrent.MVar + ( MVar + , putMVar + ) +import Control.Distributed.Process +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() +import Control.Exception (SomeException) +import Control.Monad (forever) +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) +import Prelude hiding (catch) + + +-- | A mutable cell containing a test result. +type TestResult a = MVar a + +-- | A simple @Ping@ signal +data Ping = Ping + deriving (Typeable, Eq, Show) +$(derive makeBinary ''Ping) + +ping :: ProcessId -> Process () +ping pid = send pid Ping + +-- | Control signals used to manage /test processes/ +data TestProcessControl = Stop | Go | Report ProcessId + deriving (Typeable) +$(derive makeBinary ''TestProcessControl) + +-- | Starts a test process on the local node. +startTestProcess :: Process () -> Process ProcessId +startTestProcess proc = spawnLocal $ runTestProcess proc + +-- | Runs a /test process/ around the supplied @proc@, which is executed +-- whenever the outer process loop receives a 'Go' signal. +runTestProcess :: Process () -> Process () +runTestProcess proc = forever $ do + ctl <- expect + case ctl of + Stop -> terminate + Go -> proc + Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () + +-- | Tell a /test process/ to continue executing +testProcessGo :: ProcessId -> Process () +testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go + +-- | Tell a /test process/ to stop (i.e., 'terminate') +testProcessStop :: ProcessId -> Process () +testProcessStop pid = (say $ (show pid) ++ " stop!") >> send pid Stop + +-- | Tell a /test process/ to send a report (message) +-- back to the calling process +testProcessReport :: ProcessId -> Process () +testProcessReport pid = do + self <- getSelfPid + send pid $ Report self + +-- | Does exactly what it says on the tin, doing so in the @Process@ monad. +noop :: Process () +noop = return () + +-- | Stashes a value in our 'TestResult' using @putMVar@ +stash :: TestResult a -> a -> Process () +stash mvar x = liftIO $ putMVar mvar x + +tryRunProcess :: LocalNode -> Process () -> IO () +tryRunProcess node p = do + tid <- liftIO myThreadId + runProcess node $ catch p (\e -> liftIO $ throwTo tid (e::SomeException)) + +tryForkProcess :: LocalNode -> Process () -> IO ProcessId +tryForkProcess node p = do + tid <- liftIO myThreadId + forkProcess node $ catch p (\e -> liftIO $ throwTo tid (e::SomeException)) From 38b8236f45220e5a87e116eed7bca85f7d496b96 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 10:06:19 +0000 Subject: [PATCH 0629/2357] expose the right API modules --- distributed-process-platform.cabal | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..c8b842c8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,18 @@ library Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, +<<<<<<< Updated upstream Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives +======= + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types +>>>>>>> Stashed changes test-suite TimerTests type: exitcode-stdio-1.0 From 50d1db9f76bbacb6f35b19214a71191cb5ebe708 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 10:06:19 +0000 Subject: [PATCH 0630/2357] expose the right API modules --- distributed-process-platform.cabal | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..c8b842c8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,18 @@ library Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, +<<<<<<< Updated upstream Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives +======= + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types +>>>>>>> Stashed changes test-suite TimerTests type: exitcode-stdio-1.0 From f573d8ef80b1038879451a24c3b4da9bccb5fb13 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 10:06:19 +0000 Subject: [PATCH 0631/2357] expose the right API modules --- distributed-process-platform.cabal | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..c8b842c8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,18 @@ library Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, +<<<<<<< Updated upstream Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives +======= + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types +>>>>>>> Stashed changes test-suite TimerTests type: exitcode-stdio-1.0 From 5e2cfc45ac7d976d2db797d284926913b87812f8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 9 Jan 2013 10:06:19 +0000 Subject: [PATCH 0632/2357] expose the right API modules --- distributed-process-platform.cabal | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..c8b842c8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -37,8 +37,18 @@ library Control.Distributed.Process.Platform, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Timer, +<<<<<<< Updated upstream Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives +======= + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Test + other-modules: + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types +>>>>>>> Stashed changes test-suite TimerTests type: exitcode-stdio-1.0 From 0142c49a9d22343c7da7946b8b51f4828fe8f6ae Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Wed, 9 Jan 2013 09:39:15 -0500 Subject: [PATCH 0633/2357] clean up trailing whitespace This mostly removes file trailing and line trailing whitespace from the files. --- distributed-process-platform.cabal | 11 +++++------ src/Control/Distributed/Process/Async.hs | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..e81e8879 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -61,10 +61,10 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -90,10 +90,9 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: + other-modules: Control.Distributed.Process.Platform Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs - diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 584cb631..1b42b9b5 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -8,7 +8,8 @@ module Control.Distributed.Process.Platform.Async ( import Control.Concurrent.MVar import Control.Distributed.Process.Platform import Control.Distributed.Process (Process, - ProcessId, ProcessMonitorNotification (..), + ProcessId, + ProcessMonitorNotification (..), finally, liftIO, match, monitor, receiveTimeout, From 38c4ec570d1e86e0da27dd3a8985567f30ffa602 Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Wed, 9 Jan 2013 09:39:15 -0500 Subject: [PATCH 0634/2357] clean up trailing whitespace This mostly removes file trailing and line trailing whitespace from the files. --- distributed-process-platform.cabal | 11 +++++------ .../Distributed/Process/Platform/GenProcess.hs | 13 ++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..e81e8879 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -61,10 +61,10 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -90,10 +90,9 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: + other-modules: Control.Distributed.Process.Platform Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs - diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index f285a865..f90c33ad 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -74,7 +74,7 @@ $(derive makeBinary ''Message) data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) deriving (Typeable) -$(derive makeBinary ''Rpc) +$(derive makeBinary ''Rpc) -- | Dispatcher that knows how to dispatch messages to a handler data Dispatcher s = @@ -130,10 +130,10 @@ replyVia p m = BaseProcess.sendChan p m -- starts a new server and return its id. The spawn function is typically -- one taken from "Control.Distributed.Process". -- see 'Control.Distributed.Process.spawn' --- 'Control.Distributed.Process.spawnLocal' +-- 'Control.Distributed.Process.spawnLocal' -- 'Control.Distributed.Process.spawnLink' -- 'Control.Distributed.Process.spawnMonitor' --- 'Control.Distributed.Process.spawnSupervised' +-- 'Control.Distributed.Process.spawnSupervised' start :: s -> Behaviour s -> (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> @@ -146,7 +146,7 @@ send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () send s m = do let msg = (Message None m) case s of - ServerProcess pid -> BaseProcess.send pid msg + ServerProcess pid -> BaseProcess.send pid msg NamedServer name -> BaseProcess.nsend name msg -- process request handling @@ -203,12 +203,12 @@ init s = do loop :: Behaviour s -> Timeout -> Process s TerminateReason loop s t = do s' <- processReceive (dispatchers s) t - nextAction s s' + nextAction s s' where nextAction :: Behaviour s -> ProcessAction -> Process s TerminateReason nextAction b ProcessContinue = loop b t nextAction b (ProcessTimeout t') = loop b t' - nextAction _ (ProcessStop r) = return (TerminateReason r) + nextAction _ (ProcessStop r) = return (TerminateReason r) processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction processReceive ds timeout = do @@ -241,4 +241,3 @@ trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? -- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to -- a remote pid? if so then we may handle hot server-code loading quite easily... - From 3883c4714dc605d68cd43b78e1f9649c55afe0b5 Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Wed, 9 Jan 2013 09:39:15 -0500 Subject: [PATCH 0635/2357] clean up trailing whitespace This mostly removes file trailing and line trailing whitespace from the files. --- distributed-process-platform.cabal | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..e81e8879 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -61,10 +61,10 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -90,10 +90,9 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: + other-modules: Control.Distributed.Process.Platform Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs - From d1b5fba63ee4ab1a20f68918dddde26b3e0f7101 Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Wed, 9 Jan 2013 09:39:15 -0500 Subject: [PATCH 0636/2357] clean up trailing whitespace This mostly removes file trailing and line trailing whitespace from the files. --- distributed-process-platform.cabal | 11 +++++------ src/Control/Distributed/Process/Platform.hs | 1 - .../Distributed/Process/Platform/GenProcess.hs | 13 ++++++------- .../Distributed/Process/Platform/Internal/Types.hs | 2 +- src/Control/Distributed/Process/Platform/Timer.hs | 10 +++++----- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b1be4ca9..e81e8879 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -61,10 +61,10 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform - Control.Distributed.Process.Platform.Internal.Primitives + other-modules: + Control.Distributed.Platform.Timer, + Control.Distributed.Platform + Control.Distributed.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestTimer.hs @@ -90,10 +90,9 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: + other-modules: Control.Distributed.Process.Platform Control.Distributed.Process.Platform.Internal.Primitives TestUtils extensions: CPP main-is: TestPrimitives.hs - diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 7820ff86..24a36f51 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -56,4 +56,3 @@ timeToMs Millis ms = ms timeToMs Seconds sec = sec * 1000 timeToMs Minutes mins = (mins * 60) * 1000 timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 - diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index f285a865..f90c33ad 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -74,7 +74,7 @@ $(derive makeBinary ''Message) data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) deriving (Typeable) -$(derive makeBinary ''Rpc) +$(derive makeBinary ''Rpc) -- | Dispatcher that knows how to dispatch messages to a handler data Dispatcher s = @@ -130,10 +130,10 @@ replyVia p m = BaseProcess.sendChan p m -- starts a new server and return its id. The spawn function is typically -- one taken from "Control.Distributed.Process". -- see 'Control.Distributed.Process.spawn' --- 'Control.Distributed.Process.spawnLocal' +-- 'Control.Distributed.Process.spawnLocal' -- 'Control.Distributed.Process.spawnLink' -- 'Control.Distributed.Process.spawnMonitor' --- 'Control.Distributed.Process.spawnSupervised' +-- 'Control.Distributed.Process.spawnSupervised' start :: s -> Behaviour s -> (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> @@ -146,7 +146,7 @@ send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () send s m = do let msg = (Message None m) case s of - ServerProcess pid -> BaseProcess.send pid msg + ServerProcess pid -> BaseProcess.send pid msg NamedServer name -> BaseProcess.nsend name msg -- process request handling @@ -203,12 +203,12 @@ init s = do loop :: Behaviour s -> Timeout -> Process s TerminateReason loop s t = do s' <- processReceive (dispatchers s) t - nextAction s s' + nextAction s s' where nextAction :: Behaviour s -> ProcessAction -> Process s TerminateReason nextAction b ProcessContinue = loop b t nextAction b (ProcessTimeout t') = loop b t' - nextAction _ (ProcessStop r) = return (TerminateReason r) + nextAction _ (ProcessStop r) = return (TerminateReason r) processReceive :: [Dispatcher s] -> Timeout -> Process s ProcessAction processReceive ds timeout = do @@ -241,4 +241,3 @@ trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? -- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to -- a remote pid? if so then we may handle hot server-code loading quite easily... - diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index 26ce99b1..d0529e5b 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -14,7 +14,7 @@ module Control.Distributed.Process.Platform.Internal.Types ( TimeUnit(..) , TimeInterval(..) - , Timeout(..) + , Timeout(..) ) where import Data.Binary diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index ab62e4ea..f390e3b8 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -1,7 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Process.Platform.Timer +module Control.Distributed.Process.Platform.Timer ( TimerRef , Tick(Tick) @@ -49,7 +49,7 @@ $(derive makeBinary ''SleepingPill) -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to -- acheiving this, so expect the runtime semantics (particularly with regards --- scheduling) to differ from threadDelay and/or operating system specific +-- scheduling) to differ from threadDelay and/or operating system specific -- functions that offer the same results. sleep :: TimeInterval -> Process () sleep t = do @@ -63,9 +63,9 @@ sleep t = do sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef sendAfter t pid msg = runAfter t (mkSender pid msg) --- | runs the supplied process action(s) after `t' has elapsed +-- | runs the supplied process action(s) after `t' has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef -runAfter t p = spawnLocal $ runTimer t p True +runAfter t p = spawnLocal $ runTimer t p True -- | starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from @@ -92,7 +92,7 @@ cancelTimer = (flip send) Cancel -- | cancels a running timer and flushes any viable timer messages from the -- process' message queue. This function should only be called by the process -- expecting to receive the timer's messages! -flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () +flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Timeout -> Process () flushTimer ref ignore t = do mRef <- monitor ref cancelTimer ref From 5bce269580eed10eae3e953ecb9db81a2cbd652a Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:35:17 -0500 Subject: [PATCH 0637/2357] bump the test-framework constraints to enclude 0.8 --- distributed-process-platform.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e81e8879..d178d3c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -54,7 +54,7 @@ test-suite TimerTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: @@ -83,7 +83,7 @@ test-suite PrimitivesTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: From a5cadcc2ff7d51e43706d6abbe0b294ab3a89477 Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:35:17 -0500 Subject: [PATCH 0638/2357] bump the test-framework constraints to enclude 0.8 --- distributed-process-platform.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e81e8879..d178d3c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -54,7 +54,7 @@ test-suite TimerTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: @@ -83,7 +83,7 @@ test-suite PrimitivesTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: From 13864025ee7560dd66a7f6c5bfd462ffcd1e46d2 Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:35:17 -0500 Subject: [PATCH 0639/2357] bump the test-framework constraints to enclude 0.8 --- distributed-process-platform.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e81e8879..d178d3c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -54,7 +54,7 @@ test-suite TimerTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: @@ -83,7 +83,7 @@ test-suite PrimitivesTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: From 8ec67e5b0df77ffb8a082ac71f29bae1d8711c4d Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:35:17 -0500 Subject: [PATCH 0640/2357] bump the test-framework constraints to enclude 0.8 --- distributed-process-platform.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e81e8879..d178d3c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -54,7 +54,7 @@ test-suite TimerTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: @@ -83,7 +83,7 @@ test-suite PrimitivesTests binary >= 0.5 && < 0.7, network >= 2.3 && < 2.5, HUnit >= 1.2 && < 2, - test-framework >= 0.6 && < 0.7, + test-framework >= 0.6 && < 0.9, test-framework-hunit, transformers hs-source-dirs: From 5c895f28fb50347f91ad6e74767ba07674f29daf Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Fri, 4 Jan 2013 15:47:32 -0500 Subject: [PATCH 0641/2357] add mising 'Types' module to 'other-modules' in cabal --- distributed-process-platform.cabal | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d178d3c9..de4035c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -39,6 +39,7 @@ library Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -65,6 +66,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From acbfce375cdc1b9be5f9d0cfca4bd423f4b2eeaf Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Fri, 4 Jan 2013 15:47:32 -0500 Subject: [PATCH 0642/2357] add mising 'Types' module to 'other-modules' in cabal --- distributed-process-platform.cabal | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d178d3c9..de4035c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -39,6 +39,7 @@ library Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -65,6 +66,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From 631fff8fc71ca26df12716b3569275a9e73a2018 Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Fri, 4 Jan 2013 15:47:32 -0500 Subject: [PATCH 0643/2357] add mising 'Types' module to 'other-modules' in cabal --- distributed-process-platform.cabal | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d178d3c9..de4035c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -39,6 +39,7 @@ library Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -65,6 +66,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From 4e70113d8605f16dcffbadce9d40d2c448033caa Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Fri, 4 Jan 2013 15:47:32 -0500 Subject: [PATCH 0644/2357] add mising 'Types' module to 'other-modules' in cabal --- distributed-process-platform.cabal | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d178d3c9..de4035c9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -39,6 +39,7 @@ library Control.Distributed.Process.Platform.Timer, Control.Distributed.Process.Platform.Async other-modules: Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -65,6 +66,7 @@ test-suite TimerTests Control.Distributed.Platform.Timer, Control.Distributed.Platform Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From 8d6a47b79e7b476a0019fd1e7d3aada3f4dbc9b0 Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:34:19 -0500 Subject: [PATCH 0645/2357] fix invalid module names in cabal file --- distributed-process-platform.cabal | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index de4035c9..bb570b13 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -63,10 +63,10 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives - Control.Distributed.Platform.Internal.Types + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From 55411d90d939e2f1dbc23a994ca3e0e83bb7a38f Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:34:19 -0500 Subject: [PATCH 0646/2357] fix invalid module names in cabal file --- distributed-process-platform.cabal | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index de4035c9..bb570b13 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -63,10 +63,10 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives - Control.Distributed.Platform.Internal.Types + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From bc7a1e91259cea35fbf193c1695e8301a11b3253 Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:34:19 -0500 Subject: [PATCH 0647/2357] fix invalid module names in cabal file --- distributed-process-platform.cabal | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index de4035c9..bb570b13 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -63,10 +63,10 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives - Control.Distributed.Platform.Internal.Types + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From 83fa04b2d36b6d95840dfb9e2f261f4836e85bb2 Mon Sep 17 00:00:00 2001 From: Eric Merrit Date: Wed, 9 Jan 2013 14:34:19 -0500 Subject: [PATCH 0648/2357] fix invalid module names in cabal file --- distributed-process-platform.cabal | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index de4035c9..bb570b13 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -63,10 +63,10 @@ test-suite TimerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives - Control.Distributed.Platform.Internal.Types + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform + Control.Distributed.Process.Platform.Internal.Primitives + Control.Distributed.Process.Platform.Internal.Types TestUtils extensions: CPP main-is: TestTimer.hs From e1ff2dfeef546e814f6bee9cbc4195f42d616eac Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 12:37:18 +0000 Subject: [PATCH 0649/2357] implement AsyncSTM --- distributed-process-platform.cabal | 16 +- src/Control/Distributed/Platform/Async.hs | 18 +- .../Distributed/Platform/Async/AsyncChan.hs | 35 ++-- .../Distributed/Platform/Async/AsyncSTM.hs | 163 ++++++++++++++---- tests/TestAsync.hs | 3 +- 5 files changed, 172 insertions(+), 63 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ea3f227..ab5bc5a2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,13 +68,13 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestAsync.hs @@ -102,12 +102,12 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index f00ce481..c87db4be 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -15,12 +15,20 @@ -- The modules in the @Async@ package provide operations for spawning Processes, -- waiting for their results, cancelling them and various other utilities. The -- two primary implementation are @AsyncChan@ which provides an API which is --- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that --- can be used by (as in shared across) multiple processes on a local node. --- Both abstractions can run asynchronous operations on remote node. +-- scoped to the calling process, and @Async@ which provides a mechanism that +-- can be used by (i.e., shared across) multiple processes either locally or +-- situation on remote nodes. +-- +-- Both abstractions can run asynchronous operations on remote nodes. +-- +-- Despite providing an API at a higher level than the basic primitives in +-- distributed-process, this API is still quite low level and it is +-- recommended that you read the documentation carefully to understand its +-- constraints. For a much higher level API, consider using the +-- 'Control.Distributed.Platform.Task' layer. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async +module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncTask @@ -46,7 +54,7 @@ type AsyncRef = ProcessId -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Represents the result of an asynchronous action, which can be in one of +-- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = AsyncDone a -- ^ a completed action and its result diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 7bc1403b..e02c92e3 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,7 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- -- ----------------------------------------------------------------------------- @@ -108,13 +107,13 @@ async = asyncDo True -- never left running unintentionally. This function is provided for compatibility -- with other /async/ implementations that may offer different semantics for -- @async@ with regards linking. --- --- @asyncLinked = async@ +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) asyncDo shouldLink task = do (wpid, gpid, chan) <- spawnWorkers task shouldLink return AsyncChan { @@ -130,16 +129,16 @@ spawnWorkers :: (Serializable a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan - + -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do () <- expect r <- task sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - + wref <- monitor workerPid rref <- case shouldLink of True -> monitor root >>= return . Just @@ -147,11 +146,11 @@ spawnWorkers task shouldLink = do finally (pollUntilExit workerPid chan) (unmonitor wref >> return (maybe (return ()) unmonitor rref)) - + workerPid <- expect send workerPid () return (workerPid, insulatorPid, chan) - where + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId @@ -166,7 +165,7 @@ spawnWorkers task shouldLink = do case r of Left CancelWait -> sendChan replyTo AsyncCancelled Right (fpid, d) - | fpid == wpid -> case d of + | fpid == wpid -> case d of DiedNormal -> return () _ -> sendChan replyTo (AsyncFailed d) | otherwise -> kill wpid "linkFailed" @@ -187,7 +186,7 @@ poll hAsync = do check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing - ar -> return (Just ar) + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -207,7 +206,7 @@ wait hAsync = receiveChan $ snd (channel hAsync) -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = @@ -216,7 +215,7 @@ waitTimeout t hAsync = -- | Wait for any of the supplied @AsyncChans@s to complete. If multiple -- 'Async's complete, then the value returned corresponds to the first -- completed 'Async' in the list. Only /unread/ 'Async's are of value here, --- because 'AsyncChan' does not hold on to its result after it has been read! +-- because 'AsyncChan' does not hold on to its result after it has been read! -- -- This function is analagous to the @mergePortsBiased@ primitive. -- See 'Control.Distibuted.Process.mergePortsBiased' @@ -226,7 +225,7 @@ waitAny :: (Serializable a) waitAny asyncs = let ports = map (snd . channel) asyncs in recv ports where recv :: (Serializable a) => [ReceivePort a] -> Process a - recv ps = mergePortsBiased ps >>= receiveChan + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) @@ -251,7 +250,7 @@ cancel (AsyncChan _ g _) = send g CancelWait -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For -- example, the worker may complete its task after this function is called, but -- before the cancellation instruction is acted upon. --- +-- -- If you wish to stop an asychronous operation /immediately/ (with caveats) then -- consider using 'cancelWith' or 'cancelKill' instead. -- @@ -275,14 +274,14 @@ cancelWait hAsync = cancel hAsync >> wait hAsync -- time to handle the request, which can lead to situations similar to (1) as -- listed above, if the scheduler to which the calling process' thread is bound -- decides to GC whilst another scheduler on which the worker is running is able --- to continue. +-- to continue. -- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () -cancelWith reason = (flip exit) reason . worker +cancelWith reason = (flip exit) reason . worker -- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. --- +-- -- See 'Control.Distributed.Process.kill' cancelKill :: String -> AsyncChan a -> Process () cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 23c108ce..274605cf 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -15,33 +15,59 @@ -- This module provides a set of operations for spawning Process operations -- and waiting for their results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. -- +-- The difference between 'Control.Distributed.Platform.Async.Async' and +-- 'Control.Distributed.Platform.Async.AsyncChan' is that handles of the +-- former (i.e., returned by /this/ module) can be sent across a remote +-- boundary, where the receiver can use the API calls to wait on the +-- results of the computation at their end. +-- +-- Like 'Control.Distributed.Platform.Async.AsyncChan', workers can be +-- started on a local or remote node. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async.AsyncSTM where +module Control.Distributed.Platform.Async.AsyncSTM + ( -- types/data + AsyncRef + , AsyncTask + , AsyncSTM(_asyncWorker) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing +-- , cancel +-- , cancelWait +-- , cancelWith +-- , cancelKill + -- functions to query an async-result + , poll + -- , check + , wait + -- , waitAny + -- , waitAnyTimeout + -- , waitTimeout + -- , waitCheckTimeout + -- STM versions + , pollSTM + ) where + +import Control.Applicative +import Control.Concurrent.STM import Control.Distributed.Platform.Async -import Control.Distributed.Platform.Timer - ( intervalToMs - ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) - , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Serializable - +import Control.Monad import Data.Maybe ( fromMaybe ) -import Control.Concurrent.STM -import GHC.Conc +import Prelude hiding (catch) -------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- +-- Cloud Haskell STM Async Process API -- -------------------------------------------------------------------------------- -- | An handle for an asynchronous action spawned by 'async'. @@ -49,13 +75,20 @@ import GHC.Conc -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries, nor are they +-- @Serializable@. data AsyncSTM a = AsyncSTM { - worker :: AsyncRef - , insulator :: AsyncRef - , hWait :: STM a + _asyncWorker :: AsyncRef + , _asyncMonitor :: AsyncRef + , _asyncWait :: STM (AsyncResult a) } +instance Eq (AsyncSTM a) where + AsyncSTM a b _ == AsyncSTM c d _ = a == c && b == d + +-- instance Functor AsyncSTM where +-- fmap f (AsyncSTM a b w) = AsyncSTM a b (fmap (fmap f) w) + -- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should @@ -64,7 +97,7 @@ data AsyncSTM a = AsyncSTM { -- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -async = asyncDo True +async = asyncDo False -- | This is a useful variant of 'async' that ensures an @AsyncChan@ is -- never left running unintentionally. We ensure that if the caller's process @@ -73,19 +106,89 @@ async = asyncDo True -- (if any) is discarded. -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -asyncLinked = asyncDo False +asyncLinked = asyncDo True -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) asyncDo shouldLink task = do - (wpid, gpid, hRes) <- spawnWorkers task shouldLink + root <- getSelfPid + result <- liftIO $ newEmptyTMVarIO + + -- listener/response proxy + mPid <- spawnLocal $ do + wPid <- spawnLocal $ do + () <- expect + r <- task + void $ liftIO $ atomically $ putTMVar result (AsyncDone r) + + send root wPid -- let the parent process know the worker pid + + wref <- monitor wPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit wPid result) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + send workerPid () + return AsyncSTM { - worker = wpid - , insulator = gpid - , hWait = hRes - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, STM a) -spawnWorkers task shouldLink = undefined + _asyncWorker = workerPid + , _asyncMonitor = mPid + , _asyncWait = (readTMVar result) + } + + where + pollUntilExit :: (Serializable a) + => ProcessId + -> TMVar (AsyncResult a) + -> Process () + pollUntilExit wpid result' = do + r <- receiveWait [ + match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + , match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + ] + case r of + Left CancelWait + -> liftIO $ atomically $ putTMVar result' AsyncCancelled + Right (fpid, d) + | fpid == wpid + -> case d of + DiedNormal -> return () + _ -> liftIO $ atomically $ putTMVar result' (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncSTM' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncSTM a -> Process (AsyncResult a) +poll hAsync = do + r <- liftIO $ atomically $ pollSTM hAsync + return $ fromMaybe (AsyncPending) r + +-- | Wait for an asynchronous action to complete, and return its +-- value. The result (which can include failure and/or cancellation) is +-- encoded by the 'AsyncResult' type. +-- +-- > wait = liftIO . atomically . waitSTM +-- +{-# INLINE wait #-} +wait :: AsyncSTM a -> Process (AsyncResult a) +wait = liftIO . atomically . waitSTM + +-- | A version of 'wait' that can be used inside an STM transaction. +-- +waitSTM :: AsyncSTM a -> STM (AsyncResult a) +waitSTM (AsyncSTM _ _ w) = w + +-- | A version of 'poll' that can be used inside an STM transaction. +-- +{-# INLINE pollSTM #-} +pollSTM :: AsyncSTM a -> STM (Maybe (AsyncResult a)) +pollSTM (AsyncSTM _ _ w) = (Just <$> w) `orElse` return Nothing + diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index a119b53e..843d6e06 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -1,8 +1,7 @@ module Main where -import Test.Framework (Test, defaultMain, testGroup) +import Test.Framework (Test, testGroup) import qualified Network.Transport as NT -import Network.Transport.TCP import TestAsyncChan import TestAsyncSTM import TestUtils From ede19aee06a90d35d36d7bd83bdb600c7297ccb1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 12:37:18 +0000 Subject: [PATCH 0650/2357] implement AsyncSTM --- distributed-process-platform.cabal | 16 +- src/Control/Distributed/Platform/Async.hs | 18 +- .../Distributed/Platform/Async/AsyncChan.hs | 35 ++-- .../Distributed/Platform/Async/AsyncSTM.hs | 163 ++++++++++++++---- 4 files changed, 171 insertions(+), 61 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ea3f227..ab5bc5a2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,13 +68,13 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestAsync.hs @@ -102,12 +102,12 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index f00ce481..c87db4be 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -15,12 +15,20 @@ -- The modules in the @Async@ package provide operations for spawning Processes, -- waiting for their results, cancelling them and various other utilities. The -- two primary implementation are @AsyncChan@ which provides an API which is --- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that --- can be used by (as in shared across) multiple processes on a local node. --- Both abstractions can run asynchronous operations on remote node. +-- scoped to the calling process, and @Async@ which provides a mechanism that +-- can be used by (i.e., shared across) multiple processes either locally or +-- situation on remote nodes. +-- +-- Both abstractions can run asynchronous operations on remote nodes. +-- +-- Despite providing an API at a higher level than the basic primitives in +-- distributed-process, this API is still quite low level and it is +-- recommended that you read the documentation carefully to understand its +-- constraints. For a much higher level API, consider using the +-- 'Control.Distributed.Platform.Task' layer. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async +module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncTask @@ -46,7 +54,7 @@ type AsyncRef = ProcessId -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Represents the result of an asynchronous action, which can be in one of +-- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = AsyncDone a -- ^ a completed action and its result diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 7bc1403b..e02c92e3 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,7 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- -- ----------------------------------------------------------------------------- @@ -108,13 +107,13 @@ async = asyncDo True -- never left running unintentionally. This function is provided for compatibility -- with other /async/ implementations that may offer different semantics for -- @async@ with regards linking. --- --- @asyncLinked = async@ +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) asyncDo shouldLink task = do (wpid, gpid, chan) <- spawnWorkers task shouldLink return AsyncChan { @@ -130,16 +129,16 @@ spawnWorkers :: (Serializable a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan - + -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do () <- expect r <- task sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - + wref <- monitor workerPid rref <- case shouldLink of True -> monitor root >>= return . Just @@ -147,11 +146,11 @@ spawnWorkers task shouldLink = do finally (pollUntilExit workerPid chan) (unmonitor wref >> return (maybe (return ()) unmonitor rref)) - + workerPid <- expect send workerPid () return (workerPid, insulatorPid, chan) - where + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId @@ -166,7 +165,7 @@ spawnWorkers task shouldLink = do case r of Left CancelWait -> sendChan replyTo AsyncCancelled Right (fpid, d) - | fpid == wpid -> case d of + | fpid == wpid -> case d of DiedNormal -> return () _ -> sendChan replyTo (AsyncFailed d) | otherwise -> kill wpid "linkFailed" @@ -187,7 +186,7 @@ poll hAsync = do check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing - ar -> return (Just ar) + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -207,7 +206,7 @@ wait hAsync = receiveChan $ snd (channel hAsync) -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = @@ -216,7 +215,7 @@ waitTimeout t hAsync = -- | Wait for any of the supplied @AsyncChans@s to complete. If multiple -- 'Async's complete, then the value returned corresponds to the first -- completed 'Async' in the list. Only /unread/ 'Async's are of value here, --- because 'AsyncChan' does not hold on to its result after it has been read! +-- because 'AsyncChan' does not hold on to its result after it has been read! -- -- This function is analagous to the @mergePortsBiased@ primitive. -- See 'Control.Distibuted.Process.mergePortsBiased' @@ -226,7 +225,7 @@ waitAny :: (Serializable a) waitAny asyncs = let ports = map (snd . channel) asyncs in recv ports where recv :: (Serializable a) => [ReceivePort a] -> Process a - recv ps = mergePortsBiased ps >>= receiveChan + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) @@ -251,7 +250,7 @@ cancel (AsyncChan _ g _) = send g CancelWait -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For -- example, the worker may complete its task after this function is called, but -- before the cancellation instruction is acted upon. --- +-- -- If you wish to stop an asychronous operation /immediately/ (with caveats) then -- consider using 'cancelWith' or 'cancelKill' instead. -- @@ -275,14 +274,14 @@ cancelWait hAsync = cancel hAsync >> wait hAsync -- time to handle the request, which can lead to situations similar to (1) as -- listed above, if the scheduler to which the calling process' thread is bound -- decides to GC whilst another scheduler on which the worker is running is able --- to continue. +-- to continue. -- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () -cancelWith reason = (flip exit) reason . worker +cancelWith reason = (flip exit) reason . worker -- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. --- +-- -- See 'Control.Distributed.Process.kill' cancelKill :: String -> AsyncChan a -> Process () cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 23c108ce..274605cf 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -15,33 +15,59 @@ -- This module provides a set of operations for spawning Process operations -- and waiting for their results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. -- +-- The difference between 'Control.Distributed.Platform.Async.Async' and +-- 'Control.Distributed.Platform.Async.AsyncChan' is that handles of the +-- former (i.e., returned by /this/ module) can be sent across a remote +-- boundary, where the receiver can use the API calls to wait on the +-- results of the computation at their end. +-- +-- Like 'Control.Distributed.Platform.Async.AsyncChan', workers can be +-- started on a local or remote node. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async.AsyncSTM where +module Control.Distributed.Platform.Async.AsyncSTM + ( -- types/data + AsyncRef + , AsyncTask + , AsyncSTM(_asyncWorker) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing +-- , cancel +-- , cancelWait +-- , cancelWith +-- , cancelKill + -- functions to query an async-result + , poll + -- , check + , wait + -- , waitAny + -- , waitAnyTimeout + -- , waitTimeout + -- , waitCheckTimeout + -- STM versions + , pollSTM + ) where + +import Control.Applicative +import Control.Concurrent.STM import Control.Distributed.Platform.Async -import Control.Distributed.Platform.Timer - ( intervalToMs - ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) - , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Serializable - +import Control.Monad import Data.Maybe ( fromMaybe ) -import Control.Concurrent.STM -import GHC.Conc +import Prelude hiding (catch) -------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- +-- Cloud Haskell STM Async Process API -- -------------------------------------------------------------------------------- -- | An handle for an asynchronous action spawned by 'async'. @@ -49,13 +75,20 @@ import GHC.Conc -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries, nor are they +-- @Serializable@. data AsyncSTM a = AsyncSTM { - worker :: AsyncRef - , insulator :: AsyncRef - , hWait :: STM a + _asyncWorker :: AsyncRef + , _asyncMonitor :: AsyncRef + , _asyncWait :: STM (AsyncResult a) } +instance Eq (AsyncSTM a) where + AsyncSTM a b _ == AsyncSTM c d _ = a == c && b == d + +-- instance Functor AsyncSTM where +-- fmap f (AsyncSTM a b w) = AsyncSTM a b (fmap (fmap f) w) + -- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should @@ -64,7 +97,7 @@ data AsyncSTM a = AsyncSTM { -- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -async = asyncDo True +async = asyncDo False -- | This is a useful variant of 'async' that ensures an @AsyncChan@ is -- never left running unintentionally. We ensure that if the caller's process @@ -73,19 +106,89 @@ async = asyncDo True -- (if any) is discarded. -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -asyncLinked = asyncDo False +asyncLinked = asyncDo True -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) asyncDo shouldLink task = do - (wpid, gpid, hRes) <- spawnWorkers task shouldLink + root <- getSelfPid + result <- liftIO $ newEmptyTMVarIO + + -- listener/response proxy + mPid <- spawnLocal $ do + wPid <- spawnLocal $ do + () <- expect + r <- task + void $ liftIO $ atomically $ putTMVar result (AsyncDone r) + + send root wPid -- let the parent process know the worker pid + + wref <- monitor wPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit wPid result) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + send workerPid () + return AsyncSTM { - worker = wpid - , insulator = gpid - , hWait = hRes - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, STM a) -spawnWorkers task shouldLink = undefined + _asyncWorker = workerPid + , _asyncMonitor = mPid + , _asyncWait = (readTMVar result) + } + + where + pollUntilExit :: (Serializable a) + => ProcessId + -> TMVar (AsyncResult a) + -> Process () + pollUntilExit wpid result' = do + r <- receiveWait [ + match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + , match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + ] + case r of + Left CancelWait + -> liftIO $ atomically $ putTMVar result' AsyncCancelled + Right (fpid, d) + | fpid == wpid + -> case d of + DiedNormal -> return () + _ -> liftIO $ atomically $ putTMVar result' (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncSTM' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncSTM a -> Process (AsyncResult a) +poll hAsync = do + r <- liftIO $ atomically $ pollSTM hAsync + return $ fromMaybe (AsyncPending) r + +-- | Wait for an asynchronous action to complete, and return its +-- value. The result (which can include failure and/or cancellation) is +-- encoded by the 'AsyncResult' type. +-- +-- > wait = liftIO . atomically . waitSTM +-- +{-# INLINE wait #-} +wait :: AsyncSTM a -> Process (AsyncResult a) +wait = liftIO . atomically . waitSTM + +-- | A version of 'wait' that can be used inside an STM transaction. +-- +waitSTM :: AsyncSTM a -> STM (AsyncResult a) +waitSTM (AsyncSTM _ _ w) = w + +-- | A version of 'poll' that can be used inside an STM transaction. +-- +{-# INLINE pollSTM #-} +pollSTM :: AsyncSTM a -> STM (Maybe (AsyncResult a)) +pollSTM (AsyncSTM _ _ w) = (Just <$> w) `orElse` return Nothing + From 2cb36973d40c35400ae0ebe855002ddb88d12a61 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 12:37:18 +0000 Subject: [PATCH 0651/2357] implement AsyncSTM --- distributed-process-platform.cabal | 16 +- src/Control/Distributed/Platform/Async.hs | 18 +- .../Distributed/Platform/Async/AsyncChan.hs | 35 ++-- .../Distributed/Platform/Async/AsyncSTM.hs | 163 ++++++++++++++---- 4 files changed, 171 insertions(+), 61 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ea3f227..ab5bc5a2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,13 +68,13 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestAsync.hs @@ -102,12 +102,12 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index f00ce481..c87db4be 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -15,12 +15,20 @@ -- The modules in the @Async@ package provide operations for spawning Processes, -- waiting for their results, cancelling them and various other utilities. The -- two primary implementation are @AsyncChan@ which provides an API which is --- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that --- can be used by (as in shared across) multiple processes on a local node. --- Both abstractions can run asynchronous operations on remote node. +-- scoped to the calling process, and @Async@ which provides a mechanism that +-- can be used by (i.e., shared across) multiple processes either locally or +-- situation on remote nodes. +-- +-- Both abstractions can run asynchronous operations on remote nodes. +-- +-- Despite providing an API at a higher level than the basic primitives in +-- distributed-process, this API is still quite low level and it is +-- recommended that you read the documentation carefully to understand its +-- constraints. For a much higher level API, consider using the +-- 'Control.Distributed.Platform.Task' layer. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async +module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncTask @@ -46,7 +54,7 @@ type AsyncRef = ProcessId -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Represents the result of an asynchronous action, which can be in one of +-- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = AsyncDone a -- ^ a completed action and its result diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 7bc1403b..e02c92e3 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,7 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- -- ----------------------------------------------------------------------------- @@ -108,13 +107,13 @@ async = asyncDo True -- never left running unintentionally. This function is provided for compatibility -- with other /async/ implementations that may offer different semantics for -- @async@ with regards linking. --- --- @asyncLinked = async@ +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) asyncDo shouldLink task = do (wpid, gpid, chan) <- spawnWorkers task shouldLink return AsyncChan { @@ -130,16 +129,16 @@ spawnWorkers :: (Serializable a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan - + -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do () <- expect r <- task sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - + wref <- monitor workerPid rref <- case shouldLink of True -> monitor root >>= return . Just @@ -147,11 +146,11 @@ spawnWorkers task shouldLink = do finally (pollUntilExit workerPid chan) (unmonitor wref >> return (maybe (return ()) unmonitor rref)) - + workerPid <- expect send workerPid () return (workerPid, insulatorPid, chan) - where + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId @@ -166,7 +165,7 @@ spawnWorkers task shouldLink = do case r of Left CancelWait -> sendChan replyTo AsyncCancelled Right (fpid, d) - | fpid == wpid -> case d of + | fpid == wpid -> case d of DiedNormal -> return () _ -> sendChan replyTo (AsyncFailed d) | otherwise -> kill wpid "linkFailed" @@ -187,7 +186,7 @@ poll hAsync = do check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing - ar -> return (Just ar) + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -207,7 +206,7 @@ wait hAsync = receiveChan $ snd (channel hAsync) -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = @@ -216,7 +215,7 @@ waitTimeout t hAsync = -- | Wait for any of the supplied @AsyncChans@s to complete. If multiple -- 'Async's complete, then the value returned corresponds to the first -- completed 'Async' in the list. Only /unread/ 'Async's are of value here, --- because 'AsyncChan' does not hold on to its result after it has been read! +-- because 'AsyncChan' does not hold on to its result after it has been read! -- -- This function is analagous to the @mergePortsBiased@ primitive. -- See 'Control.Distibuted.Process.mergePortsBiased' @@ -226,7 +225,7 @@ waitAny :: (Serializable a) waitAny asyncs = let ports = map (snd . channel) asyncs in recv ports where recv :: (Serializable a) => [ReceivePort a] -> Process a - recv ps = mergePortsBiased ps >>= receiveChan + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) @@ -251,7 +250,7 @@ cancel (AsyncChan _ g _) = send g CancelWait -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For -- example, the worker may complete its task after this function is called, but -- before the cancellation instruction is acted upon. --- +-- -- If you wish to stop an asychronous operation /immediately/ (with caveats) then -- consider using 'cancelWith' or 'cancelKill' instead. -- @@ -275,14 +274,14 @@ cancelWait hAsync = cancel hAsync >> wait hAsync -- time to handle the request, which can lead to situations similar to (1) as -- listed above, if the scheduler to which the calling process' thread is bound -- decides to GC whilst another scheduler on which the worker is running is able --- to continue. +-- to continue. -- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () -cancelWith reason = (flip exit) reason . worker +cancelWith reason = (flip exit) reason . worker -- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. --- +-- -- See 'Control.Distributed.Process.kill' cancelKill :: String -> AsyncChan a -> Process () cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 23c108ce..274605cf 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -15,33 +15,59 @@ -- This module provides a set of operations for spawning Process operations -- and waiting for their results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. -- +-- The difference between 'Control.Distributed.Platform.Async.Async' and +-- 'Control.Distributed.Platform.Async.AsyncChan' is that handles of the +-- former (i.e., returned by /this/ module) can be sent across a remote +-- boundary, where the receiver can use the API calls to wait on the +-- results of the computation at their end. +-- +-- Like 'Control.Distributed.Platform.Async.AsyncChan', workers can be +-- started on a local or remote node. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async.AsyncSTM where +module Control.Distributed.Platform.Async.AsyncSTM + ( -- types/data + AsyncRef + , AsyncTask + , AsyncSTM(_asyncWorker) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing +-- , cancel +-- , cancelWait +-- , cancelWith +-- , cancelKill + -- functions to query an async-result + , poll + -- , check + , wait + -- , waitAny + -- , waitAnyTimeout + -- , waitTimeout + -- , waitCheckTimeout + -- STM versions + , pollSTM + ) where + +import Control.Applicative +import Control.Concurrent.STM import Control.Distributed.Platform.Async -import Control.Distributed.Platform.Timer - ( intervalToMs - ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) - , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Serializable - +import Control.Monad import Data.Maybe ( fromMaybe ) -import Control.Concurrent.STM -import GHC.Conc +import Prelude hiding (catch) -------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- +-- Cloud Haskell STM Async Process API -- -------------------------------------------------------------------------------- -- | An handle for an asynchronous action spawned by 'async'. @@ -49,13 +75,20 @@ import GHC.Conc -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries, nor are they +-- @Serializable@. data AsyncSTM a = AsyncSTM { - worker :: AsyncRef - , insulator :: AsyncRef - , hWait :: STM a + _asyncWorker :: AsyncRef + , _asyncMonitor :: AsyncRef + , _asyncWait :: STM (AsyncResult a) } +instance Eq (AsyncSTM a) where + AsyncSTM a b _ == AsyncSTM c d _ = a == c && b == d + +-- instance Functor AsyncSTM where +-- fmap f (AsyncSTM a b w) = AsyncSTM a b (fmap (fmap f) w) + -- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should @@ -64,7 +97,7 @@ data AsyncSTM a = AsyncSTM { -- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -async = asyncDo True +async = asyncDo False -- | This is a useful variant of 'async' that ensures an @AsyncChan@ is -- never left running unintentionally. We ensure that if the caller's process @@ -73,19 +106,89 @@ async = asyncDo True -- (if any) is discarded. -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -asyncLinked = asyncDo False +asyncLinked = asyncDo True -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) asyncDo shouldLink task = do - (wpid, gpid, hRes) <- spawnWorkers task shouldLink + root <- getSelfPid + result <- liftIO $ newEmptyTMVarIO + + -- listener/response proxy + mPid <- spawnLocal $ do + wPid <- spawnLocal $ do + () <- expect + r <- task + void $ liftIO $ atomically $ putTMVar result (AsyncDone r) + + send root wPid -- let the parent process know the worker pid + + wref <- monitor wPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit wPid result) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + send workerPid () + return AsyncSTM { - worker = wpid - , insulator = gpid - , hWait = hRes - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, STM a) -spawnWorkers task shouldLink = undefined + _asyncWorker = workerPid + , _asyncMonitor = mPid + , _asyncWait = (readTMVar result) + } + + where + pollUntilExit :: (Serializable a) + => ProcessId + -> TMVar (AsyncResult a) + -> Process () + pollUntilExit wpid result' = do + r <- receiveWait [ + match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + , match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + ] + case r of + Left CancelWait + -> liftIO $ atomically $ putTMVar result' AsyncCancelled + Right (fpid, d) + | fpid == wpid + -> case d of + DiedNormal -> return () + _ -> liftIO $ atomically $ putTMVar result' (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncSTM' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncSTM a -> Process (AsyncResult a) +poll hAsync = do + r <- liftIO $ atomically $ pollSTM hAsync + return $ fromMaybe (AsyncPending) r + +-- | Wait for an asynchronous action to complete, and return its +-- value. The result (which can include failure and/or cancellation) is +-- encoded by the 'AsyncResult' type. +-- +-- > wait = liftIO . atomically . waitSTM +-- +{-# INLINE wait #-} +wait :: AsyncSTM a -> Process (AsyncResult a) +wait = liftIO . atomically . waitSTM + +-- | A version of 'wait' that can be used inside an STM transaction. +-- +waitSTM :: AsyncSTM a -> STM (AsyncResult a) +waitSTM (AsyncSTM _ _ w) = w + +-- | A version of 'poll' that can be used inside an STM transaction. +-- +{-# INLINE pollSTM #-} +pollSTM :: AsyncSTM a -> STM (Maybe (AsyncResult a)) +pollSTM (AsyncSTM _ _ w) = (Just <$> w) `orElse` return Nothing + From e5b05141b08d19e0862add355e76f4fb3c4b4186 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 12:37:18 +0000 Subject: [PATCH 0652/2357] implement AsyncSTM --- distributed-process-platform.cabal | 16 +- src/Control/Distributed/Platform/Async.hs | 18 +- .../Distributed/Platform/Async/AsyncChan.hs | 35 ++-- .../Distributed/Platform/Async/AsyncSTM.hs | 163 ++++++++++++++---- 4 files changed, 171 insertions(+), 61 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 0ea3f227..ab5bc5a2 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -68,13 +68,13 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestAsync.hs @@ -102,12 +102,12 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Platform.Timer, - Control.Distributed.Platform - Control.Distributed.Platform.Internal.Primitives + Control.Distributed.Platform, + Control.Distributed.Platform.Internal.Primitives, Control.Distributed.Platform.Async, - Control.Distributed.Platform.Async.AsyncSTM, TestUtils, TestAsyncSTM, - TestAsync + TestAsync, + Control.Distributed.Platform.Async.AsyncSTM extensions: CPP main-is: TestTimer.hs diff --git a/src/Control/Distributed/Platform/Async.hs b/src/Control/Distributed/Platform/Async.hs index f00ce481..c87db4be 100644 --- a/src/Control/Distributed/Platform/Async.hs +++ b/src/Control/Distributed/Platform/Async.hs @@ -15,12 +15,20 @@ -- The modules in the @Async@ package provide operations for spawning Processes, -- waiting for their results, cancelling them and various other utilities. The -- two primary implementation are @AsyncChan@ which provides an API which is --- scoped to the calling process, and @AsyncSTM@ which provides a mechanism that --- can be used by (as in shared across) multiple processes on a local node. --- Both abstractions can run asynchronous operations on remote node. +-- scoped to the calling process, and @Async@ which provides a mechanism that +-- can be used by (i.e., shared across) multiple processes either locally or +-- situation on remote nodes. +-- +-- Both abstractions can run asynchronous operations on remote nodes. +-- +-- Despite providing an API at a higher level than the basic primitives in +-- distributed-process, this API is still quite low level and it is +-- recommended that you read the documentation carefully to understand its +-- constraints. For a much higher level API, consider using the +-- 'Control.Distributed.Platform.Task' layer. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async +module Control.Distributed.Platform.Async ( -- types/data AsyncRef , AsyncTask @@ -46,7 +54,7 @@ type AsyncRef = ProcessId -- spawned - in the @Process a@ case the task is spawned on the local node type AsyncTask a = Process a --- | Represents the result of an asynchronous action, which can be in one of +-- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. data AsyncResult a = AsyncDone a -- ^ a completed action and its result diff --git a/src/Control/Distributed/Platform/Async/AsyncChan.hs b/src/Control/Distributed/Platform/Async/AsyncChan.hs index 7bc1403b..e02c92e3 100644 --- a/src/Control/Distributed/Platform/Async/AsyncChan.hs +++ b/src/Control/Distributed/Platform/Async/AsyncChan.hs @@ -19,7 +19,6 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- --- -- ----------------------------------------------------------------------------- @@ -108,13 +107,13 @@ async = asyncDo True -- never left running unintentionally. This function is provided for compatibility -- with other /async/ implementations that may offer different semantics for -- @async@ with regards linking. --- --- @asyncLinked = async@ +-- +-- @asyncLinked = async@ -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) asyncDo shouldLink task = do (wpid, gpid, chan) <- spawnWorkers task shouldLink return AsyncChan { @@ -130,16 +129,16 @@ spawnWorkers :: (Serializable a) spawnWorkers task shouldLink = do root <- getSelfPid chan <- newChan - + -- listener/response proxy insulatorPid <- spawnLocal $ do workerPid <- spawnLocal $ do () <- expect r <- task sendChan (fst chan) (AsyncDone r) - + send root workerPid -- let the parent process know the worker pid - + wref <- monitor workerPid rref <- case shouldLink of True -> monitor root >>= return . Just @@ -147,11 +146,11 @@ spawnWorkers task shouldLink = do finally (pollUntilExit workerPid chan) (unmonitor wref >> return (maybe (return ()) unmonitor rref)) - + workerPid <- expect send workerPid () return (workerPid, insulatorPid, chan) - where + where -- blocking receive until we see an input message pollUntilExit :: (Serializable a) => ProcessId @@ -166,7 +165,7 @@ spawnWorkers task shouldLink = do case r of Left CancelWait -> sendChan replyTo AsyncCancelled Right (fpid, d) - | fpid == wpid -> case d of + | fpid == wpid -> case d of DiedNormal -> return () _ -> sendChan replyTo (AsyncFailed d) | otherwise -> kill wpid "linkFailed" @@ -187,7 +186,7 @@ poll hAsync = do check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing - ar -> return (Just ar) + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -207,7 +206,7 @@ wait hAsync = receiveChan $ snd (channel hAsync) -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within -- the specified delay, otherwise @Just asyncResult@ is returned. If you want -- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. +-- consider using 'wait' or 'waitCheckTimeout' instead. waitTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = @@ -216,7 +215,7 @@ waitTimeout t hAsync = -- | Wait for any of the supplied @AsyncChans@s to complete. If multiple -- 'Async's complete, then the value returned corresponds to the first -- completed 'Async' in the list. Only /unread/ 'Async's are of value here, --- because 'AsyncChan' does not hold on to its result after it has been read! +-- because 'AsyncChan' does not hold on to its result after it has been read! -- -- This function is analagous to the @mergePortsBiased@ primitive. -- See 'Control.Distibuted.Process.mergePortsBiased' @@ -226,7 +225,7 @@ waitAny :: (Serializable a) waitAny asyncs = let ports = map (snd . channel) asyncs in recv ports where recv :: (Serializable a) => [ReceivePort a] -> Process a - recv ps = mergePortsBiased ps >>= receiveChan + recv ps = mergePortsBiased ps >>= receiveChan -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) @@ -251,7 +250,7 @@ cancel (AsyncChan _ g _) = send g CancelWait -- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For -- example, the worker may complete its task after this function is called, but -- before the cancellation instruction is acted upon. --- +-- -- If you wish to stop an asychronous operation /immediately/ (with caveats) then -- consider using 'cancelWith' or 'cancelKill' instead. -- @@ -275,14 +274,14 @@ cancelWait hAsync = cancel hAsync >> wait hAsync -- time to handle the request, which can lead to situations similar to (1) as -- listed above, if the scheduler to which the calling process' thread is bound -- decides to GC whilst another scheduler on which the worker is running is able --- to continue. +-- to continue. -- -- See 'Control.Distributed.Process.exit' cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () -cancelWith reason = (flip exit) reason . worker +cancelWith reason = (flip exit) reason . worker -- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. --- +-- -- See 'Control.Distributed.Process.kill' cancelKill :: String -> AsyncChan a -> Process () cancelKill reason = (flip kill) reason . worker diff --git a/src/Control/Distributed/Platform/Async/AsyncSTM.hs b/src/Control/Distributed/Platform/Async/AsyncSTM.hs index 23c108ce..274605cf 100644 --- a/src/Control/Distributed/Platform/Async/AsyncSTM.hs +++ b/src/Control/Distributed/Platform/Async/AsyncSTM.hs @@ -15,33 +15,59 @@ -- This module provides a set of operations for spawning Process operations -- and waiting for their results. It is a thin layer over the basic -- concurrency operations provided by "Control.Distributed.Process". --- The main feature it provides is a pre-canned set of APIs for waiting on the --- result of one or more asynchronously running (and potentially distributed) --- processes. -- +-- The difference between 'Control.Distributed.Platform.Async.Async' and +-- 'Control.Distributed.Platform.Async.AsyncChan' is that handles of the +-- former (i.e., returned by /this/ module) can be sent across a remote +-- boundary, where the receiver can use the API calls to wait on the +-- results of the computation at their end. +-- +-- Like 'Control.Distributed.Platform.Async.AsyncChan', workers can be +-- started on a local or remote node. ----------------------------------------------------------------------------- -module Control.Distributed.Platform.Async.AsyncSTM where +module Control.Distributed.Platform.Async.AsyncSTM + ( -- types/data + AsyncRef + , AsyncTask + , AsyncSTM(_asyncWorker) + -- functions for starting/spawning + , async + , asyncLinked + -- and stopping/killing +-- , cancel +-- , cancelWait +-- , cancelWith +-- , cancelKill + -- functions to query an async-result + , poll + -- , check + , wait + -- , waitAny + -- , waitAnyTimeout + -- , waitTimeout + -- , waitCheckTimeout + -- STM versions + , pollSTM + ) where + +import Control.Applicative +import Control.Concurrent.STM import Control.Distributed.Platform.Async -import Control.Distributed.Platform.Timer - ( intervalToMs - ) import Control.Distributed.Platform.Internal.Types ( CancelWait(..) - , TimeInterval() ) import Control.Distributed.Process import Control.Distributed.Process.Serializable - +import Control.Monad import Data.Maybe ( fromMaybe ) -import Control.Concurrent.STM -import GHC.Conc +import Prelude hiding (catch) -------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- +-- Cloud Haskell STM Async Process API -- -------------------------------------------------------------------------------- -- | An handle for an asynchronous action spawned by 'async'. @@ -49,13 +75,20 @@ import GHC.Conc -- operations are provided for waiting for asynchronous actions to -- complete and obtaining their results (see e.g. 'wait'). -- --- Handles of this type cannot cross remote boundaries. +-- Handles of this type cannot cross remote boundaries, nor are they +-- @Serializable@. data AsyncSTM a = AsyncSTM { - worker :: AsyncRef - , insulator :: AsyncRef - , hWait :: STM a + _asyncWorker :: AsyncRef + , _asyncMonitor :: AsyncRef + , _asyncWait :: STM (AsyncResult a) } +instance Eq (AsyncSTM a) where + AsyncSTM a b _ == AsyncSTM c d _ = a == c && b == d + +-- instance Functor AsyncSTM where +-- fmap f (AsyncSTM a b w) = AsyncSTM a b (fmap (fmap f) w) + -- | Spawns an asynchronous action in a new process. -- -- There is currently a contract for async workers which is that they should @@ -64,7 +97,7 @@ data AsyncSTM a = AsyncSTM { -- @AsyncFailed DiedException@ instead of containing the result. -- async :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -async = asyncDo True +async = asyncDo False -- | This is a useful variant of 'async' that ensures an @AsyncChan@ is -- never left running unintentionally. We ensure that if the caller's process @@ -73,19 +106,89 @@ async = asyncDo True -- (if any) is discarded. -- asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncSTM a) -asyncLinked = asyncDo False +asyncLinked = asyncDo True -asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) +asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncSTM a) asyncDo shouldLink task = do - (wpid, gpid, hRes) <- spawnWorkers task shouldLink + root <- getSelfPid + result <- liftIO $ newEmptyTMVarIO + + -- listener/response proxy + mPid <- spawnLocal $ do + wPid <- spawnLocal $ do + () <- expect + r <- task + void $ liftIO $ atomically $ putTMVar result (AsyncDone r) + + send root wPid -- let the parent process know the worker pid + + wref <- monitor wPid + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit wPid result) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- expect + send workerPid () + return AsyncSTM { - worker = wpid - , insulator = gpid - , hWait = hRes - } - -spawnWorkers :: (Serializable a) - => AsyncTask a - -> Bool - -> Process (AsyncRef, AsyncRef, STM a) -spawnWorkers task shouldLink = undefined + _asyncWorker = workerPid + , _asyncMonitor = mPid + , _asyncWait = (readTMVar result) + } + + where + pollUntilExit :: (Serializable a) + => ProcessId + -> TMVar (AsyncResult a) + -> Process () + pollUntilExit wpid result' = do + r <- receiveWait [ + match (\c@(CancelWait) -> kill wpid "cancel" >> return (Left c)) + , match (\(ProcessMonitorNotification _ pid' r) -> + return (Right (pid', r))) + ] + case r of + Left CancelWait + -> liftIO $ atomically $ putTMVar result' AsyncCancelled + Right (fpid, d) + | fpid == wpid + -> case d of + DiedNormal -> return () + _ -> liftIO $ atomically $ putTMVar result' (AsyncFailed d) + | otherwise -> kill wpid "linkFailed" + +-- | Check whether an 'AsyncSTM' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +poll :: (Serializable a) => AsyncSTM a -> Process (AsyncResult a) +poll hAsync = do + r <- liftIO $ atomically $ pollSTM hAsync + return $ fromMaybe (AsyncPending) r + +-- | Wait for an asynchronous action to complete, and return its +-- value. The result (which can include failure and/or cancellation) is +-- encoded by the 'AsyncResult' type. +-- +-- > wait = liftIO . atomically . waitSTM +-- +{-# INLINE wait #-} +wait :: AsyncSTM a -> Process (AsyncResult a) +wait = liftIO . atomically . waitSTM + +-- | A version of 'wait' that can be used inside an STM transaction. +-- +waitSTM :: AsyncSTM a -> STM (AsyncResult a) +waitSTM (AsyncSTM _ _ w) = w + +-- | A version of 'poll' that can be used inside an STM transaction. +-- +{-# INLINE pollSTM #-} +pollSTM :: AsyncSTM a -> STM (Maybe (AsyncResult a)) +pollSTM (AsyncSTM _ _ w) = (Just <$> w) `orElse` return Nothing + From f30466c5ffc0baf9403dc60404fde2e4a8ae0dd0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 14:16:08 +0000 Subject: [PATCH 0653/2357] oops - remove TestGenServer as it won't compile anymore --- tests/TestGenServer.hs | 162 ----------------------------------------- 1 file changed, 162 deletions(-) delete mode 100644 tests/TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs deleted file mode 100644 index c477888d..00000000 --- a/tests/TestGenServer.hs +++ /dev/null @@ -1,162 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -module TestGenServer where - -import Data.Binary (Binary (..)) -import Data.Typeable (Typeable) -import Data.DeriveTH -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar - ( newEmptyMVar - , putMVar - , takeMVar - ) -import qualified Network.Transport as NT (Transport) -import Network.Transport.TCP (TransportInternals) -import Control.Distributed.Process.Internal.Types() -import Control.Distributed.Process (say, liftIO, exit) -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() - -import Test.HUnit (Assertion) -import Test.Framework (Test, testGroup) -import Test.Framework.Providers.HUnit (testCase) - -import Control.Distributed.Process.Platform.GenServer -import Control.Distributed.Process.Platform.Internal.Types -import GenServer.Counter -import GenServer.Kitty - --------------------------------------------------------------------------------- --- The tests proper -- --------------------------------------------------------------------------------- - -data Ping = Ping - deriving (Typeable, Show) -$(derive makeBinary ''Ping) - -data Pong = Pong - deriving (Typeable, Show) -$(derive makeBinary ''Pong) - - --- | Test ping server --- TODO fix this test! -testPing :: NT.Transport -> Assertion -testPing transport = do - initDone <- newEmptyMVar - pingDone <- newEmptyMVar - pongDone <- newEmptyMVar - terminateDone <- newEmptyMVar - serverAddr <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - forkIO $ runProcess localNode $ do - say "Starting ..." - sid <- start (0 :: Int) defaultServer { - initHandler = do - --trace "Init ..." - c <- getState - liftIO $ putMVar initDone c - initOk Infinity, - terminateHandler = \_ -> do - --trace "Terminate ..." - c <- getState - liftIO $ putMVar terminateDone c - return (), - handlers = [ - handle (\Ping -> do - --trace "Ping ..." - modifyState (+1) - c <- getState - liftIO $ putMVar pingDone c - ok Pong), - handle (\Pong -> do - --trace "Pong ..." - modifyState (1 +) - c <- getState - liftIO $ putMVar pongDone c - ok ()) - ]} - liftIO $ putMVar serverAddr sid - return () - - forkIO $ runProcess localNode $ do - sid <- liftIO $ takeMVar serverAddr - - liftIO $ takeMVar initDone - --replicateM_ 10 $ do - Just Pong <- callTimeout sid (Timeout (TimeInterval Seconds 10)) Ping - liftIO $ takeMVar pingDone - cast sid Pong - liftIO $ takeMVar pongDone - exit sid () - - liftIO $ takeMVar terminateDone - return () - - - --- | Test counter server --- TODO split me! -testCounter :: NT.Transport -> Assertion -testCounter transport = do - serverDone <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - runProcess localNode $ do - cid <- startCounter 0 - _ <- getCount cid - incCount cid - incCount cid - _ <- getCount cid - resetCount cid - _ <- getCount cid - terminateCounter cid - liftIO $ putMVar serverDone True - return () - - liftIO $ takeMVar serverDone - return () - - --- | Test kitty server --- TODO split me! -testKitty :: NT.Transport -> Assertion -testKitty transport = do - serverDone <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - runProcess localNode $ do - kPid <- startKitty [Cat "c1" "black" "a black cat"] - --replicateM_ 100 $ do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - closeShop kPid - terminateKitty kPid - liftIO $ putMVar serverDone True - return () - - liftIO $ takeMVar serverDone - return () - - - -tests :: NT.Transport -> [Test] -tests transport = [ - testGroup "Basic features" [ - testCase "Counter" (testCounter transport), - testCase "Kitty" (testKitty transport), - testCase "Ping" (testPing transport) - ] - ] - -genServerTests :: NT.Transport -> TransportInternals -> IO [Test] -genServerTests transport _ = do - return (tests transport) From 73a35b7e41f9733e60b2871e0de773e1af52bfe9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 14:16:08 +0000 Subject: [PATCH 0654/2357] oops - remove TestGenServer as it won't compile anymore --- tests/TestGenServer.hs | 162 ----------------------------------------- 1 file changed, 162 deletions(-) delete mode 100644 tests/TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs deleted file mode 100644 index c477888d..00000000 --- a/tests/TestGenServer.hs +++ /dev/null @@ -1,162 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -module TestGenServer where - -import Data.Binary (Binary (..)) -import Data.Typeable (Typeable) -import Data.DeriveTH -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar - ( newEmptyMVar - , putMVar - , takeMVar - ) -import qualified Network.Transport as NT (Transport) -import Network.Transport.TCP (TransportInternals) -import Control.Distributed.Process.Internal.Types() -import Control.Distributed.Process (say, liftIO, exit) -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() - -import Test.HUnit (Assertion) -import Test.Framework (Test, testGroup) -import Test.Framework.Providers.HUnit (testCase) - -import Control.Distributed.Process.Platform.GenServer -import Control.Distributed.Process.Platform.Internal.Types -import GenServer.Counter -import GenServer.Kitty - --------------------------------------------------------------------------------- --- The tests proper -- --------------------------------------------------------------------------------- - -data Ping = Ping - deriving (Typeable, Show) -$(derive makeBinary ''Ping) - -data Pong = Pong - deriving (Typeable, Show) -$(derive makeBinary ''Pong) - - --- | Test ping server --- TODO fix this test! -testPing :: NT.Transport -> Assertion -testPing transport = do - initDone <- newEmptyMVar - pingDone <- newEmptyMVar - pongDone <- newEmptyMVar - terminateDone <- newEmptyMVar - serverAddr <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - forkIO $ runProcess localNode $ do - say "Starting ..." - sid <- start (0 :: Int) defaultServer { - initHandler = do - --trace "Init ..." - c <- getState - liftIO $ putMVar initDone c - initOk Infinity, - terminateHandler = \_ -> do - --trace "Terminate ..." - c <- getState - liftIO $ putMVar terminateDone c - return (), - handlers = [ - handle (\Ping -> do - --trace "Ping ..." - modifyState (+1) - c <- getState - liftIO $ putMVar pingDone c - ok Pong), - handle (\Pong -> do - --trace "Pong ..." - modifyState (1 +) - c <- getState - liftIO $ putMVar pongDone c - ok ()) - ]} - liftIO $ putMVar serverAddr sid - return () - - forkIO $ runProcess localNode $ do - sid <- liftIO $ takeMVar serverAddr - - liftIO $ takeMVar initDone - --replicateM_ 10 $ do - Just Pong <- callTimeout sid (Timeout (TimeInterval Seconds 10)) Ping - liftIO $ takeMVar pingDone - cast sid Pong - liftIO $ takeMVar pongDone - exit sid () - - liftIO $ takeMVar terminateDone - return () - - - --- | Test counter server --- TODO split me! -testCounter :: NT.Transport -> Assertion -testCounter transport = do - serverDone <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - runProcess localNode $ do - cid <- startCounter 0 - _ <- getCount cid - incCount cid - incCount cid - _ <- getCount cid - resetCount cid - _ <- getCount cid - terminateCounter cid - liftIO $ putMVar serverDone True - return () - - liftIO $ takeMVar serverDone - return () - - --- | Test kitty server --- TODO split me! -testKitty :: NT.Transport -> Assertion -testKitty transport = do - serverDone <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - runProcess localNode $ do - kPid <- startKitty [Cat "c1" "black" "a black cat"] - --replicateM_ 100 $ do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - closeShop kPid - terminateKitty kPid - liftIO $ putMVar serverDone True - return () - - liftIO $ takeMVar serverDone - return () - - - -tests :: NT.Transport -> [Test] -tests transport = [ - testGroup "Basic features" [ - testCase "Counter" (testCounter transport), - testCase "Kitty" (testKitty transport), - testCase "Ping" (testPing transport) - ] - ] - -genServerTests :: NT.Transport -> TransportInternals -> IO [Test] -genServerTests transport _ = do - return (tests transport) From f66cd840e6518690e4273940efa3094dc5e5bf18 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 14:16:08 +0000 Subject: [PATCH 0655/2357] oops - remove TestGenServer as it won't compile anymore --- tests/TestGenServer.hs | 162 ----------------------------------------- 1 file changed, 162 deletions(-) delete mode 100644 tests/TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs deleted file mode 100644 index c477888d..00000000 --- a/tests/TestGenServer.hs +++ /dev/null @@ -1,162 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -module TestGenServer where - -import Data.Binary (Binary (..)) -import Data.Typeable (Typeable) -import Data.DeriveTH -import Control.Concurrent (forkIO) -import Control.Concurrent.MVar - ( newEmptyMVar - , putMVar - , takeMVar - ) -import qualified Network.Transport as NT (Transport) -import Network.Transport.TCP (TransportInternals) -import Control.Distributed.Process.Internal.Types() -import Control.Distributed.Process (say, liftIO, exit) -import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() - -import Test.HUnit (Assertion) -import Test.Framework (Test, testGroup) -import Test.Framework.Providers.HUnit (testCase) - -import Control.Distributed.Process.Platform.GenServer -import Control.Distributed.Process.Platform.Internal.Types -import GenServer.Counter -import GenServer.Kitty - --------------------------------------------------------------------------------- --- The tests proper -- --------------------------------------------------------------------------------- - -data Ping = Ping - deriving (Typeable, Show) -$(derive makeBinary ''Ping) - -data Pong = Pong - deriving (Typeable, Show) -$(derive makeBinary ''Pong) - - --- | Test ping server --- TODO fix this test! -testPing :: NT.Transport -> Assertion -testPing transport = do - initDone <- newEmptyMVar - pingDone <- newEmptyMVar - pongDone <- newEmptyMVar - terminateDone <- newEmptyMVar - serverAddr <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - forkIO $ runProcess localNode $ do - say "Starting ..." - sid <- start (0 :: Int) defaultServer { - initHandler = do - --trace "Init ..." - c <- getState - liftIO $ putMVar initDone c - initOk Infinity, - terminateHandler = \_ -> do - --trace "Terminate ..." - c <- getState - liftIO $ putMVar terminateDone c - return (), - handlers = [ - handle (\Ping -> do - --trace "Ping ..." - modifyState (+1) - c <- getState - liftIO $ putMVar pingDone c - ok Pong), - handle (\Pong -> do - --trace "Pong ..." - modifyState (1 +) - c <- getState - liftIO $ putMVar pongDone c - ok ()) - ]} - liftIO $ putMVar serverAddr sid - return () - - forkIO $ runProcess localNode $ do - sid <- liftIO $ takeMVar serverAddr - - liftIO $ takeMVar initDone - --replicateM_ 10 $ do - Just Pong <- callTimeout sid (Timeout (TimeInterval Seconds 10)) Ping - liftIO $ takeMVar pingDone - cast sid Pong - liftIO $ takeMVar pongDone - exit sid () - - liftIO $ takeMVar terminateDone - return () - - - --- | Test counter server --- TODO split me! -testCounter :: NT.Transport -> Assertion -testCounter transport = do - serverDone <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - runProcess localNode $ do - cid <- startCounter 0 - _ <- getCount cid - incCount cid - incCount cid - _ <- getCount cid - resetCount cid - _ <- getCount cid - terminateCounter cid - liftIO $ putMVar serverDone True - return () - - liftIO $ takeMVar serverDone - return () - - --- | Test kitty server --- TODO split me! -testKitty :: NT.Transport -> Assertion -testKitty transport = do - serverDone <- newEmptyMVar - - localNode <- newLocalNode transport initRemoteTable - - runProcess localNode $ do - kPid <- startKitty [Cat "c1" "black" "a black cat"] - --replicateM_ 100 $ do - cat1 <- orderCat kPid "c1" "black" "a black cat" - cat2 <- orderCat kPid "c2" "black" "a black cat" - returnCat kPid cat1 - returnCat kPid cat2 - closeShop kPid - terminateKitty kPid - liftIO $ putMVar serverDone True - return () - - liftIO $ takeMVar serverDone - return () - - - -tests :: NT.Transport -> [Test] -tests transport = [ - testGroup "Basic features" [ - testCase "Counter" (testCounter transport), - testCase "Kitty" (testKitty transport), - testCase "Ping" (testPing transport) - ] - ] - -genServerTests :: NT.Transport -> TransportInternals -> IO [Test] -genServerTests transport _ = do - return (tests transport) From 2cf0a7869b5ed3d6a2de2ee899ea0a7173aa6453 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 11 Jan 2013 14:40:44 +0000 Subject: [PATCH 0656/2357] cancel{Wait} and wait{Timeout} for AsyncSTM --- src/Control/Distributed/Process/Platform/Internal/Types.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index b7b9d57b..94780ddc 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -9,6 +9,7 @@ module Control.Distributed.Process.Platform.Internal.Types , getTag , RegisterSelf(..) , CancelWait(..) + , Channel ) where import Control.Distributed.Process @@ -17,6 +18,8 @@ import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) +type Channel a = (SendPort a, ReceivePort a) + -- | Used internally in whereisOrStart. Send as (RegisterSelf,ProcessId). data RegisterSelf = RegisterSelf deriving Typeable instance Binary RegisterSelf where From 0a8a91278e5066ebc3117c495e54faf4d4e85852 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 12 Jan 2013 10:29:40 +0000 Subject: [PATCH 0657/2357] handle timeouts properly - it's microseconds not milliseconds! --- .../Process/Platform/GenProcess.hs | 8 ++--- .../Distributed/Process/Platform/GenServer.hs | 2 +- .../Distributed/Process/Platform/Time.hs | 31 +++++++++++-------- .../Distributed/Process/Platform/Timer.hs | 20 +++++------- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 41c1f559..56bcf412 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -212,17 +212,17 @@ loop s t = do nextAction _ (ProcessStop r) = return (TerminateReason r) processReceive :: [Dispatcher s] -> Delay -> Process s ProcessAction -processReceive ds timeout = do +processReceive ds t = do s <- getState let ms = map (matchMessage s) ds -- TODO: should we drain the message queue to avoid selective receive here? - case timeout of + case t of Infinity -> do (s', r) <- ST.lift $ BaseProcess.receiveWait ms putState s' return r - Delay t -> do - result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms + Delay t' -> do + result <- ST.lift $ BaseProcess.receiveTimeout (asTimeout t') ms case result of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index bc5defdf..5d21c91e 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -334,7 +334,7 @@ processReceive ds t = do putState s' return r Delay t' -> do - mayResult <- lift $ receiveTimeout (intervalToMs t') ms + mayResult <- lift $ receiveTimeout (asTimeout t') ms case mayResult of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index 7984ca1a..209ee4f6 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -22,11 +22,12 @@ module Control.Distributed.Process.Platform.Time ( -- time interval handling - milliseconds + microSeconds + , milliSeconds , seconds , minutes , hours - , intervalToMs + , asTimeout , timeToMs , TimeInterval , TimeUnit @@ -53,7 +54,7 @@ import Data.Typeable (Typeable) -------------------------------------------------------------------------------- -- | Defines the time unit for a Timeout value -data TimeUnit = Days | Hours | Minutes | Seconds | Millis +data TimeUnit = Days | Hours | Minutes | Seconds | Millis | Micros deriving (Typeable, Show) $(derive makeBinary ''TimeUnit) @@ -80,12 +81,15 @@ instance Binary TimeoutNotification where -- time interval/unit handling (milliseconds) -- | converts the supplied @TimeInterval@ to milliseconds -intervalToMs :: TimeInterval -> Int -intervalToMs (TimeInterval u v) = timeToMs u v +asTimeout :: TimeInterval -> Int +asTimeout (TimeInterval u v) = timeToMs u v + +microSeconds :: Int -> TimeInterval +microSeconds = TimeInterval Micros -- | given a number, produces a @TimeInterval@ of milliseconds -milliseconds :: Int -> TimeInterval -milliseconds = TimeInterval Millis +milliSeconds :: Int -> TimeInterval +milliSeconds = TimeInterval Millis -- | given a number, produces a @TimeInterval@ of seconds seconds :: Int -> TimeInterval @@ -101,13 +105,14 @@ hours = TimeInterval Hours -- TODO: timeToMs is not exactly efficient and we may want to scale it up --- | converts the supplied @TimeUnit@ to milliseconds +-- | converts the supplied @TimeUnit@ to microseconds timeToMs :: TimeUnit -> Int -> Int -timeToMs Millis ms = ms -timeToMs Seconds sec = sec * 1000 -timeToMs Minutes mins = (mins * 60) * 1000 -timeToMs Hours hrs = ((hrs * 60) * 60) * 1000 -timeToMs Days days = (((days * 24) * 60) * 60) * 1000 +timeToMs Micros us = us +timeToMs Millis ms = ms * (10 ^ (3 :: Int)) +timeToMs Seconds sec = sec * (10 ^ (6 :: Int)) +timeToMs Minutes mins = (mins * 60) * (10 ^ (6 :: Int)) +timeToMs Hours hrs = ((hrs * 60) * 60) * (10 ^ (6 :: Int)) +timeToMs Days days = (((days * 24) * 60) * 60) * (10 ^ (6 :: Int)) -- timeouts/delays (microseconds) diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index 346e9fab..1268e196 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -51,8 +51,9 @@ $(derive makeBinary ''SleepingPill) -- scheduling) to differ from threadDelay and/or operating system specific -- functions that offer the same results. sleep :: TimeInterval -> Process () -sleep t = do - let ms = intervalToMs t +sleep t = + let ms = asTimeout t in do + -- liftIO $ putStrLn $ "sleeping for " ++ (show ms) ++ "micros" _ <- receiveTimeout ms [matchIf (\SleepingPill -> True) (\_ -> return ())] return () @@ -60,7 +61,8 @@ sleep t = do -- | starts a timer which sends the supplied message to the destination process -- after the specified time interval. sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -sendAfter t pid msg = runAfter t (mkSender pid msg) +sendAfter t pid msg = runAfter t proc + where proc = do { send pid msg } -- | runs the supplied process action(s) after `t' has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef @@ -70,7 +72,7 @@ runAfter t p = spawnLocal $ runTimer t p True -- process each time the specified time interval elapses. To stop messages from -- being sent in future, cancelTimer can be called. startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef -startTimer t pid msg = periodically t (mkSender pid msg) +startTimer t pid msg = periodically t (send pid msg) -- | runs the supplied process action(s) repeatedly at intervals of `t' periodically :: TimeInterval -> Process () -> Process TimerRef @@ -100,7 +102,7 @@ flushTimer ref ignore t = do return () where performFlush mRef Infinity = receiveWait $ filters mRef performFlush mRef (Delay i) = - receiveTimeout (intervalToMs i) (filters mRef) >> return () + receiveTimeout (asTimeout i) (filters mRef) >> return () filters mRef = [ matchIf (\x -> x == ignore) (\_ -> return ()) @@ -118,7 +120,7 @@ ticker t pid = startTimer t pid Tick -- runs the timer process runTimer :: TimeInterval -> Process () -> Bool -> Process () runTimer t proc cancelOnReset = do - cancel <- expectTimeout (intervalToMs t) + cancel <- expectTimeout (asTimeout t) -- say $ "cancel = " ++ (show cancel) ++ "\n" case cancel of Nothing -> runProc cancelOnReset @@ -127,9 +129,3 @@ runTimer t proc cancelOnReset = do else runTimer t proc cancelOnReset where runProc True = proc runProc False = proc >> runTimer t proc cancelOnReset - --- create a 'sender' action for dispatching `msg' to `pid' -mkSender :: (Serializable a) => ProcessId -> a -> Process () -mkSender pid msg = do - -- say "sending\n" - send pid msg From b81045b66be7ef4468ceaf2053799b61b6f52d2c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 12 Jan 2013 10:29:40 +0000 Subject: [PATCH 0658/2357] handle timeouts properly - it's microseconds not milliseconds! --- src/Control/Distributed/Process/AsyncChan.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 90c3b9d8..ca7975a4 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -205,7 +205,7 @@ wait hAsync = receiveChan $ snd (channel hAsync) waitTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = - receiveChanTimeout (intervalToMs t) $ snd (channel hAsync) + receiveChanTimeout (asTimeout t) $ snd (channel hAsync) -- | Wait for an asynchronous operation to complete or timeout. If it times out, -- then 'cancelWait' the async handle instead. @@ -242,7 +242,7 @@ waitAnyTimeout :: (Serializable a) -> Process (Maybe (AsyncResult a)) waitAnyTimeout delay asyncs = let ports = map (snd . channel) asyncs - in mergePortsBiased ports >>= receiveChanTimeout (intervalToMs delay) + in mergePortsBiased ports >>= receiveChanTimeout (asTimeout delay) -- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. -- To wait for cancellation to complete, use 'cancelWait' instead. The notes From 4bf245639e5e410d409eddf8992f4fb8ff373b79 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 12 Jan 2013 10:29:40 +0000 Subject: [PATCH 0659/2357] handle timeouts properly - it's microseconds not milliseconds! --- src/Control/Distributed/Process/Platform/GenProcess.hs | 8 ++++---- src/Control/Distributed/Process/Platform/GenServer.hs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 41c1f559..56bcf412 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -212,17 +212,17 @@ loop s t = do nextAction _ (ProcessStop r) = return (TerminateReason r) processReceive :: [Dispatcher s] -> Delay -> Process s ProcessAction -processReceive ds timeout = do +processReceive ds t = do s <- getState let ms = map (matchMessage s) ds -- TODO: should we drain the message queue to avoid selective receive here? - case timeout of + case t of Infinity -> do (s', r) <- ST.lift $ BaseProcess.receiveWait ms putState s' return r - Delay t -> do - result <- ST.lift $ BaseProcess.receiveTimeout (intervalToMs t) ms + Delay t' -> do + result <- ST.lift $ BaseProcess.receiveTimeout (asTimeout t') ms case result of Just (s', r) -> do putState s' diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index bc5defdf..5d21c91e 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -334,7 +334,7 @@ processReceive ds t = do putState s' return r Delay t' -> do - mayResult <- lift $ receiveTimeout (intervalToMs t') ms + mayResult <- lift $ receiveTimeout (asTimeout t') ms case mayResult of Just (s', r) -> do putState s' From 1524b3f218e3b51412c657b8185bb898b151077a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 12 Jan 2013 12:18:37 +0000 Subject: [PATCH 0660/2357] literate APIs for time/timeout --- .../Distributed/Process/Platform/Time.hs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index 209ee4f6..fc52c459 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -28,9 +28,11 @@ module Control.Distributed.Process.Platform.Time , minutes , hours , asTimeout + , after + , within , timeToMs , TimeInterval - , TimeUnit + , TimeUnit(..) , Delay(..) -- timeouts @@ -84,6 +86,20 @@ instance Binary TimeoutNotification where asTimeout :: TimeInterval -> Int asTimeout (TimeInterval u v) = timeToMs u v +-- | Convenience for making timeouts; e.g., +-- +-- > receiveTimeout (after 3 Seconds) [ match (\"ok" -> return ()) ] +-- +after :: Int -> TimeUnit -> Int +after n m = timeToMs m n + +-- | Convenience for making 'TimeInterval'; e.g., +-- +-- > let ti = within 5 Seconds in ..... +-- +within :: Int -> TimeUnit -> TimeInterval +within n m = TimeInterval m n + microSeconds :: Int -> TimeInterval microSeconds = TimeInterval Micros From 76f4db2e3c80854b645105eef2e7d509cac9b255 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 12 Jan 2013 18:23:02 +0000 Subject: [PATCH 0661/2357] Async API for local and remote work - fixes #8 --- src/Control/Distributed/Process/Async.hs | 20 +++++++++++++++----- src/Control/Distributed/Process/AsyncChan.hs | 14 +++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 80e44100..62cf0a75 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -30,11 +30,12 @@ module Control.Distributed.Process.Platform.Async ( -- types/data AsyncRef - , AsyncTask + , AsyncTask(..) , AsyncResult(..) + , asyncDo ) where import Control.Distributed.Process - +import Control.Distributed.Process.Serializable (SerializableDict) import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -48,9 +49,15 @@ type AsyncRef = ProcessId -- | A task to be performed asynchronously. This can either take the -- form of an action that runs over some type @a@ in the @Process@ monad, --- or a tuple that adds the node on which the asynchronous task should be --- spawned - in the @Process a@ case the task is spawned on the local node -type AsyncTask a = Process a +-- or a static 'SerializableDict' and @Closure (Process a)@ neccessary for the +-- task to be spawned on a remote node. +data AsyncTask a = + AsyncTask { asyncTask :: Process a } + | AsyncRemoteTask { + asyncTaskDict :: Static (SerializableDict a) + , asyncTaskNode :: NodeId + , asyncTaskProc :: Closure (Process a) + } -- | Represents the result of an asynchronous action, which can be in one of -- several states at any given time. @@ -65,3 +72,6 @@ $(derive makeBinary ''AsyncResult) deriving instance Eq a => Eq (AsyncResult a) deriving instance Show a => Show (AsyncResult a) + +asyncDo :: Process a -> AsyncTask a +asyncDo = AsyncTask diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index ca7975a4..0de81bcb 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -47,7 +47,7 @@ module Control.Distributed.Process.Platform.Async.AsyncChan , waitCheckTimeout ) where -import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Async hiding (asyncDo) import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Internal.Types import Control.Distributed.Process @@ -67,7 +67,9 @@ type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- Handles of this type cannot cross remote boundaries. Furthermore, handles -- of this type /must not/ be passed to functions in this module by processes -- other than the caller of 'async' - that is, this module provides asynchronous --- actions whose results are accessible *only* by the initiating process. +-- actions whose results are accessible *only* by the initiating process. This +-- limitation is imposed becuase of the use of type channels, for which the +-- @ReceivePort@ component is effectively /thread local/. -- -- See 'async' data AsyncChan a = AsyncChan { @@ -109,8 +111,10 @@ asyncLinked :: (Serializable a) => AsyncTask a -> Process (AsyncChan a) asyncLinked = async asyncDo :: (Serializable a) => Bool -> AsyncTask a -> Process (AsyncChan a) -asyncDo shouldLink task = do - (wpid, gpid, chan) <- spawnWorkers task shouldLink +asyncDo shouldLink (AsyncRemoteTask d n c) = + let proc = call d n c in asyncDo shouldLink AsyncTask { asyncTask = proc } +asyncDo shouldLink (AsyncTask proc) = do + (wpid, gpid, chan) <- spawnWorkers proc shouldLink return AsyncChan { worker = wpid , insulator = gpid @@ -118,7 +122,7 @@ asyncDo shouldLink task = do } spawnWorkers :: (Serializable a) - => AsyncTask a + => Process a -> Bool -> Process (AsyncRef, AsyncRef, InternalChannel a) spawnWorkers task shouldLink = do From 0d879586757c32e3572085799153acbf5cbe918f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 12 Jan 2013 23:47:06 +0000 Subject: [PATCH 0662/2357] kill some more compiler warnings --- src/Control/Distributed/Process/Platform/Call.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Call.hs b/src/Control/Distributed/Process/Platform/Call.hs index 476fc4d2..5368bc18 100644 --- a/src/Control/Distributed/Process/Platform/Call.hs +++ b/src/Control/Distributed/Process/Platform/Call.hs @@ -85,7 +85,7 @@ multicall nodes msg tag time = (\_ -> error "multicall: unexpected termination of worker process") ] where - recv nodes monitortags mon_caller = + recv nodes' monitortags mon_caller = do let ordered [] _ = [] @@ -109,8 +109,8 @@ multicall nodes msg tag time = (\_ -> return Nothing) ] >>= maybe (return results) recv1 - resultmap <- recv1 (nodes, monitortags, M.empty) :: Process (M.Map ProcessId b) - return $ ordered nodes resultmap + resultmap <- recv1 (nodes', monitortags, M.empty) :: Process (M.Map ProcessId b) + return $ ordered nodes' resultmap data MulticallResponseType a = MulticallAccept From 1bbec27f849631a5415223b9cce002e70d63a8ea Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Thu, 10 Jan 2013 10:35:17 -0500 Subject: [PATCH 0663/2357] (Gen) Process => GenProcess, silence warnings, cosmetic - closes #53 --- .../Distributed/Process/Platform/Call.hs | 55 +++++++++---------- .../Process/Platform/GenProcess.hs | 30 +++++----- .../Distributed/Process/Platform/GenServer.hs | 7 +-- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Call.hs b/src/Control/Distributed/Process/Platform/Call.hs index 5368bc18..7fa497d4 100644 --- a/src/Control/Distributed/Process/Platform/Call.hs +++ b/src/Control/Distributed/Process/Platform/Call.hs @@ -16,10 +16,10 @@ -- Clients make synchronous calls to a running process (i.e., server) using the -- 'callAt', 'callTimeout' and 'multicall' functions. Processes acting as the -- server are constructed using Cloud Haskell's 'receive' family of primitives --- and the 'callResponse' family of functions in this module. +-- and the 'callResponse' family of functions in this module. ----------------------------------------------------------------------------- -module Control.Distributed.Process.Platform.Call +module Control.Distributed.Process.Platform.Call ( -- client API callAt , callTimeout @@ -54,8 +54,8 @@ import Control.Distributed.Process.Platform.Time -- The tag is per-process unique identifier of the transaction. If the timeout expires -- or the target process dies, Nothing will be returned. callTimeout :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Timeout -> Process (Maybe b) -callTimeout pid msg tag time = - do res <- multicall [pid] msg tag time +callTimeout pid msg tag time = + do res <- multicall [pid] msg tag time return $ join (listToMaybe res) -- | Like 'callTimeout', but with no timeout. Returns Nothing if the target process dies. @@ -85,11 +85,11 @@ multicall nodes msg tag time = (\_ -> error "multicall: unexpected termination of worker process") ] where - recv nodes' monitortags mon_caller = - do + recv nodes' monitortags mon_caller = + do let ordered [] _ = [] - ordered (x:xs) m = + ordered (x:xs) m = M.lookup x m : ordered xs m recv1 ([],_,results) = return results recv1 (_,[],results) = return results @@ -98,12 +98,12 @@ multicall nodes msg tag time = [ matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mon_caller) (\_ -> return Nothing), - matchIf (\(ProcessMonitorNotification ref pid reason) -> + matchIf (\(ProcessMonitorNotification ref pid reason) -> ref `elem` monitortagsleft && pid `elem` nodesleft && reason /= DiedNormal) - (\(ProcessMonitorNotification ref pid _reason) -> + (\(ProcessMonitorNotification ref pid _reason) -> return $ Just (delete pid nodesleft, delete ref monitortagsleft, results)), matchIf (\(MulticallResponse,mtag,_,_) -> mtag == tag) - (\(MulticallResponse,_,responder,msgx) -> + (\(MulticallResponse,_,responder,msgx) -> return $ Just (delete responder nodesleft, monitortagsleft, M.insert responder (msgx::b) results)), matchIf (\(TimeoutNotification mtag) -> mtag == tag ) (\_ -> return Nothing) @@ -117,14 +117,14 @@ data MulticallResponseType a = | MulticallForward ProcessId a | MulticallReject deriving Eq -callResponseImpl :: (Serializable a,Serializable b) => (a -> MulticallResponseType c) -> +callResponseImpl :: (Serializable a,Serializable b) => (a -> MulticallResponseType c) -> (a -> (b -> Process())-> Process c) -> Match c -callResponseImpl cond proc = - matchIf (\(Multicall,_responder,_,_,msg) -> +callResponseImpl cond proc = + matchIf (\(Multicall,_responder,_,_,msg) -> case cond msg of MulticallReject -> False - _ -> True) - (\wholemsg@(Multicall,responder,sender,tag,msg) -> + _ -> True) + (\wholemsg@(Multicall,responder,sender,tag,msg) -> case cond msg of MulticallForward target ret -> -- TODO sender should get a ProcessMonitorNotification if target dies, or we should link target do send target wholemsg @@ -138,7 +138,7 @@ callResponseImpl cond proc = -- callResponse will respond to a message of type a sent by 'callTimeout', and will respond with -- a value of type b. callResponse :: (Serializable a,Serializable b) => (a -> Process (b,c)) -> Match c -callResponse = +callResponse = callResponseIf (const True) callResponseDeferIf :: (Serializable a,Serializable b) => (a -> Bool) -> (a -> (b -> Process())-> Process c) -> Match c @@ -154,8 +154,8 @@ callResponseDefer = callResponseDeferIf (const True) -- responsibility to ensure that the forwarding process is linked to the destination process, so that if -- it fails, the sender will be notified. callForward :: Serializable a => (a -> (ProcessId, c)) -> Match c -callForward proc = - callResponseImpl +callForward proc = + callResponseImpl (\msg -> let (pid, ret) = proc msg in MulticallForward pid ret ) (\_ sender -> (sender::(() -> Process ())) `mention` error "multicallForward: Indecisive condition") @@ -165,28 +165,28 @@ callForward proc = -- handling thread dies, you'll need to call link yourself. callResponseAsync :: (Serializable a,Serializable b) => (a -> Maybe c) -> (a -> Process b) -> Match c callResponseAsync cond proc = - callResponseImpl - (\msg -> + callResponseImpl + (\msg -> case cond msg of Nothing -> MulticallReject Just _ -> MulticallAccept) - (\msg sender -> + (\msg sender -> do _ <- spawnLocal $ -- TODO linkOnFailure to spawned procss do val <- proc msg sender val case cond msg of Nothing -> error "multicallResponseAsync: Indecisive condition" - Just ret -> return ret ) + Just ret -> return ret ) callResponseIf :: (Serializable a,Serializable b) => (a -> Bool) -> (a -> Process (b,c)) -> Match c -callResponseIf cond proc = +callResponseIf cond proc = callResponseImpl - (\msg -> + (\msg -> case cond msg of True -> MulticallAccept - False -> MulticallReject) - (\msg sender -> - do (tosend,toreturn) <- proc msg + False -> MulticallReject) + (\msg sender -> + do (tosend,toreturn) <- proc msg sender tosend return toreturn) @@ -211,4 +211,3 @@ data MulticallResponse = MulticallResponse instance Binary MulticallResponse where get = return MulticallResponse put _ = return () - diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 56bcf412..5ea74f96 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -19,8 +19,6 @@ import qualified Control.Monad.State as ST (StateT, get, put, runStateT) import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Internal.Types -import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH @@ -61,12 +59,12 @@ data ProcessAction = deriving (Typeable) $(derive makeBinary ''ProcessAction) -type Process s = ST.StateT s BaseProcess.Process +type GenProcess s = ST.StateT s BaseProcess.Process -- | Handlers -type InitHandler s = Process s InitResult -type TerminateHandler s = TerminateReason -> Process s () -type RequestHandler s a = Message a -> Process s ProcessAction +type InitHandler s = GenProcess s InitResult +type TerminateHandler s = TerminateReason -> GenProcess s () +type RequestHandler s a = Message a -> GenProcess s ProcessAction -- | Contains the actual payload and possibly additional routing metadata data Message a = Message ReplyTo a @@ -168,15 +166,15 @@ handleRequestIf cond handler = DispatchIf { -- process state management -- | gets the process state -getState :: Process s s +getState :: GenProcess s s getState = ST.get -- | sets the process state -putState :: s -> Process s () +putState :: s -> GenProcess s () putState = ST.put -- | modifies the server state -modifyState :: (s -> s) -> Process s () +modifyState :: (s -> s) -> GenProcess s () modifyState = ST.modify -------------------------------------------------------------------------------- @@ -184,7 +182,7 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -runProc :: Behaviour s -> Process s () +runProc :: Behaviour s -> GenProcess s () runProc s = do ir <- init s tr <- case ir of @@ -195,23 +193,23 @@ runProc s = do terminate s tr -- | initialize server -init :: Behaviour s -> Process s InitResult +init :: Behaviour s -> GenProcess s InitResult init s = do trace $ "Server initializing ... " ir <- initHandler s return ir -loop :: Behaviour s -> Delay -> Process s TerminateReason +loop :: Behaviour s -> Delay -> GenProcess s TerminateReason loop s t = do s' <- processReceive (dispatchers s) t nextAction s s' where nextAction :: Behaviour s -> ProcessAction -> - Process s TerminateReason + GenProcess s TerminateReason nextAction b ProcessContinue = loop b t nextAction b (ProcessTimeout t') = loop b t' nextAction _ (ProcessStop r) = return (TerminateReason r) -processReceive :: [Dispatcher s] -> Delay -> Process s ProcessAction +processReceive :: [Dispatcher s] -> Delay -> GenProcess s ProcessAction processReceive ds t = do s <- getState let ms = map (matchMessage s) ds @@ -230,13 +228,13 @@ processReceive ds t = do Nothing -> do return $ ProcessStop "timed out" -terminate :: Behaviour s -> TerminateReason -> Process s () +terminate :: Behaviour s -> TerminateReason -> GenProcess s () terminate s reason = do trace $ "Server terminating: " ++ show reason (terminateHandler s) reason -- | Log a trace message using the underlying Process's say -trace :: String -> Process s () +trace :: String -> GenProcess s () trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index 5d21c91e..983a4d4e 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -56,11 +56,7 @@ import Control.Distributed.Process (AbstractMessage, Match, Process, ProcessId, - monitor, - link, - expect, expectTimeout, - monitor, unmonitor, - finally, + monitor, link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, @@ -70,7 +66,6 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Platform.Time -import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.Async.AsyncChan import Data.Binary (Binary (..), getWord8, putWord8) From 54d3c443ceb8375f548219827679e589d0710e43 Mon Sep 17 00:00:00 2001 From: Eric B Merritt Date: Thu, 10 Jan 2013 10:35:17 -0500 Subject: [PATCH 0664/2357] (Gen) Process => GenProcess, silence warnings, cosmetic - closes #53 --- .../Process/Platform/GenProcess.hs | 30 +++++++++---------- .../Distributed/Process/Platform/GenServer.hs | 7 +---- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 56bcf412..5ea74f96 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -19,8 +19,6 @@ import qualified Control.Monad.State as ST (StateT, get, put, runStateT) import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Internal.Types -import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH @@ -61,12 +59,12 @@ data ProcessAction = deriving (Typeable) $(derive makeBinary ''ProcessAction) -type Process s = ST.StateT s BaseProcess.Process +type GenProcess s = ST.StateT s BaseProcess.Process -- | Handlers -type InitHandler s = Process s InitResult -type TerminateHandler s = TerminateReason -> Process s () -type RequestHandler s a = Message a -> Process s ProcessAction +type InitHandler s = GenProcess s InitResult +type TerminateHandler s = TerminateReason -> GenProcess s () +type RequestHandler s a = Message a -> GenProcess s ProcessAction -- | Contains the actual payload and possibly additional routing metadata data Message a = Message ReplyTo a @@ -168,15 +166,15 @@ handleRequestIf cond handler = DispatchIf { -- process state management -- | gets the process state -getState :: Process s s +getState :: GenProcess s s getState = ST.get -- | sets the process state -putState :: s -> Process s () +putState :: s -> GenProcess s () putState = ST.put -- | modifies the server state -modifyState :: (s -> s) -> Process s () +modifyState :: (s -> s) -> GenProcess s () modifyState = ST.modify -------------------------------------------------------------------------------- @@ -184,7 +182,7 @@ modifyState = ST.modify -------------------------------------------------------------------------------- -- | server process -runProc :: Behaviour s -> Process s () +runProc :: Behaviour s -> GenProcess s () runProc s = do ir <- init s tr <- case ir of @@ -195,23 +193,23 @@ runProc s = do terminate s tr -- | initialize server -init :: Behaviour s -> Process s InitResult +init :: Behaviour s -> GenProcess s InitResult init s = do trace $ "Server initializing ... " ir <- initHandler s return ir -loop :: Behaviour s -> Delay -> Process s TerminateReason +loop :: Behaviour s -> Delay -> GenProcess s TerminateReason loop s t = do s' <- processReceive (dispatchers s) t nextAction s s' where nextAction :: Behaviour s -> ProcessAction -> - Process s TerminateReason + GenProcess s TerminateReason nextAction b ProcessContinue = loop b t nextAction b (ProcessTimeout t') = loop b t' nextAction _ (ProcessStop r) = return (TerminateReason r) -processReceive :: [Dispatcher s] -> Delay -> Process s ProcessAction +processReceive :: [Dispatcher s] -> Delay -> GenProcess s ProcessAction processReceive ds t = do s <- getState let ms = map (matchMessage s) ds @@ -230,13 +228,13 @@ processReceive ds t = do Nothing -> do return $ ProcessStop "timed out" -terminate :: Behaviour s -> TerminateReason -> Process s () +terminate :: Behaviour s -> TerminateReason -> GenProcess s () terminate s reason = do trace $ "Server terminating: " ++ show reason (terminateHandler s) reason -- | Log a trace message using the underlying Process's say -trace :: String -> Process s () +trace :: String -> GenProcess s () trace msg = ST.lift . BaseProcess.say $ msg -- data Upgrade = ??? diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs index 5d21c91e..983a4d4e 100644 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ b/src/Control/Distributed/Process/Platform/GenServer.hs @@ -56,11 +56,7 @@ import Control.Distributed.Process (AbstractMessage, Match, Process, ProcessId, - monitor, - link, - expect, expectTimeout, - monitor, unmonitor, - finally, + monitor, link, exit, getSelfPid, match, matchAny, matchIf, receiveTimeout, @@ -70,7 +66,6 @@ import Control.Distributed.Process (AbstractMessage, import Control.Distributed.Process.Internal.Types (MonitorRef) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Platform.Time -import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.Async.AsyncChan import Data.Binary (Binary (..), getWord8, putWord8) From 927bdb84efd9d01e467bcf7e87d11830a4f79875 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 13 Jan 2013 01:07:54 +0000 Subject: [PATCH 0665/2357] cosmetic (ish) --- .../Distributed/Process/Platform/Call.hs | 194 ++++++++++-------- 1 file changed, 112 insertions(+), 82 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Call.hs b/src/Control/Distributed/Process/Platform/Call.hs index 7fa497d4..00f49389 100644 --- a/src/Control/Distributed/Process/Platform/Call.hs +++ b/src/Control/Distributed/Process/Platform/Call.hs @@ -49,21 +49,26 @@ import Control.Distributed.Process.Platform.Time -- * Multicall ---------------------------------------------- --- | Sends a message of type a to the given process, to be handled by a corresponding --- callResponse... function, which will send back a message of type b. --- The tag is per-process unique identifier of the transaction. If the timeout expires --- or the target process dies, Nothing will be returned. -callTimeout :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Timeout -> Process (Maybe b) +-- | Sends a message of type a to the given process, to be handled by a +-- corresponding callResponse... function, which will send back a message of +-- type b. The tag is per-process unique identifier of the transaction. If the +-- timeout expires or the target process dies, Nothing will be returned. +callTimeout :: (Serializable a, Serializable b) + => ProcessId -> a -> Tag -> Timeout -> Process (Maybe b) callTimeout pid msg tag time = do res <- multicall [pid] msg tag time return $ join (listToMaybe res) --- | Like 'callTimeout', but with no timeout. Returns Nothing if the target process dies. -callAt :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Process (Maybe b) +-- | Like 'callTimeout', but with no timeout. +-- Returns Nothing if the target process dies. +callAt :: (Serializable a, Serializable b) + => ProcessId -> a -> Tag -> Process (Maybe b) callAt pid msg tag = callTimeout pid msg tag infiniteWait --- | Like 'callTimeout', but sends the message to multiple recipients and collects the results. -multicall :: forall a b.(Serializable a, Serializable b) => [ProcessId] -> a -> Tag -> Timeout -> Process [Maybe b] +-- | Like 'callTimeout', but sends the message to multiple +-- recipients and collects the results. +multicall :: forall a b.(Serializable a, Serializable b) + => [ProcessId] -> a -> Tag -> Timeout -> Process [Maybe b] multicall nodes msg tag time = do caller <- getSelfPid reciever <- spawnLocal $ @@ -71,99 +76,123 @@ multicall nodes msg tag time = mon_caller <- monitor caller () <- expect monitortags <- forM nodes monitor - forM_ nodes $ \node -> send node (Multicall, node, reciever_pid, tag, msg) + forM_ nodes $ \node -> send node (Multicall, node, + reciever_pid, tag, msg) maybeTimeout time tag reciever_pid results <- recv nodes monitortags mon_caller send caller (MulticallResponse,tag,results) mon_reciever <- monitor reciever send reciever () - receiveWait - [ + receiveWait [ matchIf (\(MulticallResponse,mtag,_) -> mtag == tag) (\(MulticallResponse,_,val) -> return val), - matchIf (\(ProcessMonitorNotification ref _pid reason) -> ref == mon_reciever && reason /= DiedNormal) - (\_ -> error "multicall: unexpected termination of worker process") + matchIf (\(ProcessMonitorNotification ref _pid reason) + -> ref == mon_reciever && reason /= DiedNormal) + (\_ -> error "multicall: unexpected termination of worker") ] - where - recv nodes' monitortags mon_caller = - do - let - ordered [] _ = [] - ordered (x:xs) m = - M.lookup x m : ordered xs m - recv1 ([],_,results) = return results - recv1 (_,[],results) = return results - recv1 (nodesleft,monitortagsleft,results) = - receiveWait - [ - matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mon_caller) - (\_ -> return Nothing), - matchIf (\(ProcessMonitorNotification ref pid reason) -> - ref `elem` monitortagsleft && pid `elem` nodesleft && reason /= DiedNormal) - (\(ProcessMonitorNotification ref pid _reason) -> - return $ Just (delete pid nodesleft, delete ref monitortagsleft, results)), - matchIf (\(MulticallResponse,mtag,_,_) -> mtag == tag) - (\(MulticallResponse,_,responder,msgx) -> - return $ Just (delete responder nodesleft, monitortagsleft, M.insert responder (msgx::b) results)), - matchIf (\(TimeoutNotification mtag) -> mtag == tag ) - (\_ -> return Nothing) - ] - >>= maybe (return results) recv1 - resultmap <- recv1 (nodes', monitortags, M.empty) :: Process (M.Map ProcessId b) - return $ ordered nodes' resultmap + where + recv nodes' monitortags mon_caller = do + resultmap <- recv1 mon_caller + (nodes', monitortags, M.empty) :: Process (M.Map ProcessId b) + return $ ordered nodes' resultmap + + ordered [] _ = [] + ordered (x:xs) m = M.lookup x m : ordered xs m + + recv1 _ ([],_,results) = return results + recv1 _ (_,[],results) = return results + recv1 ref (nodesleft,monitortagsleft,results) = + receiveWait [ + matchIf (\(ProcessMonitorNotification ref' _ _) + -> ref' == ref) + (\_ -> return Nothing) + , matchIf (\(ProcessMonitorNotification ref' pid reason) -> + ref' `elem` monitortagsleft && + pid `elem` nodesleft + && reason /= DiedNormal) + (\(ProcessMonitorNotification ref' pid _reason) -> + return $ Just (delete pid nodesleft, + delete ref' monitortagsleft, results)) + , matchIf (\(MulticallResponse, mtag, _, _) -> mtag == tag) + (\(MulticallResponse, _, responder, msgx) -> + return $ Just (delete responder nodesleft, + monitortagsleft, + M.insert responder (msgx :: b) results)) + , matchIf (\(TimeoutNotification mtag) -> mtag == tag ) + (\_ -> return Nothing) + ] + >>= maybe (return results) (recv1 ref) data MulticallResponseType a = MulticallAccept | MulticallForward ProcessId a | MulticallReject deriving Eq -callResponseImpl :: (Serializable a,Serializable b) => (a -> MulticallResponseType c) -> - (a -> (b -> Process())-> Process c) -> Match c +callResponseImpl :: (Serializable a,Serializable b) + => (a -> MulticallResponseType c) -> + (a -> (b -> Process())-> Process c) -> Match c callResponseImpl cond proc = - matchIf (\(Multicall,_responder,_,_,msg) -> - case cond msg of - MulticallReject -> False - _ -> True) - (\wholemsg@(Multicall,responder,sender,tag,msg) -> - case cond msg of - MulticallForward target ret -> -- TODO sender should get a ProcessMonitorNotification if target dies, or we should link target - do send target wholemsg - return ret - MulticallReject -> error "multicallResponseImpl: Indecisive condition" - MulticallAccept -> - let resultSender tosend = send sender (MulticallResponse,tag::Tag,responder::ProcessId, tosend) - in proc msg resultSender) - --- | Produces a Match that can be used with the 'receiveWait' family of message-receiving functions. --- callResponse will respond to a message of type a sent by 'callTimeout', and will respond with --- a value of type b. -callResponse :: (Serializable a,Serializable b) => (a -> Process (b,c)) -> Match c -callResponse = - callResponseIf (const True) - -callResponseDeferIf :: (Serializable a,Serializable b) => (a -> Bool) -> (a -> (b -> Process())-> Process c) -> Match c -callResponseDeferIf cond = callResponseImpl (\msg -> if cond msg then MulticallAccept else MulticallReject) - -callResponseDefer :: (Serializable a,Serializable b) => (a -> (b -> Process())-> Process c) -> Match c + matchIf (\(Multicall,_responder,_,_,msg) -> + case cond msg of + MulticallReject -> False + _ -> True) + (\wholemsg@(Multicall,responder,sender,tag,msg) -> + case cond msg of + -- TODO: sender should get a ProcessMonitorNotification if + -- our target dies, or we should link to it (?) + MulticallForward target ret -> send target wholemsg >> return ret + -- TODO: use `die Reason` when issue #110 is resolved + MulticallReject -> error "multicallResponseImpl: Indecisive condition" + MulticallAccept -> + let resultSender tosend = + send sender (MulticallResponse, + tag::Tag, + responder::ProcessId, + tosend) + in proc msg resultSender) + +-- | Produces a Match that can be used with the 'receiveWait' family of +-- message-receiving functions. @callResponse@ will respond to a message of +-- type a sent by 'callTimeout', and will respond with a value of type b. +callResponse :: (Serializable a,Serializable b) + => (a -> Process (b,c)) -> Match c +callResponse = callResponseIf (const True) + +callResponseDeferIf :: (Serializable a,Serializable b) + => (a -> Bool) + -> (a -> (b -> Process()) -> Process c) + -> Match c +callResponseDeferIf cond = + callResponseImpl (\msg -> + if cond msg + then MulticallAccept + else MulticallReject) + +callResponseDefer :: (Serializable a,Serializable b) + => (a -> (b -> Process())-> Process c) -> Match c callResponseDefer = callResponseDeferIf (const True) - --- | Produces a Match that can be used with the 'receiveWait' family of message-receiving functions. --- When calllForward receives a message of type from from 'callTimeout' (and similar), it will forward --- the message to another process, who will be responsible for responding to it. It is the user's --- responsibility to ensure that the forwarding process is linked to the destination process, so that if --- it fails, the sender will be notified. +-- | Produces a Match that can be used with the 'receiveWait' family of +-- message-receiving functions. When calllForward receives a message of type +-- from from 'callTimeout' (and similar), it will forward the message to another +-- process, who will be responsible for responding to it. It is the user's +-- responsibility to ensure that the forwarding process is linked to the +-- destination process, so that if it fails, the sender will be notified. callForward :: Serializable a => (a -> (ProcessId, c)) -> Match c callForward proc = callResponseImpl (\msg -> let (pid, ret) = proc msg - in MulticallForward pid ret ) - (\_ sender -> (sender::(() -> Process ())) `mention` error "multicallForward: Indecisive condition") - --- | The message handling code is started in a separate thread. It's not automatically --- linked to the calling thread, so if you want it to be terminated when the message --- handling thread dies, you'll need to call link yourself. -callResponseAsync :: (Serializable a,Serializable b) => (a -> Maybe c) -> (a -> Process b) -> Match c + in MulticallForward pid ret ) + (\_ sender -> + (sender::(() -> Process ())) `mention` + error "multicallForward: Indecisive condition") + +-- | The message handling code is started in a separate thread. It's not +-- automatically linked to the calling thread, so if you want it to be +-- terminated when the message handling thread dies, you'll need to call +-- link yourself. +callResponseAsync :: (Serializable a,Serializable b) + => (a -> Maybe c) -> (a -> Process b) -> Match c callResponseAsync cond proc = callResponseImpl (\msg -> @@ -178,7 +207,8 @@ callResponseAsync cond proc = Nothing -> error "multicallResponseAsync: Indecisive condition" Just ret -> return ret ) -callResponseIf :: (Serializable a,Serializable b) => (a -> Bool) -> (a -> Process (b,c)) -> Match c +callResponseIf :: (Serializable a,Serializable b) + => (a -> Bool) -> (a -> Process (b,c)) -> Match c callResponseIf cond proc = callResponseImpl (\msg -> From 1245e845610fadb9ae5eea8ce22286b830b4f049 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 09:44:41 +0000 Subject: [PATCH 0666/2357] do not use the state monad to implement GenProcess --- .../Process/Platform/GenProcess.hs | 406 +++++++++--------- 1 file changed, 196 insertions(+), 210 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5ea74f96..dfda8741 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -1,242 +1,228 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ImpredicativeTypes #-} module Control.Distributed.Process.Platform.GenProcess where -- TODO: define API and hide internals... -import qualified Control.Distributed.Process as BaseProcess -import qualified Control.Monad.State as ST (StateT, get, - lift, modify, - put, runStateT) - +import Control.Applicative +import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time +import qualified Control.Monad.State as ST + ( StateT + , get + , lift + , modify + , put + , runStateT + ) import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - - -type ServerName = String -type ServerPid = BaseProcess.ProcessId - -data ServerId = ServerProcess ServerPid | NamedServer ServerName - -data Recipient a = SendToPid BaseProcess.ProcessId | - SendToPort (BaseProcess.SendPort a) - --- | Initialize handler result -data InitResult = - InitOk Delay - | InitStop String - +import Data.Typeable (Typeable) +import Prelude hiding (init) + +data ServerId = ServerId ProcessId | ServerName String + +data Recipient = + SendToPid ProcessId + | SendToService String + | SendToRemoteService String NodeId + deriving (Typeable) +$(derive makeBinary ''Recipient) + +data Message a = + CastMessage { payload :: a } + | CallMessage { payload :: a, sender :: Recipient } + deriving (Typeable) +$(derive makeBinary ''Message) + -- | Terminate reason data TerminateReason = TerminateNormal | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - -data ReplyTo = ReplyTo BaseProcess.ProcessId | None - deriving (Typeable, Show) -$(derive makeBinary ''ReplyTo) - --- | The result of a call -data ProcessAction = - ProcessContinue - | ProcessTimeout Delay - | ProcessStop String - deriving (Typeable) -$(derive makeBinary ''ProcessAction) - -type GenProcess s = ST.StateT s BaseProcess.Process - --- | Handlers -type InitHandler s = GenProcess s InitResult -type TerminateHandler s = TerminateReason -> GenProcess s () -type RequestHandler s a = Message a -> GenProcess s ProcessAction - --- | Contains the actual payload and possibly additional routing metadata -data Message a = Message ReplyTo a - deriving (Show, Typeable) -$(derive makeBinary ''Message) + | forall r. (Serializable r) => + TerminateOther r + deriving (Typeable) + +-- | Initialization +data InitResult s = + InitOk s Delay + | forall r. (Serializable r) => InitStop r + +data ProcessAction s = + ProcessContinue { nextState :: s } + | ProcessTimeout { delay :: Delay, nextState :: s } + | ProcessHibernate { delay :: Delay, nextState :: s } + | ProcessStop { reason :: TerminateReason } + +data ProcessReply s a = + ProcessReply { response :: a + , action :: ProcessAction s } + | NoReply { action :: ProcessAction s} + +type InitHandler a s = a -> InitResult s +type TerminateHandler s = s -> TerminateReason -> Process () +type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) +type CallHandler s a b = s -> a -> Process (ProcessReply s b) +type CastHandler s a = s -> a -> Process (ProcessAction s) + +data Req a b = Req a b -data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) - deriving (Typeable) -$(derive makeBinary ''Rpc) +-- dispatching to implementation callbacks -- | Dispatcher that knows how to dispatch messages to a handler +-- s The server state data Dispatcher s = - forall a . (Serializable a) => - Dispatch { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction) } | - forall a . (Serializable a) => - DispatchIf { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction), - condition :: s -> Message a -> Bool } - --- dispatching to implementation callbacks + forall a . (Serializable a) => Dispatch { + dispatch :: s -> Message a -> Process (ProcessAction s) + } + | forall a . (Serializable a) => DispatchIf { + dispatch :: s -> Message a -> Process (ProcessAction s) + , dispatchIf :: s -> Message a -> Bool + } + | forall a . (Serializable a) => DispatchReply { + handle :: s -> Message a -> Process (ProcessAction s) + } + | forall a . (Serializable a) => DispatchReplyIf { + handle :: s -> Message a -> Process (ProcessAction s) + , handleIf :: s -> Message a -> Bool + } -- | Matches messages using a dispatcher -class Dispatchable d where - matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) +class MessageMatcher d where + matchMessage :: s -> d s -> Match (ProcessAction s) -- | Matches messages to a MessageDispatcher -instance Dispatchable Dispatcher where - matchMessage s (Dispatch d ) = BaseProcess.match (d s) - matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) - +instance MessageMatcher Dispatcher where + matchMessage s (Dispatch d) = match (d s) + matchMessage s (DispatchIf d cond) = matchIf (cond s) (d s) + matchMessage s (DispatchReply d) = match (d s) + matchMessage s (DispatchReplyIf d cond) = matchIf (cond s) (d s) data Behaviour s = Behaviour { - initHandler :: InitHandler s -- ^ initialization handler - , dispatchers :: [Dispatcher s] - , terminateHandler :: TerminateHandler s -- ^ termination handler - } + dispatchers :: [Dispatcher s] + , timeoutHandler :: TimeoutHandler s + , terminateHandler :: TerminateHandler s -- ^ termination handler + } + +-- sending replies --- | Management message --- TODO is there a std way of terminating a process from another process? -data Termination = Terminate TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''Termination) +replyTo :: (Serializable m) => Recipient -> m -> Process () +replyTo (SendToPid p) m = send p m +replyTo (SendToService s) m = nsend s m +replyTo (SendToRemoteService s n) m = nsendRemote n s m -------------------------------------------------------------------------------- --- API -- +-- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- --- | Start a new server and return it's id --- start :: Behaviour s -> Process ProcessId --- start handlers = spawnLocal $ runProcess handlers - -reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () -reply (ReplyTo pid) m = BaseProcess.send pid m -reply _ _ = return () - -replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> - BaseProcess.Process () -replyVia p m = BaseProcess.sendChan p m - --- | Given a state, behaviour specificiation and spawn function, --- starts a new server and return its id. The spawn function is typically --- one taken from "Control.Distributed.Process". --- see 'Control.Distributed.Process.spawn' --- 'Control.Distributed.Process.spawnLocal' --- 'Control.Distributed.Process.spawnLink' --- 'Control.Distributed.Process.spawnMonitor' --- 'Control.Distributed.Process.spawnSupervised' -start :: - s -> Behaviour s -> - (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> - BaseProcess.Process BaseProcess.ProcessId -start state handlers spawn = spawn $ do - _ <- ST.runStateT (runProc handlers) state - return () - -send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () -send s m = do - let msg = (Message None m) - case s of - ServerProcess pid -> BaseProcess.send pid msg - NamedServer name -> BaseProcess.nsend name msg - --- process request handling - -handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s -handleRequest = handleRequestIf (const True) - -handleRequestIf :: (Serializable a) => (a -> Bool) -> - RequestHandler s a -> Dispatcher s -handleRequestIf cond handler = DispatchIf { - dispatch = (\state m@(Message _ _) -> do - (r, s') <- ST.runStateT (handler m) state - return (s', r) - ), - condition = \_ (Message _ req) -> cond req -} - --- process state management - --- | gets the process state -getState :: GenProcess s s -getState = ST.get - --- | sets the process state -putState :: s -> GenProcess s () -putState = ST.put - --- | modifies the server state -modifyState :: (s -> s) -> GenProcess s () -modifyState = ST.modify +start :: Process () +start = undefined + +stop :: Process () +stop = undefined + +call :: Process () +call = undefined + +cast :: Process () +cast = undefined -------------------------------------------------------------------------------- --- Implementation -- +-- Constructing Handlers from *ordinary* functions -- -------------------------------------------------------------------------------- --- | server process -runProc :: Behaviour s -> GenProcess s () -runProc s = do - ir <- init s - tr <- case ir of - InitOk t -> do - trace $ "Server ready to receive messages!" - loop s t - InitStop r -> return (TerminateReason r) - terminate s tr - --- | initialize server -init :: Behaviour s -> GenProcess s InitResult -init s = do - trace $ "Server initializing ... " - ir <- initHandler s - return ir - -loop :: Behaviour s -> Delay -> GenProcess s TerminateReason -loop s t = do - s' <- processReceive (dispatchers s) t - nextAction s s' - where nextAction :: Behaviour s -> ProcessAction -> - GenProcess s TerminateReason - nextAction b ProcessContinue = loop b t - nextAction b (ProcessTimeout t') = loop b t' - nextAction _ (ProcessStop r) = return (TerminateReason r) - -processReceive :: [Dispatcher s] -> Delay -> GenProcess s ProcessAction -processReceive ds t = do - s <- getState - let ms = map (matchMessage s) ds - -- TODO: should we drain the message queue to avoid selective receive here? - case t of - Infinity -> do - (s', r) <- ST.lift $ BaseProcess.receiveWait ms - putState s' - return r - Delay t' -> do - result <- ST.lift $ BaseProcess.receiveTimeout (asTimeout t') ms - case result of - Just (s', r) -> do - putState s' - return r - Nothing -> do - return $ ProcessStop "timed out" - -terminate :: Behaviour s -> TerminateReason -> GenProcess s () -terminate s reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler s) reason - --- | Log a trace message using the underlying Process's say -trace :: String -> GenProcess s () -trace msg = ST.lift . BaseProcess.say $ msg - --- data Upgrade = ??? --- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may handle hot server-code loading quite easily... +reply :: (Serializable r) => r -> s -> ProcessReply s r +reply r s = replyWith r (ProcessContinue s) + +replyWith :: (Serializable m) => m -> ProcessAction s -> ProcessReply s m +replyWith msg state = ProcessReply msg state + +-- | Constructs a 'call' handler from an ordinary function in the 'Process' +-- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, +-- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion +-- in a 'Behaviour' specification for the /GenProcess/. +-- +handleCall :: (Serializable a, Serializable b) + => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s +handleCall handler = DispatchReplyIf { + handle = doHandle handler + , handleIf = doCheck + } + where doHandle :: (Serializable a, Serializable b) + => (s -> a -> Process (ProcessReply s b)) -> s + -> Message a -> Process (ProcessAction s) + doHandle h s (CallMessage p c) = (h s p) >>= mkReply c + doHandle _ _ _ = error "illegal input" + -- TODO: standard 'this cannot happen' error message + + doCheck _ (CallMessage _ _) = True + doCheck _ _ = False + + mkReply :: (Serializable b) + => Recipient -> ProcessReply s b -> Process (ProcessAction s) + mkReply _ (NoReply a) = return a + mkReply c (ProcessReply r' a) = replyTo c r' >> return a + +-- | Constructs a 'cast' handler from an ordinary function in the 'Process' +-- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, +-- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion +-- in a 'Behaviour' specification for the /GenProcess/. +-- +handleCast :: (Serializable a) + => CastHandler s a -> Dispatcher s +handleCast h = Dispatch { + dispatch = (\s (CastMessage p) -> h s p) + } + +loop :: Behaviour s -> s -> Delay -> Process TerminateReason +loop b s t = do + ac <- processReceive s b t + nextAction b ac + where nextAction :: Behaviour s -> ProcessAction s -> Process TerminateReason + nextAction b (ProcessContinue s') = loop b s' t + nextAction b (ProcessTimeout t' s') = loop b s' t' + nextAction _ (ProcessStop r) = return (r :: TerminateReason) + +processReceive :: s -> Behaviour s -> Delay -> Process (ProcessAction s) +processReceive s b t = + let ms = map (matchMessage s) (dispatchers b) in do + next <- recv ms t + case next of + Nothing -> (timeoutHandler b) s t + Just pa -> return pa + where + recv :: [Match (ProcessAction s)] -> Delay -> Process (Maybe (ProcessAction s)) + recv matches d = + case d of + Infinity -> receiveWait matches >>= return . Just + Delay t' -> receiveTimeout (asTimeout t') matches + +demo :: Behaviour [String] +demo = + Behaviour { + dispatchers = [ + handleCall add + ] + , terminateHandler = undefined + } + +add :: [String] -> String -> Process (ProcessReply [String] String) +add s x = + let s' = (x:s) + in return $ reply "ok" s' + +onTimeout :: TimeoutHandler [String] +onTimeout _ _ = return ProcessStop { reason = (TerminateOther "timeout") } + From 61e8905c8ebeba0ad7f78c3d0b8df2234edc0469 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 09:44:41 +0000 Subject: [PATCH 0667/2357] do not use the state monad to implement GenProcess --- .../Process/Platform/GenProcess.hs | 406 +++++++++--------- 1 file changed, 196 insertions(+), 210 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5ea74f96..dfda8741 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -1,242 +1,228 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ImpredicativeTypes #-} module Control.Distributed.Process.Platform.GenProcess where -- TODO: define API and hide internals... -import qualified Control.Distributed.Process as BaseProcess -import qualified Control.Monad.State as ST (StateT, get, - lift, modify, - put, runStateT) - +import Control.Applicative +import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time +import qualified Control.Monad.State as ST + ( StateT + , get + , lift + , modify + , put + , runStateT + ) import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - - -type ServerName = String -type ServerPid = BaseProcess.ProcessId - -data ServerId = ServerProcess ServerPid | NamedServer ServerName - -data Recipient a = SendToPid BaseProcess.ProcessId | - SendToPort (BaseProcess.SendPort a) - --- | Initialize handler result -data InitResult = - InitOk Delay - | InitStop String - +import Data.Typeable (Typeable) +import Prelude hiding (init) + +data ServerId = ServerId ProcessId | ServerName String + +data Recipient = + SendToPid ProcessId + | SendToService String + | SendToRemoteService String NodeId + deriving (Typeable) +$(derive makeBinary ''Recipient) + +data Message a = + CastMessage { payload :: a } + | CallMessage { payload :: a, sender :: Recipient } + deriving (Typeable) +$(derive makeBinary ''Message) + -- | Terminate reason data TerminateReason = TerminateNormal | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - -data ReplyTo = ReplyTo BaseProcess.ProcessId | None - deriving (Typeable, Show) -$(derive makeBinary ''ReplyTo) - --- | The result of a call -data ProcessAction = - ProcessContinue - | ProcessTimeout Delay - | ProcessStop String - deriving (Typeable) -$(derive makeBinary ''ProcessAction) - -type GenProcess s = ST.StateT s BaseProcess.Process - --- | Handlers -type InitHandler s = GenProcess s InitResult -type TerminateHandler s = TerminateReason -> GenProcess s () -type RequestHandler s a = Message a -> GenProcess s ProcessAction - --- | Contains the actual payload and possibly additional routing metadata -data Message a = Message ReplyTo a - deriving (Show, Typeable) -$(derive makeBinary ''Message) + | forall r. (Serializable r) => + TerminateOther r + deriving (Typeable) + +-- | Initialization +data InitResult s = + InitOk s Delay + | forall r. (Serializable r) => InitStop r + +data ProcessAction s = + ProcessContinue { nextState :: s } + | ProcessTimeout { delay :: Delay, nextState :: s } + | ProcessHibernate { delay :: Delay, nextState :: s } + | ProcessStop { reason :: TerminateReason } + +data ProcessReply s a = + ProcessReply { response :: a + , action :: ProcessAction s } + | NoReply { action :: ProcessAction s} + +type InitHandler a s = a -> InitResult s +type TerminateHandler s = s -> TerminateReason -> Process () +type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) +type CallHandler s a b = s -> a -> Process (ProcessReply s b) +type CastHandler s a = s -> a -> Process (ProcessAction s) + +data Req a b = Req a b -data Rpc a b = ProcessRpc (Message a) b | PortRpc a (BaseProcess.SendPort b) - deriving (Typeable) -$(derive makeBinary ''Rpc) +-- dispatching to implementation callbacks -- | Dispatcher that knows how to dispatch messages to a handler +-- s The server state data Dispatcher s = - forall a . (Serializable a) => - Dispatch { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction) } | - forall a . (Serializable a) => - DispatchIf { dispatch :: s -> Message a -> - BaseProcess.Process (s, ProcessAction), - condition :: s -> Message a -> Bool } - --- dispatching to implementation callbacks + forall a . (Serializable a) => Dispatch { + dispatch :: s -> Message a -> Process (ProcessAction s) + } + | forall a . (Serializable a) => DispatchIf { + dispatch :: s -> Message a -> Process (ProcessAction s) + , dispatchIf :: s -> Message a -> Bool + } + | forall a . (Serializable a) => DispatchReply { + handle :: s -> Message a -> Process (ProcessAction s) + } + | forall a . (Serializable a) => DispatchReplyIf { + handle :: s -> Message a -> Process (ProcessAction s) + , handleIf :: s -> Message a -> Bool + } -- | Matches messages using a dispatcher -class Dispatchable d where - matchMessage :: s -> d s -> BaseProcess.Match (s, ProcessAction) +class MessageMatcher d where + matchMessage :: s -> d s -> Match (ProcessAction s) -- | Matches messages to a MessageDispatcher -instance Dispatchable Dispatcher where - matchMessage s (Dispatch d ) = BaseProcess.match (d s) - matchMessage s (DispatchIf d c) = BaseProcess.matchIf (c s) (d s) - +instance MessageMatcher Dispatcher where + matchMessage s (Dispatch d) = match (d s) + matchMessage s (DispatchIf d cond) = matchIf (cond s) (d s) + matchMessage s (DispatchReply d) = match (d s) + matchMessage s (DispatchReplyIf d cond) = matchIf (cond s) (d s) data Behaviour s = Behaviour { - initHandler :: InitHandler s -- ^ initialization handler - , dispatchers :: [Dispatcher s] - , terminateHandler :: TerminateHandler s -- ^ termination handler - } + dispatchers :: [Dispatcher s] + , timeoutHandler :: TimeoutHandler s + , terminateHandler :: TerminateHandler s -- ^ termination handler + } + +-- sending replies --- | Management message --- TODO is there a std way of terminating a process from another process? -data Termination = Terminate TerminateReason - deriving (Show, Typeable) -$(derive makeBinary ''Termination) +replyTo :: (Serializable m) => Recipient -> m -> Process () +replyTo (SendToPid p) m = send p m +replyTo (SendToService s) m = nsend s m +replyTo (SendToRemoteService s n) m = nsendRemote n s m -------------------------------------------------------------------------------- --- API -- +-- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- --- | Start a new server and return it's id --- start :: Behaviour s -> Process ProcessId --- start handlers = spawnLocal $ runProcess handlers - -reply :: (Serializable m) => ReplyTo -> m -> BaseProcess.Process () -reply (ReplyTo pid) m = BaseProcess.send pid m -reply _ _ = return () - -replyVia :: (Serializable m) => BaseProcess.SendPort m -> m -> - BaseProcess.Process () -replyVia p m = BaseProcess.sendChan p m - --- | Given a state, behaviour specificiation and spawn function, --- starts a new server and return its id. The spawn function is typically --- one taken from "Control.Distributed.Process". --- see 'Control.Distributed.Process.spawn' --- 'Control.Distributed.Process.spawnLocal' --- 'Control.Distributed.Process.spawnLink' --- 'Control.Distributed.Process.spawnMonitor' --- 'Control.Distributed.Process.spawnSupervised' -start :: - s -> Behaviour s -> - (BaseProcess.Process () -> BaseProcess.Process BaseProcess.ProcessId) -> - BaseProcess.Process BaseProcess.ProcessId -start state handlers spawn = spawn $ do - _ <- ST.runStateT (runProc handlers) state - return () - -send :: (Serializable m) => ServerId -> m -> BaseProcess.Process () -send s m = do - let msg = (Message None m) - case s of - ServerProcess pid -> BaseProcess.send pid msg - NamedServer name -> BaseProcess.nsend name msg - --- process request handling - -handleRequest :: (Serializable m) => RequestHandler s m -> Dispatcher s -handleRequest = handleRequestIf (const True) - -handleRequestIf :: (Serializable a) => (a -> Bool) -> - RequestHandler s a -> Dispatcher s -handleRequestIf cond handler = DispatchIf { - dispatch = (\state m@(Message _ _) -> do - (r, s') <- ST.runStateT (handler m) state - return (s', r) - ), - condition = \_ (Message _ req) -> cond req -} - --- process state management - --- | gets the process state -getState :: GenProcess s s -getState = ST.get - --- | sets the process state -putState :: s -> GenProcess s () -putState = ST.put - --- | modifies the server state -modifyState :: (s -> s) -> GenProcess s () -modifyState = ST.modify +start :: Process () +start = undefined + +stop :: Process () +stop = undefined + +call :: Process () +call = undefined + +cast :: Process () +cast = undefined -------------------------------------------------------------------------------- --- Implementation -- +-- Constructing Handlers from *ordinary* functions -- -------------------------------------------------------------------------------- --- | server process -runProc :: Behaviour s -> GenProcess s () -runProc s = do - ir <- init s - tr <- case ir of - InitOk t -> do - trace $ "Server ready to receive messages!" - loop s t - InitStop r -> return (TerminateReason r) - terminate s tr - --- | initialize server -init :: Behaviour s -> GenProcess s InitResult -init s = do - trace $ "Server initializing ... " - ir <- initHandler s - return ir - -loop :: Behaviour s -> Delay -> GenProcess s TerminateReason -loop s t = do - s' <- processReceive (dispatchers s) t - nextAction s s' - where nextAction :: Behaviour s -> ProcessAction -> - GenProcess s TerminateReason - nextAction b ProcessContinue = loop b t - nextAction b (ProcessTimeout t') = loop b t' - nextAction _ (ProcessStop r) = return (TerminateReason r) - -processReceive :: [Dispatcher s] -> Delay -> GenProcess s ProcessAction -processReceive ds t = do - s <- getState - let ms = map (matchMessage s) ds - -- TODO: should we drain the message queue to avoid selective receive here? - case t of - Infinity -> do - (s', r) <- ST.lift $ BaseProcess.receiveWait ms - putState s' - return r - Delay t' -> do - result <- ST.lift $ BaseProcess.receiveTimeout (asTimeout t') ms - case result of - Just (s', r) -> do - putState s' - return r - Nothing -> do - return $ ProcessStop "timed out" - -terminate :: Behaviour s -> TerminateReason -> GenProcess s () -terminate s reason = do - trace $ "Server terminating: " ++ show reason - (terminateHandler s) reason - --- | Log a trace message using the underlying Process's say -trace :: String -> GenProcess s () -trace msg = ST.lift . BaseProcess.say $ msg - --- data Upgrade = ??? --- TODO: can we use 'Static (SerializableDict a)' to pass a Behaviour spec to --- a remote pid? if so then we may handle hot server-code loading quite easily... +reply :: (Serializable r) => r -> s -> ProcessReply s r +reply r s = replyWith r (ProcessContinue s) + +replyWith :: (Serializable m) => m -> ProcessAction s -> ProcessReply s m +replyWith msg state = ProcessReply msg state + +-- | Constructs a 'call' handler from an ordinary function in the 'Process' +-- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, +-- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion +-- in a 'Behaviour' specification for the /GenProcess/. +-- +handleCall :: (Serializable a, Serializable b) + => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s +handleCall handler = DispatchReplyIf { + handle = doHandle handler + , handleIf = doCheck + } + where doHandle :: (Serializable a, Serializable b) + => (s -> a -> Process (ProcessReply s b)) -> s + -> Message a -> Process (ProcessAction s) + doHandle h s (CallMessage p c) = (h s p) >>= mkReply c + doHandle _ _ _ = error "illegal input" + -- TODO: standard 'this cannot happen' error message + + doCheck _ (CallMessage _ _) = True + doCheck _ _ = False + + mkReply :: (Serializable b) + => Recipient -> ProcessReply s b -> Process (ProcessAction s) + mkReply _ (NoReply a) = return a + mkReply c (ProcessReply r' a) = replyTo c r' >> return a + +-- | Constructs a 'cast' handler from an ordinary function in the 'Process' +-- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, +-- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion +-- in a 'Behaviour' specification for the /GenProcess/. +-- +handleCast :: (Serializable a) + => CastHandler s a -> Dispatcher s +handleCast h = Dispatch { + dispatch = (\s (CastMessage p) -> h s p) + } + +loop :: Behaviour s -> s -> Delay -> Process TerminateReason +loop b s t = do + ac <- processReceive s b t + nextAction b ac + where nextAction :: Behaviour s -> ProcessAction s -> Process TerminateReason + nextAction b (ProcessContinue s') = loop b s' t + nextAction b (ProcessTimeout t' s') = loop b s' t' + nextAction _ (ProcessStop r) = return (r :: TerminateReason) + +processReceive :: s -> Behaviour s -> Delay -> Process (ProcessAction s) +processReceive s b t = + let ms = map (matchMessage s) (dispatchers b) in do + next <- recv ms t + case next of + Nothing -> (timeoutHandler b) s t + Just pa -> return pa + where + recv :: [Match (ProcessAction s)] -> Delay -> Process (Maybe (ProcessAction s)) + recv matches d = + case d of + Infinity -> receiveWait matches >>= return . Just + Delay t' -> receiveTimeout (asTimeout t') matches + +demo :: Behaviour [String] +demo = + Behaviour { + dispatchers = [ + handleCall add + ] + , terminateHandler = undefined + } + +add :: [String] -> String -> Process (ProcessReply [String] String) +add s x = + let s' = (x:s) + in return $ reply "ok" s' + +onTimeout :: TimeoutHandler [String] +onTimeout _ _ = return ProcessStop { reason = (TerminateOther "timeout") } + From 960fa806de231f1c1d26e0b12871e6b1ae2df613 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 14:44:54 +0000 Subject: [PATCH 0668/2357] extend GenProcess to handleInfo --- .../Process/Platform/GenProcess.hs | 246 ++++++++++++------ 1 file changed, 166 insertions(+), 80 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index dfda8741..9db3a10e 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -15,18 +15,10 @@ module Control.Distributed.Process.Platform.GenProcess where -- TODO: define API and hide internals... -import Control.Applicative +import Control.Concurrent (threadDelay) import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time -import qualified Control.Monad.State as ST - ( StateT - , get - , lift - , modify - , put - , runStateT - ) import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -61,10 +53,10 @@ data InitResult s = | forall r. (Serializable r) => InitStop r data ProcessAction s = - ProcessContinue { nextState :: s } - | ProcessTimeout { delay :: Delay, nextState :: s } - | ProcessHibernate { delay :: Delay, nextState :: s } - | ProcessStop { reason :: TerminateReason } + ProcessContinue { nextState :: s } + | ProcessTimeout { delay :: TimeInterval, nextState :: s } + | ProcessHibernate { duration :: TimeInterval, nextState :: s } + | ProcessStop { reason :: TerminateReason } data ProcessReply s a = ProcessReply { response :: a @@ -74,15 +66,10 @@ data ProcessReply s a = type InitHandler a s = a -> InitResult s type TerminateHandler s = s -> TerminateReason -> Process () type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) -type CallHandler s a b = s -> a -> Process (ProcessReply s b) -type CastHandler s a = s -> a -> Process (ProcessAction s) - -data Req a b = Req a b -- dispatching to implementation callbacks --- | Dispatcher that knows how to dispatch messages to a handler --- s The server state +-- | this type defines dispatch from abstract messages to a typed handler data Dispatcher s = forall a . (Serializable a) => Dispatch { dispatch :: s -> Message a -> Process (ProcessAction s) @@ -91,38 +78,39 @@ data Dispatcher s = dispatch :: s -> Message a -> Process (ProcessAction s) , dispatchIf :: s -> Message a -> Bool } - | forall a . (Serializable a) => DispatchReply { - handle :: s -> Message a -> Process (ProcessAction s) - } - | forall a . (Serializable a) => DispatchReplyIf { - handle :: s -> Message a -> Process (ProcessAction s) - , handleIf :: s -> Message a -> Bool - } + | DispatchInfo { + dispatchInfo :: UnhandledMessagePolicy + -> s + -> AbstractMessage + -> Process (ProcessAction s) + } --- | Matches messages using a dispatcher +-- | matches messages of specific types using a dispatcher class MessageMatcher d where - matchMessage :: s -> d s -> Match (ProcessAction s) + matchMessage :: UnhandledMessagePolicy -> s -> d s -> Match (ProcessAction s) --- | Matches messages to a MessageDispatcher +-- | matches messages to a MessageDispatcher instance MessageMatcher Dispatcher where - matchMessage s (Dispatch d) = match (d s) - matchMessage s (DispatchIf d cond) = matchIf (cond s) (d s) - matchMessage s (DispatchReply d) = match (d s) - matchMessage s (DispatchReplyIf d cond) = matchIf (cond s) (d s) + matchMessage _ s (Dispatch d) = match (d s) + matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) + matchMessage p s (DispatchInfo d) = matchAny (d p s) + +-- | Policy for handling unexpected messages, i.e., messages which are not +-- sent using the 'call' or 'cast' APIs, and which are not handled by any of the +-- 'handleInfo' handlers. +data UnhandledMessagePolicy = + Terminate + | DeadLetter ProcessId + | Drop data Behaviour s = Behaviour { dispatchers :: [Dispatcher s] + , infoHandlers :: [Dispatcher s] , timeoutHandler :: TimeoutHandler s , terminateHandler :: TerminateHandler s -- ^ termination handler + , unhandledMessagePolicy :: UnhandledMessagePolicy } --- sending replies - -replyTo :: (Serializable m) => Recipient -> m -> Process () -replyTo (SendToPid p) m = send p m -replyTo (SendToService s) m = nsend s m -replyTo (SendToRemoteService s n) m = nsendRemote n s m - -------------------------------------------------------------------------------- -- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- @@ -130,39 +118,63 @@ replyTo (SendToRemoteService s n) m = nsendRemote n s m start :: Process () start = undefined -stop :: Process () -stop = undefined - call :: Process () call = undefined cast :: Process () cast = undefined --------------------------------------------------------------------------------- --- Constructing Handlers from *ordinary* functions -- --------------------------------------------------------------------------------- +-- Constructing Handlers from *ordinary* functions +-- | INstructs the process to send a reply and continue working. +-- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> ProcessReply s r -reply r s = replyWith r (ProcessContinue s) +reply r s = replyWith r (continue s) +-- | Instructs the process to send a reply and evaluate the 'ProcessAction' +-- thereafter. replyWith :: (Serializable m) => m -> ProcessAction s -> ProcessReply s m replyWith msg state = ProcessReply msg state +-- | Instructs the process to continue running and receiving messages. +continue :: s -> ProcessAction s +continue s = ProcessContinue s + +-- | Instructs the process to wait for incoming messages until 'TimeInterval' +-- is exceeded. If no messages are handled during this period, the /timeout/ +-- handler will be called. Note that this alters the process timeout permanently +-- such that the given @TimeInterval@ will remain in use until changed. +timeoutAfter :: TimeInterval -> s -> ProcessAction s +timeoutAfter d s = ProcessTimeout d s + +-- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note +-- that no messages will be removed from the mailbox until after hibernation has +-- ceased. This is equivalent to calling @threadDelay@. +-- +hibernate :: TimeInterval -> s -> ProcessAction s +hibernate d s = ProcessHibernate d s + +-- | Instructs the process to cease, giving the supplied reason for termination. +stop :: TerminateReason -> ProcessAction s +stop r = ProcessStop r + -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion -- in a 'Behaviour' specification for the /GenProcess/. -- handleCall :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall handler = DispatchReplyIf { - handle = doHandle handler - , handleIf = doCheck + => (s -> a -> Process (ProcessReply s b)) + -> Dispatcher s +handleCall handler = DispatchIf { + dispatch = doHandle handler + , dispatchIf = doCheck } where doHandle :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) -> s - -> Message a -> Process (ProcessAction s) + => (s -> a -> Process (ProcessReply s b)) + -> s + -> Message a + -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message @@ -170,6 +182,8 @@ handleCall handler = DispatchReplyIf { doCheck _ (CallMessage _ _) = True doCheck _ _ = False + -- handling 'reply-to' in the main process loop is awkward at best, + -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) => Recipient -> ProcessReply s b -> Process (ProcessAction s) mkReply _ (NoReply a) = return a @@ -181,42 +195,114 @@ handleCall handler = DispatchReplyIf { -- in a 'Behaviour' specification for the /GenProcess/. -- handleCast :: (Serializable a) - => CastHandler s a -> Dispatcher s -handleCast h = Dispatch { - dispatch = (\s (CastMessage p) -> h s p) - } - -loop :: Behaviour s -> s -> Delay -> Process TerminateReason -loop b s t = do - ac <- processReceive s b t - nextAction b ac - where nextAction :: Behaviour s -> ProcessAction s -> Process TerminateReason - nextAction b (ProcessContinue s') = loop b s' t - nextAction b (ProcessTimeout t' s') = loop b s' t' - nextAction _ (ProcessStop r) = return (r :: TerminateReason) - -processReceive :: s -> Behaviour s -> Delay -> Process (ProcessAction s) -processReceive s b t = - let ms = map (matchMessage s) (dispatchers b) in do + => (s -> a -> Process (ProcessAction s)) -> Dispatcher s +handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } + +handleInfo :: forall s a. (Serializable a) + => (s -> a -> Process (ProcessAction s)) -> Dispatcher s +handleInfo h = DispatchInfo { + dispatchInfo = dispatchIt h + } + where dispatchIt :: (Serializable a) + => (s -> a -> Process (ProcessAction s)) + -> UnhandledMessagePolicy + -> s + -> AbstractMessage -> Process (ProcessAction s) + dispatchIt h' pol s msg = do + m <- maybeHandleMessage msg (h' s) + case m of + Nothing -> applyPolicy s pol msg + Just act -> return (act :: ProcessAction s) + +-- Process Implementation + +applyPolicy :: s + -> UnhandledMessagePolicy + -> AbstractMessage + -> Process (ProcessAction s) +applyPolicy s p m = do + case p of + Terminate -> return $ stop (TerminateOther "unexpected-input") + DeadLetter pid -> forward m pid >> return (continue s) + Drop -> return (continue s) + +initLoop :: Behaviour s -> s -> Process TerminateReason +initLoop b s = + let p = unhandledMessagePolicy b + t = timeoutHandler b + ms = map (matchMessage p s) (dispatchers b) + ms' = addInfoHandlers b s p ms + in loop ms' t s Infinity + where + addInfoHandlers :: Behaviour s + -> s + -> UnhandledMessagePolicy + -> [Match (ProcessAction s)] + -> [Match (ProcessAction s)] + addInfoHandlers b' s' p rms = + rms ++ addInfoAux p s' (infoHandlers b') [] + + -- if there's more than one info handler then we /do not/ want to apply the + -- policy until we reach the last one, otherwise we'll miss out the others + addInfoAux :: UnhandledMessagePolicy + -> s + -> [Dispatcher s] + -> [Match (ProcessAction s)] + -> [Match (ProcessAction s)] + addInfoAux _ _ [] _ = [] + addInfoAux p s'' (d:ds :: [Dispatcher s]) acc + | length ds == 0 = reverse ((matchMessage p s'' d):acc) + | otherwise = ((matchMessage Drop s'' d):(addInfoAux p s'' ds acc)) + +loop :: [Match (ProcessAction s)] + -> TimeoutHandler s + -> s + -> Delay + -> Process TerminateReason +loop ms h s t = do + ac <- processReceive ms h s t + case ac of + (ProcessContinue s') -> loop ms h s' t + (ProcessTimeout t' s') -> loop ms h s' (Delay t') + (ProcessHibernate d' s') -> block d' >> loop ms h s' t + (ProcessStop r) -> return (r :: TerminateReason) + where block :: TimeInterval -> Process () + block i = liftIO $ threadDelay (asTimeout i) + +processReceive :: [Match (ProcessAction s)] + -> TimeoutHandler s + -> s + -> Delay + -> Process (ProcessAction s) +processReceive ms h s t = do next <- recv ms t case next of - Nothing -> (timeoutHandler b) s t + Nothing -> h s t Just pa -> return pa where - recv :: [Match (ProcessAction s)] -> Delay -> Process (Maybe (ProcessAction s)) + recv :: [Match (ProcessAction s)] + -> Delay + -> Process (Maybe (ProcessAction s)) recv matches d = case d of Infinity -> receiveWait matches >>= return . Just - Delay t' -> receiveTimeout (asTimeout t') matches + Delay t' -> receiveTimeout (asTimeout t') matches + +-- internal/utility + +replyTo :: (Serializable m) => Recipient -> m -> Process () +replyTo (SendToPid p) m = send p m +replyTo (SendToService s) m = nsend s m +replyTo (SendToRemoteService s n) m = nsendRemote n s m demo :: Behaviour [String] -demo = - Behaviour { - dispatchers = [ - handleCall add - ] - , terminateHandler = undefined - } +demo = Behaviour { + dispatchers = [ + handleCall add + ] + , infoHandlers = [] + , terminateHandler = undefined + } add :: [String] -> String -> Process (ProcessReply [String] String) add s x = From 5d2e6f2d044ab79cb93b8c48d0453f6cb2677106 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 14:44:54 +0000 Subject: [PATCH 0669/2357] extend GenProcess to handleInfo --- .../Process/Platform/GenProcess.hs | 246 ++++++++++++------ 1 file changed, 166 insertions(+), 80 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index dfda8741..9db3a10e 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -15,18 +15,10 @@ module Control.Distributed.Process.Platform.GenProcess where -- TODO: define API and hide internals... -import Control.Applicative +import Control.Concurrent (threadDelay) import Control.Distributed.Process import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time -import qualified Control.Monad.State as ST - ( StateT - , get - , lift - , modify - , put - , runStateT - ) import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -61,10 +53,10 @@ data InitResult s = | forall r. (Serializable r) => InitStop r data ProcessAction s = - ProcessContinue { nextState :: s } - | ProcessTimeout { delay :: Delay, nextState :: s } - | ProcessHibernate { delay :: Delay, nextState :: s } - | ProcessStop { reason :: TerminateReason } + ProcessContinue { nextState :: s } + | ProcessTimeout { delay :: TimeInterval, nextState :: s } + | ProcessHibernate { duration :: TimeInterval, nextState :: s } + | ProcessStop { reason :: TerminateReason } data ProcessReply s a = ProcessReply { response :: a @@ -74,15 +66,10 @@ data ProcessReply s a = type InitHandler a s = a -> InitResult s type TerminateHandler s = s -> TerminateReason -> Process () type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) -type CallHandler s a b = s -> a -> Process (ProcessReply s b) -type CastHandler s a = s -> a -> Process (ProcessAction s) - -data Req a b = Req a b -- dispatching to implementation callbacks --- | Dispatcher that knows how to dispatch messages to a handler --- s The server state +-- | this type defines dispatch from abstract messages to a typed handler data Dispatcher s = forall a . (Serializable a) => Dispatch { dispatch :: s -> Message a -> Process (ProcessAction s) @@ -91,38 +78,39 @@ data Dispatcher s = dispatch :: s -> Message a -> Process (ProcessAction s) , dispatchIf :: s -> Message a -> Bool } - | forall a . (Serializable a) => DispatchReply { - handle :: s -> Message a -> Process (ProcessAction s) - } - | forall a . (Serializable a) => DispatchReplyIf { - handle :: s -> Message a -> Process (ProcessAction s) - , handleIf :: s -> Message a -> Bool - } + | DispatchInfo { + dispatchInfo :: UnhandledMessagePolicy + -> s + -> AbstractMessage + -> Process (ProcessAction s) + } --- | Matches messages using a dispatcher +-- | matches messages of specific types using a dispatcher class MessageMatcher d where - matchMessage :: s -> d s -> Match (ProcessAction s) + matchMessage :: UnhandledMessagePolicy -> s -> d s -> Match (ProcessAction s) --- | Matches messages to a MessageDispatcher +-- | matches messages to a MessageDispatcher instance MessageMatcher Dispatcher where - matchMessage s (Dispatch d) = match (d s) - matchMessage s (DispatchIf d cond) = matchIf (cond s) (d s) - matchMessage s (DispatchReply d) = match (d s) - matchMessage s (DispatchReplyIf d cond) = matchIf (cond s) (d s) + matchMessage _ s (Dispatch d) = match (d s) + matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) + matchMessage p s (DispatchInfo d) = matchAny (d p s) + +-- | Policy for handling unexpected messages, i.e., messages which are not +-- sent using the 'call' or 'cast' APIs, and which are not handled by any of the +-- 'handleInfo' handlers. +data UnhandledMessagePolicy = + Terminate + | DeadLetter ProcessId + | Drop data Behaviour s = Behaviour { dispatchers :: [Dispatcher s] + , infoHandlers :: [Dispatcher s] , timeoutHandler :: TimeoutHandler s , terminateHandler :: TerminateHandler s -- ^ termination handler + , unhandledMessagePolicy :: UnhandledMessagePolicy } --- sending replies - -replyTo :: (Serializable m) => Recipient -> m -> Process () -replyTo (SendToPid p) m = send p m -replyTo (SendToService s) m = nsend s m -replyTo (SendToRemoteService s n) m = nsendRemote n s m - -------------------------------------------------------------------------------- -- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- @@ -130,39 +118,63 @@ replyTo (SendToRemoteService s n) m = nsendRemote n s m start :: Process () start = undefined -stop :: Process () -stop = undefined - call :: Process () call = undefined cast :: Process () cast = undefined --------------------------------------------------------------------------------- --- Constructing Handlers from *ordinary* functions -- --------------------------------------------------------------------------------- +-- Constructing Handlers from *ordinary* functions +-- | INstructs the process to send a reply and continue working. +-- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> ProcessReply s r -reply r s = replyWith r (ProcessContinue s) +reply r s = replyWith r (continue s) +-- | Instructs the process to send a reply and evaluate the 'ProcessAction' +-- thereafter. replyWith :: (Serializable m) => m -> ProcessAction s -> ProcessReply s m replyWith msg state = ProcessReply msg state +-- | Instructs the process to continue running and receiving messages. +continue :: s -> ProcessAction s +continue s = ProcessContinue s + +-- | Instructs the process to wait for incoming messages until 'TimeInterval' +-- is exceeded. If no messages are handled during this period, the /timeout/ +-- handler will be called. Note that this alters the process timeout permanently +-- such that the given @TimeInterval@ will remain in use until changed. +timeoutAfter :: TimeInterval -> s -> ProcessAction s +timeoutAfter d s = ProcessTimeout d s + +-- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note +-- that no messages will be removed from the mailbox until after hibernation has +-- ceased. This is equivalent to calling @threadDelay@. +-- +hibernate :: TimeInterval -> s -> ProcessAction s +hibernate d s = ProcessHibernate d s + +-- | Instructs the process to cease, giving the supplied reason for termination. +stop :: TerminateReason -> ProcessAction s +stop r = ProcessStop r + -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion -- in a 'Behaviour' specification for the /GenProcess/. -- handleCall :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall handler = DispatchReplyIf { - handle = doHandle handler - , handleIf = doCheck + => (s -> a -> Process (ProcessReply s b)) + -> Dispatcher s +handleCall handler = DispatchIf { + dispatch = doHandle handler + , dispatchIf = doCheck } where doHandle :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) -> s - -> Message a -> Process (ProcessAction s) + => (s -> a -> Process (ProcessReply s b)) + -> s + -> Message a + -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message @@ -170,6 +182,8 @@ handleCall handler = DispatchReplyIf { doCheck _ (CallMessage _ _) = True doCheck _ _ = False + -- handling 'reply-to' in the main process loop is awkward at best, + -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) => Recipient -> ProcessReply s b -> Process (ProcessAction s) mkReply _ (NoReply a) = return a @@ -181,42 +195,114 @@ handleCall handler = DispatchReplyIf { -- in a 'Behaviour' specification for the /GenProcess/. -- handleCast :: (Serializable a) - => CastHandler s a -> Dispatcher s -handleCast h = Dispatch { - dispatch = (\s (CastMessage p) -> h s p) - } - -loop :: Behaviour s -> s -> Delay -> Process TerminateReason -loop b s t = do - ac <- processReceive s b t - nextAction b ac - where nextAction :: Behaviour s -> ProcessAction s -> Process TerminateReason - nextAction b (ProcessContinue s') = loop b s' t - nextAction b (ProcessTimeout t' s') = loop b s' t' - nextAction _ (ProcessStop r) = return (r :: TerminateReason) - -processReceive :: s -> Behaviour s -> Delay -> Process (ProcessAction s) -processReceive s b t = - let ms = map (matchMessage s) (dispatchers b) in do + => (s -> a -> Process (ProcessAction s)) -> Dispatcher s +handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } + +handleInfo :: forall s a. (Serializable a) + => (s -> a -> Process (ProcessAction s)) -> Dispatcher s +handleInfo h = DispatchInfo { + dispatchInfo = dispatchIt h + } + where dispatchIt :: (Serializable a) + => (s -> a -> Process (ProcessAction s)) + -> UnhandledMessagePolicy + -> s + -> AbstractMessage -> Process (ProcessAction s) + dispatchIt h' pol s msg = do + m <- maybeHandleMessage msg (h' s) + case m of + Nothing -> applyPolicy s pol msg + Just act -> return (act :: ProcessAction s) + +-- Process Implementation + +applyPolicy :: s + -> UnhandledMessagePolicy + -> AbstractMessage + -> Process (ProcessAction s) +applyPolicy s p m = do + case p of + Terminate -> return $ stop (TerminateOther "unexpected-input") + DeadLetter pid -> forward m pid >> return (continue s) + Drop -> return (continue s) + +initLoop :: Behaviour s -> s -> Process TerminateReason +initLoop b s = + let p = unhandledMessagePolicy b + t = timeoutHandler b + ms = map (matchMessage p s) (dispatchers b) + ms' = addInfoHandlers b s p ms + in loop ms' t s Infinity + where + addInfoHandlers :: Behaviour s + -> s + -> UnhandledMessagePolicy + -> [Match (ProcessAction s)] + -> [Match (ProcessAction s)] + addInfoHandlers b' s' p rms = + rms ++ addInfoAux p s' (infoHandlers b') [] + + -- if there's more than one info handler then we /do not/ want to apply the + -- policy until we reach the last one, otherwise we'll miss out the others + addInfoAux :: UnhandledMessagePolicy + -> s + -> [Dispatcher s] + -> [Match (ProcessAction s)] + -> [Match (ProcessAction s)] + addInfoAux _ _ [] _ = [] + addInfoAux p s'' (d:ds :: [Dispatcher s]) acc + | length ds == 0 = reverse ((matchMessage p s'' d):acc) + | otherwise = ((matchMessage Drop s'' d):(addInfoAux p s'' ds acc)) + +loop :: [Match (ProcessAction s)] + -> TimeoutHandler s + -> s + -> Delay + -> Process TerminateReason +loop ms h s t = do + ac <- processReceive ms h s t + case ac of + (ProcessContinue s') -> loop ms h s' t + (ProcessTimeout t' s') -> loop ms h s' (Delay t') + (ProcessHibernate d' s') -> block d' >> loop ms h s' t + (ProcessStop r) -> return (r :: TerminateReason) + where block :: TimeInterval -> Process () + block i = liftIO $ threadDelay (asTimeout i) + +processReceive :: [Match (ProcessAction s)] + -> TimeoutHandler s + -> s + -> Delay + -> Process (ProcessAction s) +processReceive ms h s t = do next <- recv ms t case next of - Nothing -> (timeoutHandler b) s t + Nothing -> h s t Just pa -> return pa where - recv :: [Match (ProcessAction s)] -> Delay -> Process (Maybe (ProcessAction s)) + recv :: [Match (ProcessAction s)] + -> Delay + -> Process (Maybe (ProcessAction s)) recv matches d = case d of Infinity -> receiveWait matches >>= return . Just - Delay t' -> receiveTimeout (asTimeout t') matches + Delay t' -> receiveTimeout (asTimeout t') matches + +-- internal/utility + +replyTo :: (Serializable m) => Recipient -> m -> Process () +replyTo (SendToPid p) m = send p m +replyTo (SendToService s) m = nsend s m +replyTo (SendToRemoteService s n) m = nsendRemote n s m demo :: Behaviour [String] -demo = - Behaviour { - dispatchers = [ - handleCall add - ] - , terminateHandler = undefined - } +demo = Behaviour { + dispatchers = [ + handleCall add + ] + , infoHandlers = [] + , terminateHandler = undefined + } add :: [String] -> String -> Process (ProcessReply [String] String) add s x = From bb02a25e675ea0cfce0409f1df7247dc45aca3b8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 15:18:23 +0000 Subject: [PATCH 0670/2357] oops - we need to wrap the info handlers ourselves. --- .../Process/Platform/GenProcess.hs | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9db3a10e..e40e29b3 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -78,12 +78,10 @@ data Dispatcher s = dispatch :: s -> Message a -> Process (ProcessAction s) , dispatchIf :: s -> Message a -> Bool } - | DispatchInfo { - dispatchInfo :: UnhandledMessagePolicy - -> s - -> AbstractMessage - -> Process (ProcessAction s) - } + +data InfoDispatcher s = InfoDispatcher { + dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) + } -- | matches messages of specific types using a dispatcher class MessageMatcher d where @@ -93,7 +91,6 @@ class MessageMatcher d where instance MessageMatcher Dispatcher where matchMessage _ s (Dispatch d) = match (d s) matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) - matchMessage p s (DispatchInfo d) = matchAny (d p s) -- | Policy for handling unexpected messages, i.e., messages which are not -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the @@ -105,7 +102,7 @@ data UnhandledMessagePolicy = data Behaviour s = Behaviour { dispatchers :: [Dispatcher s] - , infoHandlers :: [Dispatcher s] + , infoHandlers :: [InfoDispatcher s] , timeoutHandler :: TimeoutHandler s , terminateHandler :: TerminateHandler s -- ^ termination handler , unhandledMessagePolicy :: UnhandledMessagePolicy @@ -199,20 +196,11 @@ handleCast :: (Serializable a) handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } handleInfo :: forall s a. (Serializable a) - => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleInfo h = DispatchInfo { - dispatchInfo = dispatchIt h - } - where dispatchIt :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> UnhandledMessagePolicy - -> s - -> AbstractMessage -> Process (ProcessAction s) - dispatchIt h' pol s msg = do - m <- maybeHandleMessage msg (h' s) - case m of - Nothing -> applyPolicy s pol msg - Just act -> return (act :: ProcessAction s) + => (s -> a -> Process (ProcessAction s)) + -> s + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) +handleInfo h' s msg = maybeHandleMessage msg (h' s) -- Process Implementation @@ -240,19 +228,35 @@ initLoop b s = -> [Match (ProcessAction s)] -> [Match (ProcessAction s)] addInfoHandlers b' s' p rms = - rms ++ addInfoAux p s' (infoHandlers b') [] + rms ++ addInfoAux p s' (infoHandlers b') - -- if there's more than one info handler then we /do not/ want to apply the - -- policy until we reach the last one, otherwise we'll miss out the others addInfoAux :: UnhandledMessagePolicy -> s - -> [Dispatcher s] - -> [Match (ProcessAction s)] + -> [InfoDispatcher s] -> [Match (ProcessAction s)] - addInfoAux _ _ [] _ = [] - addInfoAux p s'' (d:ds :: [Dispatcher s]) acc - | length ds == 0 = reverse ((matchMessage p s'' d):acc) - | otherwise = ((matchMessage Drop s'' d):(addInfoAux p s'' ds acc)) + addInfoAux _ _ [] = [] + addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] + + infoHandler :: UnhandledMessagePolicy + -> s + -> [InfoDispatcher s] + -> AbstractMessage + -> Process (ProcessAction s) + infoHandler _ _ [] _ = error "addInfoAux doest not permit this" + infoHandler pol st (d:ds :: [InfoDispatcher s]) msg + | length ds > 0 = let dh = dispatchInfo d in do + -- NB: we *do not* want to terminate/dead-letter messages until + -- we've exhausted all the possible info handlers + m <- dh st msg + case m of + Nothing -> infoHandler pol st ds msg + Just act -> return act + -- but here we *do* let the policy kick in + | otherwise = let dh = dispatchInfo d in do + m <- dh st msg + case m of + Nothing -> applyPolicy st pol msg + Just act -> return act loop :: [Match (ProcessAction s)] -> TimeoutHandler s From a62886567283e1369537443bdd47a9173f43919b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 15:18:23 +0000 Subject: [PATCH 0671/2357] oops - we need to wrap the info handlers ourselves. --- .../Process/Platform/GenProcess.hs | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9db3a10e..e40e29b3 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -78,12 +78,10 @@ data Dispatcher s = dispatch :: s -> Message a -> Process (ProcessAction s) , dispatchIf :: s -> Message a -> Bool } - | DispatchInfo { - dispatchInfo :: UnhandledMessagePolicy - -> s - -> AbstractMessage - -> Process (ProcessAction s) - } + +data InfoDispatcher s = InfoDispatcher { + dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) + } -- | matches messages of specific types using a dispatcher class MessageMatcher d where @@ -93,7 +91,6 @@ class MessageMatcher d where instance MessageMatcher Dispatcher where matchMessage _ s (Dispatch d) = match (d s) matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) - matchMessage p s (DispatchInfo d) = matchAny (d p s) -- | Policy for handling unexpected messages, i.e., messages which are not -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the @@ -105,7 +102,7 @@ data UnhandledMessagePolicy = data Behaviour s = Behaviour { dispatchers :: [Dispatcher s] - , infoHandlers :: [Dispatcher s] + , infoHandlers :: [InfoDispatcher s] , timeoutHandler :: TimeoutHandler s , terminateHandler :: TerminateHandler s -- ^ termination handler , unhandledMessagePolicy :: UnhandledMessagePolicy @@ -199,20 +196,11 @@ handleCast :: (Serializable a) handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } handleInfo :: forall s a. (Serializable a) - => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleInfo h = DispatchInfo { - dispatchInfo = dispatchIt h - } - where dispatchIt :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> UnhandledMessagePolicy - -> s - -> AbstractMessage -> Process (ProcessAction s) - dispatchIt h' pol s msg = do - m <- maybeHandleMessage msg (h' s) - case m of - Nothing -> applyPolicy s pol msg - Just act -> return (act :: ProcessAction s) + => (s -> a -> Process (ProcessAction s)) + -> s + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) +handleInfo h' s msg = maybeHandleMessage msg (h' s) -- Process Implementation @@ -240,19 +228,35 @@ initLoop b s = -> [Match (ProcessAction s)] -> [Match (ProcessAction s)] addInfoHandlers b' s' p rms = - rms ++ addInfoAux p s' (infoHandlers b') [] + rms ++ addInfoAux p s' (infoHandlers b') - -- if there's more than one info handler then we /do not/ want to apply the - -- policy until we reach the last one, otherwise we'll miss out the others addInfoAux :: UnhandledMessagePolicy -> s - -> [Dispatcher s] - -> [Match (ProcessAction s)] + -> [InfoDispatcher s] -> [Match (ProcessAction s)] - addInfoAux _ _ [] _ = [] - addInfoAux p s'' (d:ds :: [Dispatcher s]) acc - | length ds == 0 = reverse ((matchMessage p s'' d):acc) - | otherwise = ((matchMessage Drop s'' d):(addInfoAux p s'' ds acc)) + addInfoAux _ _ [] = [] + addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] + + infoHandler :: UnhandledMessagePolicy + -> s + -> [InfoDispatcher s] + -> AbstractMessage + -> Process (ProcessAction s) + infoHandler _ _ [] _ = error "addInfoAux doest not permit this" + infoHandler pol st (d:ds :: [InfoDispatcher s]) msg + | length ds > 0 = let dh = dispatchInfo d in do + -- NB: we *do not* want to terminate/dead-letter messages until + -- we've exhausted all the possible info handlers + m <- dh st msg + case m of + Nothing -> infoHandler pol st ds msg + Just act -> return act + -- but here we *do* let the policy kick in + | otherwise = let dh = dispatchInfo d in do + m <- dh st msg + case m of + Nothing -> applyPolicy st pol msg + Just act -> return act loop :: [Match (ProcessAction s)] -> TimeoutHandler s From 49e17a4ac638a9feefa7d656974521f5c3d1fcee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 15:48:21 +0000 Subject: [PATCH 0672/2357] get the signature for handleInfo right --- .../Process/Platform/GenProcess.hs | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index e40e29b3..9083ccf1 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -98,7 +98,7 @@ instance MessageMatcher Dispatcher where data UnhandledMessagePolicy = Terminate | DeadLetter ProcessId - | Drop + | Drop data Behaviour s = Behaviour { dispatchers :: [Dispatcher s] @@ -197,10 +197,15 @@ handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) - -> s - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) -handleInfo h' s msg = maybeHandleMessage msg (h' s) + -> InfoDispatcher s +handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } + where + doHandleInfo :: forall s2 a2. (Serializable a2) + => (s2 -> a2 -> Process (ProcessAction s2)) + -> s2 + -> AbstractMessage + -> Process (Maybe (ProcessAction s2)) + doHandleInfo h' s msg = maybeHandleMessage msg (h' s) -- Process Implementation @@ -299,20 +304,35 @@ replyTo (SendToPid p) m = send p m replyTo (SendToService s) m = nsend s m replyTo (SendToRemoteService s n) m = nsendRemote n s m -demo :: Behaviour [String] +data Reset = Reset + deriving (Typeable) +$(derive makeBinary ''Reset) + +type MyState = [String] + +demo :: Behaviour MyState demo = Behaviour { dispatchers = [ handleCall add + , handleCast reset ] - , infoHandlers = [] + , infoHandlers = [handleInfo handleMonitorSignal] + , timeoutHandler = onTimeout , terminateHandler = undefined + , unhandledMessagePolicy = Drop } -add :: [String] -> String -> Process (ProcessReply [String] String) +add :: MyState -> String -> Process (ProcessReply MyState String) add s x = let s' = (x:s) in return $ reply "ok" s' -onTimeout :: TimeoutHandler [String] +reset :: MyState -> Reset -> Process (ProcessAction MyState) +reset _ Reset = return $ continue [] + +handleMonitorSignal :: MyState -> ProcessMonitorNotification -> Process (ProcessAction MyState) +handleMonitorSignal s (ProcessMonitorNotification _ _ _) = return $ continue s + +onTimeout :: TimeoutHandler MyState onTimeout _ _ = return ProcessStop { reason = (TerminateOther "timeout") } From 102f36319b755812fd5f0dda3270316fd6d4736e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 15:48:21 +0000 Subject: [PATCH 0673/2357] get the signature for handleInfo right --- .../Process/Platform/GenProcess.hs | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index e40e29b3..9083ccf1 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -98,7 +98,7 @@ instance MessageMatcher Dispatcher where data UnhandledMessagePolicy = Terminate | DeadLetter ProcessId - | Drop + | Drop data Behaviour s = Behaviour { dispatchers :: [Dispatcher s] @@ -197,10 +197,15 @@ handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) - -> s - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) -handleInfo h' s msg = maybeHandleMessage msg (h' s) + -> InfoDispatcher s +handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } + where + doHandleInfo :: forall s2 a2. (Serializable a2) + => (s2 -> a2 -> Process (ProcessAction s2)) + -> s2 + -> AbstractMessage + -> Process (Maybe (ProcessAction s2)) + doHandleInfo h' s msg = maybeHandleMessage msg (h' s) -- Process Implementation @@ -299,20 +304,35 @@ replyTo (SendToPid p) m = send p m replyTo (SendToService s) m = nsend s m replyTo (SendToRemoteService s n) m = nsendRemote n s m -demo :: Behaviour [String] +data Reset = Reset + deriving (Typeable) +$(derive makeBinary ''Reset) + +type MyState = [String] + +demo :: Behaviour MyState demo = Behaviour { dispatchers = [ handleCall add + , handleCast reset ] - , infoHandlers = [] + , infoHandlers = [handleInfo handleMonitorSignal] + , timeoutHandler = onTimeout , terminateHandler = undefined + , unhandledMessagePolicy = Drop } -add :: [String] -> String -> Process (ProcessReply [String] String) +add :: MyState -> String -> Process (ProcessReply MyState String) add s x = let s' = (x:s) in return $ reply "ok" s' -onTimeout :: TimeoutHandler [String] +reset :: MyState -> Reset -> Process (ProcessAction MyState) +reset _ Reset = return $ continue [] + +handleMonitorSignal :: MyState -> ProcessMonitorNotification -> Process (ProcessAction MyState) +handleMonitorSignal s (ProcessMonitorNotification _ _ _) = return $ continue s + +onTimeout :: TimeoutHandler MyState onTimeout _ _ = return ProcessStop { reason = (TerminateOther "timeout") } From 4a5161e4a170442ec8249a62ffa99f921856a82e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 15:54:00 +0000 Subject: [PATCH 0674/2357] make the utility functions run in the Process monad --- .../Process/Platform/GenProcess.hs | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9083ccf1..b8ba98e3 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -125,35 +125,38 @@ cast = undefined -- | INstructs the process to send a reply and continue working. -- > reply reply' state = replyWith reply' (continue state) -reply :: (Serializable r) => r -> s -> ProcessReply s r -reply r s = replyWith r (continue s) +reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) +reply r s = continue s >>= replyWith r -- | Instructs the process to send a reply and evaluate the 'ProcessAction' -- thereafter. -replyWith :: (Serializable m) => m -> ProcessAction s -> ProcessReply s m -replyWith msg state = ProcessReply msg state +replyWith :: (Serializable m) + => m + -> ProcessAction s + -> Process (ProcessReply s m) +replyWith msg state = return $ ProcessReply msg state -- | Instructs the process to continue running and receiving messages. -continue :: s -> ProcessAction s -continue s = ProcessContinue s +continue :: s -> Process (ProcessAction s) +continue s = return $ ProcessContinue s -- | Instructs the process to wait for incoming messages until 'TimeInterval' -- is exceeded. If no messages are handled during this period, the /timeout/ -- handler will be called. Note that this alters the process timeout permanently -- such that the given @TimeInterval@ will remain in use until changed. -timeoutAfter :: TimeInterval -> s -> ProcessAction s -timeoutAfter d s = ProcessTimeout d s +timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) +timeoutAfter d s = return $ ProcessTimeout d s -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to calling @threadDelay@. -- -hibernate :: TimeInterval -> s -> ProcessAction s -hibernate d s = ProcessHibernate d s +hibernate :: TimeInterval -> s -> Process (ProcessAction s) +hibernate d s = return $ ProcessHibernate d s -- | Instructs the process to cease, giving the supplied reason for termination. -stop :: TerminateReason -> ProcessAction s -stop r = ProcessStop r +stop :: TerminateReason -> Process (ProcessAction s) +stop r = return $ ProcessStop r -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, @@ -215,9 +218,9 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = do case p of - Terminate -> return $ stop (TerminateOther "unexpected-input") - DeadLetter pid -> forward m pid >> return (continue s) - Drop -> return (continue s) + Terminate -> stop (TerminateOther "unexpected-input") + DeadLetter pid -> forward m pid >> continue s + Drop -> continue s initLoop :: Behaviour s -> s -> Process TerminateReason initLoop b s = @@ -325,14 +328,13 @@ demo = Behaviour { add :: MyState -> String -> Process (ProcessReply MyState String) add s x = let s' = (x:s) - in return $ reply "ok" s' + in reply "ok" s' reset :: MyState -> Reset -> Process (ProcessAction MyState) -reset _ Reset = return $ continue [] +reset _ Reset = continue [] handleMonitorSignal :: MyState -> ProcessMonitorNotification -> Process (ProcessAction MyState) -handleMonitorSignal s (ProcessMonitorNotification _ _ _) = return $ continue s +handleMonitorSignal s (ProcessMonitorNotification _ _ _) = continue s onTimeout :: TimeoutHandler MyState -onTimeout _ _ = return ProcessStop { reason = (TerminateOther "timeout") } - +onTimeout _ _ = stop $ TerminateOther "timeout" From fd400fffe72e3d7b7de26102ea8d8ce4b654c349 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 15:54:00 +0000 Subject: [PATCH 0675/2357] make the utility functions run in the Process monad --- .../Process/Platform/GenProcess.hs | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9083ccf1..b8ba98e3 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -125,35 +125,38 @@ cast = undefined -- | INstructs the process to send a reply and continue working. -- > reply reply' state = replyWith reply' (continue state) -reply :: (Serializable r) => r -> s -> ProcessReply s r -reply r s = replyWith r (continue s) +reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) +reply r s = continue s >>= replyWith r -- | Instructs the process to send a reply and evaluate the 'ProcessAction' -- thereafter. -replyWith :: (Serializable m) => m -> ProcessAction s -> ProcessReply s m -replyWith msg state = ProcessReply msg state +replyWith :: (Serializable m) + => m + -> ProcessAction s + -> Process (ProcessReply s m) +replyWith msg state = return $ ProcessReply msg state -- | Instructs the process to continue running and receiving messages. -continue :: s -> ProcessAction s -continue s = ProcessContinue s +continue :: s -> Process (ProcessAction s) +continue s = return $ ProcessContinue s -- | Instructs the process to wait for incoming messages until 'TimeInterval' -- is exceeded. If no messages are handled during this period, the /timeout/ -- handler will be called. Note that this alters the process timeout permanently -- such that the given @TimeInterval@ will remain in use until changed. -timeoutAfter :: TimeInterval -> s -> ProcessAction s -timeoutAfter d s = ProcessTimeout d s +timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) +timeoutAfter d s = return $ ProcessTimeout d s -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to calling @threadDelay@. -- -hibernate :: TimeInterval -> s -> ProcessAction s -hibernate d s = ProcessHibernate d s +hibernate :: TimeInterval -> s -> Process (ProcessAction s) +hibernate d s = return $ ProcessHibernate d s -- | Instructs the process to cease, giving the supplied reason for termination. -stop :: TerminateReason -> ProcessAction s -stop r = ProcessStop r +stop :: TerminateReason -> Process (ProcessAction s) +stop r = return $ ProcessStop r -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, @@ -215,9 +218,9 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = do case p of - Terminate -> return $ stop (TerminateOther "unexpected-input") - DeadLetter pid -> forward m pid >> return (continue s) - Drop -> return (continue s) + Terminate -> stop (TerminateOther "unexpected-input") + DeadLetter pid -> forward m pid >> continue s + Drop -> continue s initLoop :: Behaviour s -> s -> Process TerminateReason initLoop b s = @@ -325,14 +328,13 @@ demo = Behaviour { add :: MyState -> String -> Process (ProcessReply MyState String) add s x = let s' = (x:s) - in return $ reply "ok" s' + in reply "ok" s' reset :: MyState -> Reset -> Process (ProcessAction MyState) -reset _ Reset = return $ continue [] +reset _ Reset = continue [] handleMonitorSignal :: MyState -> ProcessMonitorNotification -> Process (ProcessAction MyState) -handleMonitorSignal s (ProcessMonitorNotification _ _ _) = return $ continue s +handleMonitorSignal s (ProcessMonitorNotification _ _ _) = continue s onTimeout :: TimeoutHandler MyState -onTimeout _ _ = return ProcessStop { reason = (TerminateOther "timeout") } - +onTimeout _ _ = stop $ TerminateOther "timeout" From 96650f51657edcf70db03c39ff69504178286767 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 16:49:48 +0000 Subject: [PATCH 0676/2357] provide 'if' versions of the handler factories --- .../Process/Platform/GenProcess.hs | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index b8ba98e3..4f8cca1a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -158,17 +158,23 @@ hibernate d s = return $ ProcessHibernate d s stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r +handleCall :: (Serializable a, Serializable b) + => (s -> a -> Process (ProcessReply s b)) + -> Dispatcher s +handleCall handler = handleCallIf (const True) handler + -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion -- in a 'Behaviour' specification for the /GenProcess/. -- -handleCall :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) +handleCallIf :: (Serializable a, Serializable b) + => (a -> Bool) + -> (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall handler = DispatchIf { +handleCallIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheck + , dispatchIf = doCheck cond } where doHandle :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) @@ -179,8 +185,10 @@ handleCall handler = DispatchIf { doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - doCheck _ (CallMessage _ _) = True - doCheck _ _ = False + doCheck :: forall s a. (Serializable a) + => (a -> Bool) -> s -> Message a -> Bool + doCheck c _ (CallMessage m _) = c m + doCheck _ _ _ = False -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -196,7 +204,18 @@ handleCall handler = DispatchIf { -- handleCast :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } +handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } + +-- | Constructs a 'handleCast' handler, matching on the supplied condition. +-- +handleCastIf :: (Serializable a) + => (a -> Bool) + -> (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleCastIf cond h = DispatchIf { + dispatch = (\s (CastMessage p) -> h s p) + , dispatchIf = \_ msg -> cond (payload msg) + } handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) @@ -274,10 +293,10 @@ loop :: [Match (ProcessAction s)] loop ms h s t = do ac <- processReceive ms h s t case ac of - (ProcessContinue s') -> loop ms h s' t - (ProcessTimeout t' s') -> loop ms h s' (Delay t') + (ProcessContinue s') -> loop ms h s' t + (ProcessTimeout t' s') -> loop ms h s' (Delay t') (ProcessHibernate d' s') -> block d' >> loop ms h s' t - (ProcessStop r) -> return (r :: TerminateReason) + (ProcessStop r) -> return (r :: TerminateReason) where block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) From 4d5d85d6d2225278706084d76b1ef2218218ff6f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 16:49:48 +0000 Subject: [PATCH 0677/2357] provide 'if' versions of the handler factories --- .../Process/Platform/GenProcess.hs | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index b8ba98e3..4f8cca1a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -158,17 +158,23 @@ hibernate d s = return $ ProcessHibernate d s stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r +handleCall :: (Serializable a, Serializable b) + => (s -> a -> Process (ProcessReply s b)) + -> Dispatcher s +handleCall handler = handleCallIf (const True) handler + -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion -- in a 'Behaviour' specification for the /GenProcess/. -- -handleCall :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) +handleCallIf :: (Serializable a, Serializable b) + => (a -> Bool) + -> (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall handler = DispatchIf { +handleCallIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheck + , dispatchIf = doCheck cond } where doHandle :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) @@ -179,8 +185,10 @@ handleCall handler = DispatchIf { doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - doCheck _ (CallMessage _ _) = True - doCheck _ _ = False + doCheck :: forall s a. (Serializable a) + => (a -> Bool) -> s -> Message a -> Bool + doCheck c _ (CallMessage m _) = c m + doCheck _ _ _ = False -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -196,7 +204,18 @@ handleCall handler = DispatchIf { -- handleCast :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } +handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } + +-- | Constructs a 'handleCast' handler, matching on the supplied condition. +-- +handleCastIf :: (Serializable a) + => (a -> Bool) + -> (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleCastIf cond h = DispatchIf { + dispatch = (\s (CastMessage p) -> h s p) + , dispatchIf = \_ msg -> cond (payload msg) + } handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) @@ -274,10 +293,10 @@ loop :: [Match (ProcessAction s)] loop ms h s t = do ac <- processReceive ms h s t case ac of - (ProcessContinue s') -> loop ms h s' t - (ProcessTimeout t' s') -> loop ms h s' (Delay t') + (ProcessContinue s') -> loop ms h s' t + (ProcessTimeout t' s') -> loop ms h s' (Delay t') (ProcessHibernate d' s') -> block d' >> loop ms h s' t - (ProcessStop r) -> return (r :: TerminateReason) + (ProcessStop r) -> return (r :: TerminateReason) where block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) From 7a009c5c95c14d7a077554bd66d41f5790db5bae Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 16:50:41 +0000 Subject: [PATCH 0678/2357] simplify --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 4f8cca1a..ee554c8c 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -235,7 +235,7 @@ applyPolicy :: s -> UnhandledMessagePolicy -> AbstractMessage -> Process (ProcessAction s) -applyPolicy s p m = do +applyPolicy s p m = case p of Terminate -> stop (TerminateOther "unexpected-input") DeadLetter pid -> forward m pid >> continue s From 525b0be6cff0a124bad5386e4bf4840add5a2d04 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 16:50:41 +0000 Subject: [PATCH 0679/2357] simplify --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 4f8cca1a..ee554c8c 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -235,7 +235,7 @@ applyPolicy :: s -> UnhandledMessagePolicy -> AbstractMessage -> Process (ProcessAction s) -applyPolicy s p m = do +applyPolicy s p m = case p of Terminate -> stop (TerminateOther "unexpected-input") DeadLetter pid -> forward m pid >> continue s From 9e1db7f970166a5058fed0d2f37f17b1c84889ce Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 16:54:21 +0000 Subject: [PATCH 0680/2357] strip out unused pragmas --- src/Control/Distributed/Process/Platform/GenProcess.hs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index ee554c8c..1eb01ec0 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -1,15 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE ImpredicativeTypes #-} module Control.Distributed.Process.Platform.GenProcess where @@ -29,7 +21,7 @@ data ServerId = ServerId ProcessId | ServerName String data Recipient = SendToPid ProcessId | SendToService String - | SendToRemoteService String NodeId + | SendToRemoteService String NodeId deriving (Typeable) $(derive makeBinary ''Recipient) From 9a56139dd668ea99ee2d56c63b32f85887cc1ff7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 16:54:21 +0000 Subject: [PATCH 0681/2357] strip out unused pragmas --- src/Control/Distributed/Process/Platform/GenProcess.hs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index ee554c8c..1eb01ec0 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -1,15 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE ImpredicativeTypes #-} module Control.Distributed.Process.Platform.GenProcess where @@ -29,7 +21,7 @@ data ServerId = ServerId ProcessId | ServerName String data Recipient = SendToPid ProcessId | SendToService String - | SendToRemoteService String NodeId + | SendToRemoteService String NodeId deriving (Typeable) $(derive makeBinary ''Recipient) From ab0ff0988310de496d908f3f68717d1147d75aee Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 17:08:10 +0000 Subject: [PATCH 0682/2357] export an API, get rid of warnings --- .../Process/Platform/GenProcess.hs | 103 +++++++++--------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 1eb01ec0..5e1ac0fe 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -3,12 +3,38 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Process.Platform.GenProcess where +module Control.Distributed.Process.Platform.GenProcess + ( ServerId(..) + , Recipient(..) + , TerminateReason(..) + , InitResult(..) + , ProcessAction + , ProcessReply + , InitHandler + , TerminateHandler + , TimeoutHandler + , UnhandledMessagePolicy(..) + , Behaviour(..) + , start + , call + , cast + , reply + , replyWith + , continue + , timeoutAfter + , hibernate + , stop + , handleCall + , handleCallIf + , handleCast + , handleCastIf + , handleInfo + ) where -- TODO: define API and hide internals... import Control.Concurrent (threadDelay) -import Control.Distributed.Process +import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time import Data.Binary @@ -26,8 +52,8 @@ data Recipient = $(derive makeBinary ''Recipient) data Message a = - CastMessage { payload :: a } - | CallMessage { payload :: a, sender :: Recipient } + CastMessage a + | CallMessage a Recipient deriving (Typeable) $(derive makeBinary ''Message) @@ -42,20 +68,19 @@ data TerminateReason = -- | Initialization data InitResult s = InitOk s Delay - | forall r. (Serializable r) => InitStop r + | forall r. (Serializable r) => InitFail r data ProcessAction s = - ProcessContinue { nextState :: s } - | ProcessTimeout { delay :: TimeInterval, nextState :: s } - | ProcessHibernate { duration :: TimeInterval, nextState :: s } - | ProcessStop { reason :: TerminateReason } + ProcessContinue s + | ProcessTimeout TimeInterval s + | ProcessHibernate TimeInterval s + | ProcessStop TerminateReason data ProcessReply s a = - ProcessReply { response :: a - , action :: ProcessAction s } - | NoReply { action :: ProcessAction s} + ProcessReply a (ProcessAction s) + | NoReply (ProcessAction s) -type InitHandler a s = a -> InitResult s +type InitHandler a s = a -> Process (InitResult s) type TerminateHandler s = s -> TerminateReason -> Process () type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) @@ -104,8 +129,12 @@ data Behaviour s = Behaviour { -- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- -start :: Process () -start = undefined +start :: a -> InitHandler a s -> Behaviour s -> Process TerminateReason +start args init behave = do + ir <- init args + case ir of + InitOk initState initDelay -> initLoop behave initState initDelay + InitFail why -> return $ TerminateOther why call :: Process () call = undefined @@ -150,6 +179,8 @@ hibernate d s = return $ ProcessHibernate d s stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r +-- wrapping /normal/ functions with Dispatcher + handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s @@ -206,9 +237,11 @@ handleCastIf :: (Serializable a) -> Dispatcher s handleCastIf cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) - , dispatchIf = \_ msg -> cond (payload msg) + , dispatchIf = \_ (CastMessage msg) -> cond msg } +-- wrapping /normal/ functions with InfoDispatcher + handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) -> InfoDispatcher s @@ -233,13 +266,13 @@ applyPolicy s p m = DeadLetter pid -> forward m pid >> continue s Drop -> continue s -initLoop :: Behaviour s -> s -> Process TerminateReason -initLoop b s = +initLoop :: Behaviour s -> s -> Delay -> Process TerminateReason +initLoop b s w = let p = unhandledMessagePolicy b t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) ms' = addInfoHandlers b s p ms - in loop ms' t s Infinity + in loop ms' t s w where addInfoHandlers :: Behaviour s -> s @@ -317,35 +350,3 @@ replyTo :: (Serializable m) => Recipient -> m -> Process () replyTo (SendToPid p) m = send p m replyTo (SendToService s) m = nsend s m replyTo (SendToRemoteService s n) m = nsendRemote n s m - -data Reset = Reset - deriving (Typeable) -$(derive makeBinary ''Reset) - -type MyState = [String] - -demo :: Behaviour MyState -demo = Behaviour { - dispatchers = [ - handleCall add - , handleCast reset - ] - , infoHandlers = [handleInfo handleMonitorSignal] - , timeoutHandler = onTimeout - , terminateHandler = undefined - , unhandledMessagePolicy = Drop - } - -add :: MyState -> String -> Process (ProcessReply MyState String) -add s x = - let s' = (x:s) - in reply "ok" s' - -reset :: MyState -> Reset -> Process (ProcessAction MyState) -reset _ Reset = continue [] - -handleMonitorSignal :: MyState -> ProcessMonitorNotification -> Process (ProcessAction MyState) -handleMonitorSignal s (ProcessMonitorNotification _ _ _) = continue s - -onTimeout :: TimeoutHandler MyState -onTimeout _ _ = stop $ TerminateOther "timeout" From 2bc507ac720e3419c7a4484523482f207d0a8caf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 17:08:10 +0000 Subject: [PATCH 0683/2357] export an API, get rid of warnings --- .../Process/Platform/GenProcess.hs | 103 +++++++++--------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 1eb01ec0..5e1ac0fe 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -3,12 +3,38 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Process.Platform.GenProcess where +module Control.Distributed.Process.Platform.GenProcess + ( ServerId(..) + , Recipient(..) + , TerminateReason(..) + , InitResult(..) + , ProcessAction + , ProcessReply + , InitHandler + , TerminateHandler + , TimeoutHandler + , UnhandledMessagePolicy(..) + , Behaviour(..) + , start + , call + , cast + , reply + , replyWith + , continue + , timeoutAfter + , hibernate + , stop + , handleCall + , handleCallIf + , handleCast + , handleCastIf + , handleInfo + ) where -- TODO: define API and hide internals... import Control.Concurrent (threadDelay) -import Control.Distributed.Process +import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time import Data.Binary @@ -26,8 +52,8 @@ data Recipient = $(derive makeBinary ''Recipient) data Message a = - CastMessage { payload :: a } - | CallMessage { payload :: a, sender :: Recipient } + CastMessage a + | CallMessage a Recipient deriving (Typeable) $(derive makeBinary ''Message) @@ -42,20 +68,19 @@ data TerminateReason = -- | Initialization data InitResult s = InitOk s Delay - | forall r. (Serializable r) => InitStop r + | forall r. (Serializable r) => InitFail r data ProcessAction s = - ProcessContinue { nextState :: s } - | ProcessTimeout { delay :: TimeInterval, nextState :: s } - | ProcessHibernate { duration :: TimeInterval, nextState :: s } - | ProcessStop { reason :: TerminateReason } + ProcessContinue s + | ProcessTimeout TimeInterval s + | ProcessHibernate TimeInterval s + | ProcessStop TerminateReason data ProcessReply s a = - ProcessReply { response :: a - , action :: ProcessAction s } - | NoReply { action :: ProcessAction s} + ProcessReply a (ProcessAction s) + | NoReply (ProcessAction s) -type InitHandler a s = a -> InitResult s +type InitHandler a s = a -> Process (InitResult s) type TerminateHandler s = s -> TerminateReason -> Process () type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) @@ -104,8 +129,12 @@ data Behaviour s = Behaviour { -- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- -start :: Process () -start = undefined +start :: a -> InitHandler a s -> Behaviour s -> Process TerminateReason +start args init behave = do + ir <- init args + case ir of + InitOk initState initDelay -> initLoop behave initState initDelay + InitFail why -> return $ TerminateOther why call :: Process () call = undefined @@ -150,6 +179,8 @@ hibernate d s = return $ ProcessHibernate d s stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r +-- wrapping /normal/ functions with Dispatcher + handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s @@ -206,9 +237,11 @@ handleCastIf :: (Serializable a) -> Dispatcher s handleCastIf cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) - , dispatchIf = \_ msg -> cond (payload msg) + , dispatchIf = \_ (CastMessage msg) -> cond msg } +-- wrapping /normal/ functions with InfoDispatcher + handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) -> InfoDispatcher s @@ -233,13 +266,13 @@ applyPolicy s p m = DeadLetter pid -> forward m pid >> continue s Drop -> continue s -initLoop :: Behaviour s -> s -> Process TerminateReason -initLoop b s = +initLoop :: Behaviour s -> s -> Delay -> Process TerminateReason +initLoop b s w = let p = unhandledMessagePolicy b t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) ms' = addInfoHandlers b s p ms - in loop ms' t s Infinity + in loop ms' t s w where addInfoHandlers :: Behaviour s -> s @@ -317,35 +350,3 @@ replyTo :: (Serializable m) => Recipient -> m -> Process () replyTo (SendToPid p) m = send p m replyTo (SendToService s) m = nsend s m replyTo (SendToRemoteService s n) m = nsendRemote n s m - -data Reset = Reset - deriving (Typeable) -$(derive makeBinary ''Reset) - -type MyState = [String] - -demo :: Behaviour MyState -demo = Behaviour { - dispatchers = [ - handleCall add - , handleCast reset - ] - , infoHandlers = [handleInfo handleMonitorSignal] - , timeoutHandler = onTimeout - , terminateHandler = undefined - , unhandledMessagePolicy = Drop - } - -add :: MyState -> String -> Process (ProcessReply MyState String) -add s x = - let s' = (x:s) - in reply "ok" s' - -reset :: MyState -> Reset -> Process (ProcessAction MyState) -reset _ Reset = continue [] - -handleMonitorSignal :: MyState -> ProcessMonitorNotification -> Process (ProcessAction MyState) -handleMonitorSignal s (ProcessMonitorNotification _ _ _) = continue s - -onTimeout :: TimeoutHandler MyState -onTimeout _ _ = stop $ TerminateOther "timeout" From b0dcd775ecc5092302c856ec839568bd67f21ea1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 17:09:47 +0000 Subject: [PATCH 0684/2357] add comments and close TODO --- src/Control/Distributed/Process/Platform/GenProcess.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5e1ac0fe..4572745d 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -4,7 +4,8 @@ {-# LANGUAGE TemplateHaskell #-} module Control.Distributed.Process.Platform.GenProcess - ( ServerId(..) + ( -- exported data types + ServerId(..) , Recipient(..) , TerminateReason(..) , InitResult(..) @@ -15,15 +16,18 @@ module Control.Distributed.Process.Platform.GenProcess , TimeoutHandler , UnhandledMessagePolicy(..) , Behaviour(..) + -- interaction with the process , start , call , cast + -- interaction inside the process , reply , replyWith , continue , timeoutAfter , hibernate , stop + -- callback creation , handleCall , handleCallIf , handleCast @@ -31,8 +35,6 @@ module Control.Distributed.Process.Platform.GenProcess , handleInfo ) where --- TODO: define API and hide internals... - import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable From 9460f80644ef91433b77628c2e9679bc979b9bb1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 17:09:47 +0000 Subject: [PATCH 0685/2357] add comments and close TODO --- src/Control/Distributed/Process/Platform/GenProcess.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5e1ac0fe..4572745d 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -4,7 +4,8 @@ {-# LANGUAGE TemplateHaskell #-} module Control.Distributed.Process.Platform.GenProcess - ( ServerId(..) + ( -- exported data types + ServerId(..) , Recipient(..) , TerminateReason(..) , InitResult(..) @@ -15,15 +16,18 @@ module Control.Distributed.Process.Platform.GenProcess , TimeoutHandler , UnhandledMessagePolicy(..) , Behaviour(..) + -- interaction with the process , start , call , cast + -- interaction inside the process , reply , replyWith , continue , timeoutAfter , hibernate , stop + -- callback creation , handleCall , handleCallIf , handleCast @@ -31,8 +35,6 @@ module Control.Distributed.Process.Platform.GenProcess , handleInfo ) where --- TODO: define API and hide internals... - import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable From 2fa96da690c8b69c011e5c675d304ae94bf6ddf5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 17:39:27 +0000 Subject: [PATCH 0686/2357] use Async to give us an API --- .../Process/Platform/GenProcess.hs | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 4572745d..130e2f01 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -19,6 +19,8 @@ module Control.Distributed.Process.Platform.GenProcess -- interaction with the process , start , call + , callAsync + , callTimeout , cast -- interaction inside the process , reply @@ -39,6 +41,9 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Platform.Async (asyncDo) +import Control.Distributed.Process.Platform.Async.AsyncChan + import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -59,6 +64,10 @@ data Message a = deriving (Typeable) $(derive makeBinary ''Message) +data CallResponse a = CallResponse a + deriving (Typeable) +$(derive makeBinary ''CallResponse) + -- | Terminate reason data TerminateReason = TerminateNormal @@ -138,8 +147,42 @@ start args init behave = do InitOk initState initDelay -> initLoop behave initState initDelay InitFail why -> return $ TerminateOther why -call :: Process () -call = undefined +-- | Make a syncrhonous call +call :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process b +call sid msg = callAsync sid msg >>= wait >>= unpack + where unpack :: AsyncResult b -> Process b + unpack (AsyncDone r) = return r + unpack _ = fail "boo hoo" + +callTimeout :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> TimeInterval -> Process (Maybe b) +callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack + where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) + unpack Nothing = return Nothing + unpack (Just (AsyncDone r)) = return $ Just r + unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate +-- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 + +callAsync :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process (AsyncChan b) +callAsync sid msg = do + self <- getSelfPid + mRef <- monitor sid +-- TODO: use a unified async API here if possible +-- https://github.com/haskell-distributed/distributed-process-platform/issues/55 + async $ asyncDo $ do + sendTo (SendToPid sid) (CallMessage msg (SendToPid self)) + r <- receiveWait [ + match (\((CallResponse m) :: CallResponse b) -> return (Right m)) + , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) + (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) + ] + case r of + Right m -> return m + Left err -> fail $ "call: remote process died: " ++ show err + + cast :: Process () cast = undefined @@ -220,7 +263,7 @@ handleCallIf cond handler = DispatchIf { mkReply :: (Serializable b) => Recipient -> ProcessReply s b -> Process (ProcessAction s) mkReply _ (NoReply a) = return a - mkReply c (ProcessReply r' a) = replyTo c r' >> return a + mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, @@ -348,7 +391,7 @@ processReceive ms h s t = do -- internal/utility -replyTo :: (Serializable m) => Recipient -> m -> Process () -replyTo (SendToPid p) m = send p m -replyTo (SendToService s) m = nsend s m -replyTo (SendToRemoteService s n) m = nsendRemote n s m +sendTo :: (Serializable m) => Recipient -> m -> Process () +sendTo (SendToPid p) m = send p m +sendTo (SendToService s) m = nsend s m +sendTo (SendToRemoteService s n) m = nsendRemote n s m From f9910b346c85e2665e7b613e7983e155d227837f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 17:39:27 +0000 Subject: [PATCH 0687/2357] use Async to give us an API --- .../Process/Platform/GenProcess.hs | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 4572745d..130e2f01 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -19,6 +19,8 @@ module Control.Distributed.Process.Platform.GenProcess -- interaction with the process , start , call + , callAsync + , callTimeout , cast -- interaction inside the process , reply @@ -39,6 +41,9 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Platform.Async (asyncDo) +import Control.Distributed.Process.Platform.Async.AsyncChan + import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -59,6 +64,10 @@ data Message a = deriving (Typeable) $(derive makeBinary ''Message) +data CallResponse a = CallResponse a + deriving (Typeable) +$(derive makeBinary ''CallResponse) + -- | Terminate reason data TerminateReason = TerminateNormal @@ -138,8 +147,42 @@ start args init behave = do InitOk initState initDelay -> initLoop behave initState initDelay InitFail why -> return $ TerminateOther why -call :: Process () -call = undefined +-- | Make a syncrhonous call +call :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process b +call sid msg = callAsync sid msg >>= wait >>= unpack + where unpack :: AsyncResult b -> Process b + unpack (AsyncDone r) = return r + unpack _ = fail "boo hoo" + +callTimeout :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> TimeInterval -> Process (Maybe b) +callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack + where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) + unpack Nothing = return Nothing + unpack (Just (AsyncDone r)) = return $ Just r + unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate +-- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 + +callAsync :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process (AsyncChan b) +callAsync sid msg = do + self <- getSelfPid + mRef <- monitor sid +-- TODO: use a unified async API here if possible +-- https://github.com/haskell-distributed/distributed-process-platform/issues/55 + async $ asyncDo $ do + sendTo (SendToPid sid) (CallMessage msg (SendToPid self)) + r <- receiveWait [ + match (\((CallResponse m) :: CallResponse b) -> return (Right m)) + , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) + (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) + ] + case r of + Right m -> return m + Left err -> fail $ "call: remote process died: " ++ show err + + cast :: Process () cast = undefined @@ -220,7 +263,7 @@ handleCallIf cond handler = DispatchIf { mkReply :: (Serializable b) => Recipient -> ProcessReply s b -> Process (ProcessAction s) mkReply _ (NoReply a) = return a - mkReply c (ProcessReply r' a) = replyTo c r' >> return a + mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, @@ -348,7 +391,7 @@ processReceive ms h s t = do -- internal/utility -replyTo :: (Serializable m) => Recipient -> m -> Process () -replyTo (SendToPid p) m = send p m -replyTo (SendToService s) m = nsend s m -replyTo (SendToRemoteService s n) m = nsendRemote n s m +sendTo :: (Serializable m) => Recipient -> m -> Process () +sendTo (SendToPid p) m = send p m +sendTo (SendToService s) m = nsend s m +sendTo (SendToRemoteService s n) m = nsendRemote n s m From 011417a8b4adf53b20670f9ce7f615687a2b43ce Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:25:41 +0000 Subject: [PATCH 0688/2357] implement cast --- src/Control/Distributed/Process/Platform/GenProcess.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 130e2f01..9a57a5aa 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -181,11 +181,12 @@ callAsync sid msg = do case r of Right m -> return m Left err -> fail $ "call: remote process died: " ++ show err - - -cast :: Process () -cast = undefined +-- | Sends a /cast/ message to the server identified by 'ServerId'. The server +-- will not send a response. +cast :: forall a . (Serializable a) + => ProcessId -> a -> Process () +cast sid msg = send sid (CastMessage msg) -- Constructing Handlers from *ordinary* functions From 8dd33ee91646f5641982026aa14830be8511525d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:25:41 +0000 Subject: [PATCH 0689/2357] implement cast --- src/Control/Distributed/Process/Platform/GenProcess.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 130e2f01..9a57a5aa 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -181,11 +181,12 @@ callAsync sid msg = do case r of Right m -> return m Left err -> fail $ "call: remote process died: " ++ show err - - -cast :: Process () -cast = undefined +-- | Sends a /cast/ message to the server identified by 'ServerId'. The server +-- will not send a response. +cast :: forall a . (Serializable a) + => ProcessId -> a -> Process () +cast sid msg = send sid (CastMessage msg) -- Constructing Handlers from *ordinary* functions From 2c4eddda979d653815204c6e8181717ab8617ca1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:26:03 +0000 Subject: [PATCH 0690/2357] typo --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9a57a5aa..6b36e7e5 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -190,7 +190,7 @@ cast sid msg = send sid (CastMessage msg) -- Constructing Handlers from *ordinary* functions --- | INstructs the process to send a reply and continue working. +-- | Instructs the process to send a reply and continue working. -- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r From a18e6504892505f717ac3d2e2046d75bc4764a0e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:26:03 +0000 Subject: [PATCH 0691/2357] typo --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9a57a5aa..6b36e7e5 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -190,7 +190,7 @@ cast sid msg = send sid (CastMessage msg) -- Constructing Handlers from *ordinary* functions --- | INstructs the process to send a reply and continue working. +-- | Instructs the process to send a reply and continue working. -- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r From f469562b8d1d568da91a55f49128ed6eaba1d48d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:35:01 +0000 Subject: [PATCH 0692/2357] add some TODO --- src/Control/Distributed/Process/Platform/GenProcess.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 6b36e7e5..84e909cb 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -155,6 +155,8 @@ call sid msg = callAsync sid msg >>= wait >>= unpack unpack (AsyncDone r) = return r unpack _ = fail "boo hoo" +-- TODO: provide version of call that will throw/exit on failure + callTimeout :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack @@ -178,6 +180,7 @@ callAsync sid msg = do , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) ] + -- TODO: better failure API case r of Right m -> return m Left err -> fail $ "call: remote process died: " ++ show err From 6547b6d3a8585ac14db1d893889a6a09e502560e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:35:01 +0000 Subject: [PATCH 0693/2357] add some TODO --- src/Control/Distributed/Process/Platform/GenProcess.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 6b36e7e5..84e909cb 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -155,6 +155,8 @@ call sid msg = callAsync sid msg >>= wait >>= unpack unpack (AsyncDone r) = return r unpack _ = fail "boo hoo" +-- TODO: provide version of call that will throw/exit on failure + callTimeout :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack @@ -178,6 +180,7 @@ callAsync sid msg = do , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) ] + -- TODO: better failure API case r of Right m -> return m Left err -> fail $ "call: remote process died: " ++ show err From be9e21ce814ed8d140c36c29dfec7b44136c26da Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:35:40 +0000 Subject: [PATCH 0694/2357] API comments --- src/Control/Distributed/Process/AsyncChan.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 0de81bcb..5ed23a74 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -59,6 +59,10 @@ import Data.Maybe -- | Private channel used to synchronise task results type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) +-------------------------------------------------------------------------------- +-- Cloud Haskell Typed Channel Async API -- +-------------------------------------------------------------------------------- + -- | A handle for an asynchronous action spawned by 'async'. -- Asynchronous actions are run in a separate process, and -- operations are provided for waiting for asynchronous actions to From 61e9f2372ea647af5866ced7e9c48fb76783706e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 14 Jan 2013 18:35:53 +0000 Subject: [PATCH 0695/2357] rename parameters --- src/Control/Distributed/Process/Platform/Time.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index fc52c459..33506dfe 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -126,9 +126,9 @@ timeToMs :: TimeUnit -> Int -> Int timeToMs Micros us = us timeToMs Millis ms = ms * (10 ^ (3 :: Int)) timeToMs Seconds sec = sec * (10 ^ (6 :: Int)) -timeToMs Minutes mins = (mins * 60) * (10 ^ (6 :: Int)) -timeToMs Hours hrs = ((hrs * 60) * 60) * (10 ^ (6 :: Int)) -timeToMs Days days = (((days * 24) * 60) * 60) * (10 ^ (6 :: Int)) +timeToMs Minutes sec = (sec * 60) * (10 ^ (6 :: Int)) +timeToMs Hours mins = ((mins * 60) * 60) * (10 ^ (6 :: Int)) +timeToMs Days hrs = (((hrs * 24) * 60) * 60) * (10 ^ (6 :: Int)) -- timeouts/delays (microseconds) From a85e50324cbdd91c86475ef9625c3a6989e07a73 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 00:57:43 +0000 Subject: [PATCH 0696/2357] safeCall for error detection without failure --- src/Control/Distributed/Process/Platform/GenProcess.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 84e909cb..5495c9eb 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -19,6 +19,7 @@ module Control.Distributed.Process.Platform.GenProcess -- interaction with the process , start , call + , safeCall , callAsync , callTimeout , cast @@ -140,6 +141,8 @@ data Behaviour s = Behaviour { -- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- +-- TODO: automatic registration + start :: a -> InitHandler a s -> Behaviour s -> Process TerminateReason start args init behave = do ir <- init args @@ -155,6 +158,13 @@ call sid msg = callAsync sid msg >>= wait >>= unpack unpack (AsyncDone r) = return r unpack _ = fail "boo hoo" +-- | Safe version of 'call' that returns 'Nothing' if the operation fails. +safeCall :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process (Maybe b) +safeCall s m = callAsync s m >>= wait >>= unpack + where unpack (AsyncDone r) = return $ Just r + unpack _ = return Nothing + -- TODO: provide version of call that will throw/exit on failure callTimeout :: forall a b . (Serializable a, Serializable b) From 70fbf8c814fa660b01b5c8446fe7d56dc2a78018 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 00:57:43 +0000 Subject: [PATCH 0697/2357] safeCall for error detection without failure --- src/Control/Distributed/Process/Platform/GenProcess.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 84e909cb..5495c9eb 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -19,6 +19,7 @@ module Control.Distributed.Process.Platform.GenProcess -- interaction with the process , start , call + , safeCall , callAsync , callTimeout , cast @@ -140,6 +141,8 @@ data Behaviour s = Behaviour { -- Cloud Haskell Generic Process API -- -------------------------------------------------------------------------------- +-- TODO: automatic registration + start :: a -> InitHandler a s -> Behaviour s -> Process TerminateReason start args init behave = do ir <- init args @@ -155,6 +158,13 @@ call sid msg = callAsync sid msg >>= wait >>= unpack unpack (AsyncDone r) = return r unpack _ = fail "boo hoo" +-- | Safe version of 'call' that returns 'Nothing' if the operation fails. +safeCall :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process (Maybe b) +safeCall s m = callAsync s m >>= wait >>= unpack + where unpack (AsyncDone r) = return $ Just r + unpack _ = return Nothing + -- TODO: provide version of call that will throw/exit on failure callTimeout :: forall a b . (Serializable a, Serializable b) From 12de0f35830c2ad3c2112a869152fd0e1e5cc9f0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:02:38 +0000 Subject: [PATCH 0698/2357] drain info messages and apply policy even when no info handlers exist fixes #56 --- .../Distributed/Process/Platform/GenProcess.hs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5495c9eb..369b5474 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -330,22 +330,13 @@ initLoop b s w = let p = unhandledMessagePolicy b t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) - ms' = addInfoHandlers b s p ms + ms' = ms ++ addInfoAux p s (infoHandlers b) in loop ms' t s w where - addInfoHandlers :: Behaviour s - -> s - -> UnhandledMessagePolicy - -> [Match (ProcessAction s)] - -> [Match (ProcessAction s)] - addInfoHandlers b' s' p rms = - rms ++ addInfoAux p s' (infoHandlers b') - addInfoAux :: UnhandledMessagePolicy -> s -> [InfoDispatcher s] -> [Match (ProcessAction s)] - addInfoAux _ _ [] = [] addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] infoHandler :: UnhandledMessagePolicy @@ -353,7 +344,7 @@ initLoop b s w = -> [InfoDispatcher s] -> AbstractMessage -> Process (ProcessAction s) - infoHandler _ _ [] _ = error "addInfoAux doest not permit this" + infoHandler pol st [] msg = applyPolicy st pol msg infoHandler pol st (d:ds :: [InfoDispatcher s]) msg | length ds > 0 = let dh = dispatchInfo d in do -- NB: we *do not* want to terminate/dead-letter messages until From 7210efb30bcd36519c24b1a4546d42be0c974c61 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:02:38 +0000 Subject: [PATCH 0699/2357] drain info messages and apply policy even when no info handlers exist fixes #56 --- .../Distributed/Process/Platform/GenProcess.hs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5495c9eb..369b5474 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -330,22 +330,13 @@ initLoop b s w = let p = unhandledMessagePolicy b t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) - ms' = addInfoHandlers b s p ms + ms' = ms ++ addInfoAux p s (infoHandlers b) in loop ms' t s w where - addInfoHandlers :: Behaviour s - -> s - -> UnhandledMessagePolicy - -> [Match (ProcessAction s)] - -> [Match (ProcessAction s)] - addInfoHandlers b' s' p rms = - rms ++ addInfoAux p s' (infoHandlers b') - addInfoAux :: UnhandledMessagePolicy -> s -> [InfoDispatcher s] -> [Match (ProcessAction s)] - addInfoAux _ _ [] = [] addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] infoHandler :: UnhandledMessagePolicy @@ -353,7 +344,7 @@ initLoop b s w = -> [InfoDispatcher s] -> AbstractMessage -> Process (ProcessAction s) - infoHandler _ _ [] _ = error "addInfoAux doest not permit this" + infoHandler pol st [] msg = applyPolicy st pol msg infoHandler pol st (d:ds :: [InfoDispatcher s]) msg | length ds > 0 = let dh = dispatchInfo d in do -- NB: we *do not* want to terminate/dead-letter messages until From fef26ee2117e0450abd41dd0763756cbf88055e1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:19:03 +0000 Subject: [PATCH 0700/2357] helps if you monitor from the right place --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 369b5474..edb69de9 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -180,10 +180,10 @@ callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncChan b) callAsync sid msg = do self <- getSelfPid - mRef <- monitor sid -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 async $ asyncDo $ do + mRef <- monitor sid sendTo (SendToPid sid) (CallMessage msg (SendToPid self)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) From 20fa4d6266924677db0d8da49a7c445a57820e78 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:19:03 +0000 Subject: [PATCH 0701/2357] helps if you monitor from the right place --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 369b5474..edb69de9 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -180,10 +180,10 @@ callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncChan b) callAsync sid msg = do self <- getSelfPid - mRef <- monitor sid -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 async $ asyncDo $ do + mRef <- monitor sid sendTo (SendToPid sid) (CallMessage msg (SendToPid self)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) From fb69e04a75381c6d6fbb7139d7f9c748c66b584f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:26:51 +0000 Subject: [PATCH 0702/2357] re-use a bit --- src/Control/Distributed/Process/Platform/GenProcess.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index edb69de9..c06861b9 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -153,10 +153,11 @@ start args init behave = do -- | Make a syncrhonous call call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b -call sid msg = callAsync sid msg >>= wait >>= unpack - where unpack :: AsyncResult b -> Process b - unpack (AsyncDone r) = return r - unpack _ = fail "boo hoo" +call sid msg = do + r <- safeCall sid msg + case r of + Nothing -> fail "call failed" -- TODO: exit protocol !? + Just ar -> return ar -- | Safe version of 'call' that returns 'Nothing' if the operation fails. safeCall :: forall a b . (Serializable a, Serializable b) From b3f99df72b83f8e4049efbdea8d119948ebc7c42 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:26:51 +0000 Subject: [PATCH 0703/2357] re-use a bit --- src/Control/Distributed/Process/Platform/GenProcess.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index edb69de9..c06861b9 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -153,10 +153,11 @@ start args init behave = do -- | Make a syncrhonous call call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b -call sid msg = callAsync sid msg >>= wait >>= unpack - where unpack :: AsyncResult b -> Process b - unpack (AsyncDone r) = return r - unpack _ = fail "boo hoo" +call sid msg = do + r <- safeCall sid msg + case r of + Nothing -> fail "call failed" -- TODO: exit protocol !? + Just ar -> return ar -- | Safe version of 'call' that returns 'Nothing' if the operation fails. safeCall :: forall a b . (Serializable a, Serializable b) From b8ee9b3f384b8d25f1f109157a4ae8817824c336 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:39:24 +0000 Subject: [PATCH 0704/2357] doc fix/update package names --- src/Control/Distributed/Process/Async.hs | 2 +- src/Control/Distributed/Process/AsyncChan.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 62cf0a75..9bbf0d0e 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -3,7 +3,7 @@ {-# LANGUAGE StandaloneDeriving #-} ----------------------------------------------------------------------------- -- | --- Module : Control.Distributed.Platform.Async +-- Module : Control.Distributed.Process.Platform.Async -- Copyright : (c) Tim Watson 2012 -- License : BSD3 (see the file LICENSE) -- diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 5ed23a74..71e4ce98 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -4,7 +4,7 @@ ----------------------------------------------------------------------------- -- | --- Module : Control.Distributed.Platform.Async.AsyncChan +-- Module : Control.Distributed.Process.Platform.Async.AsyncChan -- Copyright : (c) Tim Watson 2012 -- License : BSD3 (see the file LICENSE) -- From 3ece4812de126778776091c9bc2ece3462bd8ea5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:50:33 +0000 Subject: [PATCH 0705/2357] tidy up timeToMs and make it more readable --- .../Distributed/Process/Platform/Time.hs | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index 33506dfe..d43eb9d8 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} @@ -119,17 +120,34 @@ minutes = TimeInterval Minutes hours :: Int -> TimeInterval hours = TimeInterval Hours --- TODO: timeToMs is not exactly efficient and we may want to scale it up +-- TODO: is timeToMs efficient? -- | converts the supplied @TimeUnit@ to microseconds +{-# INLINE timeToMs #-} timeToMs :: TimeUnit -> Int -> Int timeToMs Micros us = us -timeToMs Millis ms = ms * (10 ^ (3 :: Int)) -timeToMs Seconds sec = sec * (10 ^ (6 :: Int)) -timeToMs Minutes sec = (sec * 60) * (10 ^ (6 :: Int)) -timeToMs Hours mins = ((mins * 60) * 60) * (10 ^ (6 :: Int)) -timeToMs Days hrs = (((hrs * 24) * 60) * 60) * (10 ^ (6 :: Int)) - +timeToMs Millis ms = ms * (10 ^ (3 :: Int)) -- (1000s == 1ms) +timeToMs Seconds secs = timeToMs Millis (secs * milliSecondsPerSecond) +timeToMs Minutes mins = timeToMs Seconds (mins * secondsPerMinute) +timeToMs Hours hrs = timeToMs Minutes (hrs * minutesPerHour) +timeToMs Days days = timeToMs Hours (days * hoursPerDay) + +{-# INLINE hoursPerDay #-} +hoursPerDay :: Int +hoursPerDay = 60 + +{-# INLINE minutesPerHour #-} +minutesPerHour :: Int +minutesPerHour = 60 + +{-# INLINE secondsPerMinute #-} +secondsPerMinute :: Int +secondsPerMinute = 60 + +{-# INLINE milliSecondsPerSecond #-} +milliSecondsPerSecond :: Int +milliSecondsPerSecond = 1000 + -- timeouts/delays (microseconds) -- | Constructs an inifinite 'Timeout'. From cf53960e0e89aacca5e8b0e2d3f3f9cd9f3bdd85 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 02:56:12 +0000 Subject: [PATCH 0706/2357] cosmetic --- src/Control/Distributed/Process/Platform/Timer.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index 1268e196..1dd100da 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -21,8 +21,8 @@ import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) +import Data.Typeable (Typeable) +import Prelude hiding (init) -- | an opaque reference to a timer type TimerRef = ProcessId From 5e269ad6742c27004e1fbcb7476c235409ff99ba Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 03:15:26 +0000 Subject: [PATCH 0707/2357] documentation --- .../Process/Platform/Internal/Primitives.hs | 2 +- .../Distributed/Process/Platform/Timer.hs | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index 39cd060f..0b7e22ff 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -66,7 +66,7 @@ spawnMonitorLocal p = do -- | CH's 'link' primitive, unlike Erlang's, will trigger when the target -- process dies for any reason. linkOnFailure has semantics like Erlang's: --- it will trigger only when the target function dies abnormally. +-- it will trigger only when the target dies abnormally. linkOnFailure :: ProcessId -> Process () linkOnFailure them = do us <- getSelfPid diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index 1dd100da..d119cfed 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -1,6 +1,21 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.Timer +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- Provides an API for running code or sending messages, either after some +-- initial delay or periodically, and for cancelling, re-setting and/or +-- flushing pending /timers/. +----------------------------------------------------------------------------- + module Control.Distributed.Process.Platform.Timer ( TimerRef @@ -47,9 +62,8 @@ $(derive makeBinary ''SleepingPill) -- | blocks the calling Process for the specified TimeInterval. Note that this -- function assumes that a blocking receive is the most efficient approach to --- acheiving this, so expect the runtime semantics (particularly with regards --- scheduling) to differ from threadDelay and/or operating system specific --- functions that offer the same results. +-- acheiving this, however the runtime semantics (particularly with regards +-- scheduling) should not differ from threadDelay in practise. sleep :: TimeInterval -> Process () sleep t = let ms = asTimeout t in do @@ -64,7 +78,7 @@ sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process Timer sendAfter t pid msg = runAfter t proc where proc = do { send pid msg } --- | runs the supplied process action(s) after `t' has elapsed +-- | runs the supplied process action(s) after @t@ has elapsed runAfter :: TimeInterval -> Process () -> Process TimerRef runAfter t p = spawnLocal $ runTimer t p True @@ -74,16 +88,16 @@ runAfter t p = spawnLocal $ runTimer t p True startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef startTimer t pid msg = periodically t (send pid msg) --- | runs the supplied process action(s) repeatedly at intervals of `t' +-- | runs the supplied process action(s) repeatedly at intervals of @t@ periodically :: TimeInterval -> Process () -> Process TimerRef periodically t p = spawnLocal $ runTimer t p False -- | resets a running timer. Note: Cancelling a timer does not guarantee that --- a timer's messages are prevented from being delivered to the target process. --- Also note that resetting an ongoing timer (started using the `startTimer' or --- `periodically' functions) will only cause the current elapsed period to time +-- all its messages are prevented from being delivered to the target process. +-- Also note that resetting an ongoing timer (started using the 'startTimer' or +-- 'periodically' functions) will only cause the current elapsed period to time -- out, after which the timer will continue running. To stop a long-running --- timer, you should use `cancelTimer' instead. +-- timer permanently, you should use 'cancelTimer' instead. resetTimer :: TimerRef -> Process () resetTimer = (flip send) Reset @@ -109,7 +123,7 @@ flushTimer ref ignore t = do , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef') (\_ -> return ()) ] --- | sets up a timer that sends `Tick' repeatedly at intervals of `t' +-- | sets up a timer that sends 'Tick' repeatedly at intervals of @t@ ticker :: TimeInterval -> ProcessId -> Process TimerRef ticker t pid = startTimer t pid Tick From 847deb403b02e11ecb97dd0ccfc417d5ae94b0b6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 03:15:35 +0000 Subject: [PATCH 0708/2357] cosmetic --- .../Distributed/Process/Platform/Timer.hs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index d119cfed..1ab63d38 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -72,9 +72,13 @@ sleep t = (\_ -> return ())] return () --- | starts a timer which sends the supplied message to the destination process --- after the specified time interval. -sendAfter :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +-- | starts a timer which sends the supplied message to the destination +-- process after the specified time interval. +sendAfter :: (Serializable a) + => TimeInterval + -> ProcessId + -> a + -> Process TimerRef sendAfter t pid msg = runAfter t proc where proc = do { send pid msg } @@ -84,8 +88,12 @@ runAfter t p = spawnLocal $ runTimer t p True -- | starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from --- being sent in future, cancelTimer can be called. -startTimer :: (Serializable a) => TimeInterval -> ProcessId -> a -> Process TimerRef +-- being sent in future, 'cancelTimer' can be called. +startTimer :: (Serializable a) + => TimeInterval + -> ProcessId + -> a + -> Process TimerRef startTimer t pid msg = periodically t (send pid msg) -- | runs the supplied process action(s) repeatedly at intervals of @t@ @@ -101,6 +109,7 @@ periodically t p = spawnLocal $ runTimer t p False resetTimer :: TimerRef -> Process () resetTimer = (flip send) Reset +-- | permanently cancels a timer cancelTimer :: TimerRef -> Process () cancelTimer = (flip send) Cancel @@ -111,7 +120,6 @@ flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Delay -> Process () flushTimer ref ignore t = do mRef <- monitor ref cancelTimer ref - -- TODO: monitor the timer ref (pid) and ensure it's gone before finishing performFlush mRef t return () where performFlush mRef Infinity = receiveWait $ filters mRef From 792208955d1b4044c4f338eec2ef92212311bd8a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 15 Jan 2013 03:15:35 +0000 Subject: [PATCH 0709/2357] cosmetic --- src/Control/Distributed/Process/Async.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 9bbf0d0e..a7eda821 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE StandaloneDeriving #-} + ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.Platform.Async From ea1cb1022b4bd26be495fcf3ee8f8398902ab752 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 00:51:27 +0000 Subject: [PATCH 0710/2357] set the reply-to properly for call messages --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c06861b9..8e4a1825 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -180,12 +180,12 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncChan b) callAsync sid msg = do - self <- getSelfPid -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 async $ asyncDo $ do mRef <- monitor sid - sendTo (SendToPid sid) (CallMessage msg (SendToPid self)) + wpid <- getSelfPid + sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) From 3f96cf7ca1ebcf61f110a44e0b6d4c13313f25fe Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 00:51:27 +0000 Subject: [PATCH 0711/2357] set the reply-to properly for call messages --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c06861b9..8e4a1825 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -180,12 +180,12 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncChan b) callAsync sid msg = do - self <- getSelfPid -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 async $ asyncDo $ do mRef <- monitor sid - sendTo (SendToPid sid) (CallMessage msg (SendToPid self)) + wpid <- getSelfPid + sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) From d197793897e357651c47836f44dd8d62cb76253b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:41:36 +0000 Subject: [PATCH 0712/2357] support for creating handlers that ignore/bypass server state --- .../Process/Platform/GenProcess.hs | 209 +++++++++++++++--- 1 file changed, 184 insertions(+), 25 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 8e4a1825..32dbd050 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -18,6 +18,7 @@ module Control.Distributed.Process.Platform.GenProcess , Behaviour(..) -- interaction with the process , start + , statelessProcess , call , safeCall , callAsync @@ -36,6 +37,18 @@ module Control.Distributed.Process.Platform.GenProcess , handleCast , handleCastIf , handleInfo + -- stateless handlers + , action + , handleCall_ + , handleCallIf_ + , handleCast_ + , handleCastIf_ + , continue_ + , timeoutAfter_ + , hibernate_ + , stop_ + -- lower level handlers + , handleDispatch ) where import Control.Concurrent (threadDelay) @@ -130,10 +143,10 @@ data UnhandledMessagePolicy = | Drop data Behaviour s = Behaviour { - dispatchers :: [Dispatcher s] - , infoHandlers :: [InfoDispatcher s] - , timeoutHandler :: TimeoutHandler s - , terminateHandler :: TerminateHandler s -- ^ termination handler + dispatchers :: [Dispatcher s] + , infoHandlers :: [InfoDispatcher s] + , timeoutHandler :: TimeoutHandler s + , terminateHandler :: TerminateHandler s -- ^ termination handler , unhandledMessagePolicy :: UnhandledMessagePolicy } @@ -143,12 +156,32 @@ data Behaviour s = Behaviour { -- TODO: automatic registration -start :: a -> InitHandler a s -> Behaviour s -> Process TerminateReason +-- | Starts a gen-process configured with the supplied process definition, +-- using an init handler and its initial arguments. This code will run the +-- 'Process' until completion and return @Right TerminateReason@ *or*, +-- if initialisation fails, return @Left InitResult@ which will be +-- @InitFail why@. +start :: a + -> InitHandler a s + -> Behaviour s + -> Process (Either (InitResult s) TerminateReason) start args init behave = do ir <- init args case ir of - InitOk initState initDelay -> initLoop behave initState initDelay - InitFail why -> return $ TerminateOther why + InitOk s d -> initLoop behave s d >>= return . Right + f@(InitFail _) -> return $ Left f + +-- | A basic, stateless process definition, where the unhandled message policy +-- is set to 'Terminate', the default timeout handlers does nothing (i.e., the +-- same as calling @continue ()@ and the terminate handler is a no-op. +statelessProcess :: Behaviour () +statelessProcess = Behaviour { + dispatchers = [] + , infoHandlers = [] + , timeoutHandler = \s _ -> continue s + , terminateHandler = \_ _ -> return () + , unhandledMessagePolicy = Terminate + } -- | Make a syncrhonous call call :: forall a b . (Serializable a, Serializable b) @@ -219,7 +252,10 @@ replyWith msg state = return $ ProcessReply msg state -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) -continue s = return $ ProcessContinue s +continue = return . ProcessContinue + +continue_ :: (s -> Process (ProcessAction s)) +continue_ = return . ProcessContinue -- | Instructs the process to wait for incoming messages until 'TimeInterval' -- is exceeded. If no messages are handled during this period, the /timeout/ @@ -228,6 +264,10 @@ continue s = return $ ProcessContinue s timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) timeoutAfter d s = return $ ProcessTimeout d s +-- | Version of 'timeoutAfter' that ignores the process state. +timeoutAfter_ :: TimeInterval -> (s -> Process (ProcessAction s)) +timeoutAfter_ d = return . ProcessTimeout d + -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to calling @threadDelay@. @@ -235,21 +275,68 @@ timeoutAfter d s = return $ ProcessTimeout d s hibernate :: TimeInterval -> s -> Process (ProcessAction s) hibernate d s = return $ ProcessHibernate d s +hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) +hibernate_ d = return . ProcessHibernate d + -- | Instructs the process to cease, giving the supplied reason for termination. stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r --- wrapping /normal/ functions with Dispatcher +stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) +stop_ r _ = stop r + +-- wrapping /normal/ functions with matching functionality + +-- | Constructs a 'call' handler from a function in the 'Process' monad. +-- +-- > handleCall_ = handleCallIf_ (const True) +-- +handleCall_ :: (Serializable a, Serializable b) + => (a -> Process b) + -> Dispatcher s +handleCall_ = handleCallIf_ (const True) +-- | Constructs a 'call' handler from an ordinary function in the 'Process' +-- monad. This variant ignores the state argument present in 'handleCall' and +-- 'handleCallIf' and is therefore useful in a stateless server. Messages are +-- only dispatched to the handler if the supplied condition evaluates to @True@ +-- +handleCallIf_ :: (Serializable a, Serializable b) + => (a -> Bool) + -> (a -> Process b) + -> Dispatcher s +handleCallIf_ cond handler = DispatchIf { + dispatch = doHandle handler + , dispatchIf = doCheckCall cond + } + where doHandle :: (Serializable a, Serializable b) + => (a -> Process b) + -> s + -> Message a + -> Process (ProcessAction s) + doHandle h s (CallMessage p c) = (h p) >>= mkReply c s + doHandle _ _ _ = error "illegal input" + -- TODO: standard 'this cannot happen' error message + + -- handling 'reply-to' in the main process loop is awkward at best, + -- so we handle it here instead and return the 'action' to the loop + mkReply :: (Serializable b) + => Recipient -> s -> b -> Process (ProcessAction s) + mkReply c s m = sendTo c (CallResponse m) >> continue s + +-- | Constructs a 'call' handler from a function in the 'Process' monad. +-- > handleCall = handleCallIf (const True) +-- handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall handler = handleCallIf (const True) handler +handleCall = handleCallIf (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. +-- in a 'Behaviour' specification for the /GenProcess/. Messages are only +-- dispatched to the handler if the supplied condition evaluates to @True@ -- handleCallIf :: (Serializable a, Serializable b) => (a -> Bool) @@ -257,7 +344,7 @@ handleCallIf :: (Serializable a, Serializable b) -> Dispatcher s handleCallIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheck cond + , dispatchIf = doCheckCall cond } where doHandle :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) @@ -265,14 +352,9 @@ handleCallIf cond handler = DispatchIf { -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = error "illegal input" + doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - doCheck :: forall s a. (Serializable a) - => (a -> Bool) -> s -> Message a -> Bool - doCheck c _ (CallMessage m _) = c m - doCheck _ _ _ = False - -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -281,27 +363,99 @@ handleCallIf cond handler = DispatchIf { mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a -- | Constructs a 'cast' handler from an ordinary function in the 'Process' --- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, --- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. +-- monad. +-- > handleCast = handleCastIf (const True) -- handleCast :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } +handleCast = handleCastIf (const True) --- | Constructs a 'handleCast' handler, matching on the supplied condition. +-- | Constructs a 'cast' handler from an ordinary function in the 'Process' +-- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, +-- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion +-- in a 'Behaviour' specification for the /GenProcess/. -- handleCastIf :: (Serializable a) => (a -> Bool) -> (s -> a -> Process (ProcessAction s)) -> Dispatcher s handleCastIf cond h = DispatchIf { - dispatch = (\s (CastMessage p) -> h s p) + dispatch = (\s (CastMessage p) -> h s p) + , dispatchIf = \_ (CastMessage msg) -> cond msg + } + +-- | Version of 'handleCast' that ignores the server state. +-- +handleCast_ :: (Serializable a) + => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s +handleCast_ = handleCastIf_ (const True) + +-- | Version of 'handleCastIf' that ignores the server state. +-- +handleCastIf_ :: (Serializable a) + => (a -> Bool) + -> (a -> (s -> Process (ProcessAction s))) + -> Dispatcher s +handleCastIf_ cond h = DispatchIf { + dispatch = (\s (CastMessage p) -> h p $ s) , dispatchIf = \_ (CastMessage msg) -> cond msg } +-- | Constructs an /action/ handler. Like 'handleDispatch' this can handle both +-- 'cast' and 'call' messages and you won't know which you're dealing with. +-- This can be useful where certain inputs require a definite action, such as +-- stopping the server, without concern for the state (e.g., when stopping we +-- need only decide to stop, as the terminate handler can deal with state +-- cleanup etc). For example: +-- +-- > action (\MyStopSignal -> stop_ TerminateNormal) +-- +action :: forall s a . (Serializable a) + => (a -> (s -> Process (ProcessAction s))) + -> Dispatcher s +action h = handleDispatch perform + where perform :: (s -> a -> Process (ProcessAction s)) + perform s a = let f = h a in f s + +-- | Constructs a handler for both /call/ and /cast/ messages. +-- > handleDispatch = handleDispatchIf (const True) +-- +handleDispatch :: (Serializable a) + => (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleDispatch = handleDispatchIf (const True) + +-- | Constructs a handler for both /call/ and /cast/ messages. Messages are only +-- dispatched to the handler if the supplied condition evaluates to @True@. +-- +handleDispatchIf :: (Serializable a) + => (a -> Bool) + -> (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleDispatchIf cond handler = DispatchIf { + dispatch = doHandle handler + , dispatchIf = doCheck cond + } + where doHandle :: (Serializable a) + => (s -> a -> Process (ProcessAction s)) + -> s + -> Message a + -> Process (ProcessAction s) + doHandle h s msg = + case msg of + (CallMessage p _) -> (h s p) + (CastMessage p) -> (h s p) + + doCheck :: forall s a. (Serializable a) + => (a -> Bool) -> s -> Message a -> Bool + doCheck c _ (CallMessage m _) = c m + doCheck c _ (CastMessage m) = c m + -- wrapping /normal/ functions with InfoDispatcher +-- | Creates a generic input handler (i.e., for recieved messages that are /not/ +-- sent using the 'cast' or 'call' APIs) from an ordinary function in the +-- 'Process' monad. handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) -> InfoDispatcher s @@ -314,6 +468,11 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) +doCheckCall :: forall s a. (Serializable a) + => (a -> Bool) -> s -> Message a -> Bool +doCheckCall c _ (CallMessage m _) = c m +doCheckCall _ _ _ = False + -- Process Implementation applyPolicy :: s @@ -359,7 +518,7 @@ initLoop b s w = m <- dh st msg case m of Nothing -> applyPolicy st pol msg - Just act -> return act + Just act -> return act loop :: [Match (ProcessAction s)] -> TimeoutHandler s From abec98ac9715768ec13bd1e972d8ce9f6c115b2d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:41:36 +0000 Subject: [PATCH 0713/2357] support for creating handlers that ignore/bypass server state --- .../Process/Platform/GenProcess.hs | 209 +++++++++++++++--- 1 file changed, 184 insertions(+), 25 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 8e4a1825..32dbd050 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -18,6 +18,7 @@ module Control.Distributed.Process.Platform.GenProcess , Behaviour(..) -- interaction with the process , start + , statelessProcess , call , safeCall , callAsync @@ -36,6 +37,18 @@ module Control.Distributed.Process.Platform.GenProcess , handleCast , handleCastIf , handleInfo + -- stateless handlers + , action + , handleCall_ + , handleCallIf_ + , handleCast_ + , handleCastIf_ + , continue_ + , timeoutAfter_ + , hibernate_ + , stop_ + -- lower level handlers + , handleDispatch ) where import Control.Concurrent (threadDelay) @@ -130,10 +143,10 @@ data UnhandledMessagePolicy = | Drop data Behaviour s = Behaviour { - dispatchers :: [Dispatcher s] - , infoHandlers :: [InfoDispatcher s] - , timeoutHandler :: TimeoutHandler s - , terminateHandler :: TerminateHandler s -- ^ termination handler + dispatchers :: [Dispatcher s] + , infoHandlers :: [InfoDispatcher s] + , timeoutHandler :: TimeoutHandler s + , terminateHandler :: TerminateHandler s -- ^ termination handler , unhandledMessagePolicy :: UnhandledMessagePolicy } @@ -143,12 +156,32 @@ data Behaviour s = Behaviour { -- TODO: automatic registration -start :: a -> InitHandler a s -> Behaviour s -> Process TerminateReason +-- | Starts a gen-process configured with the supplied process definition, +-- using an init handler and its initial arguments. This code will run the +-- 'Process' until completion and return @Right TerminateReason@ *or*, +-- if initialisation fails, return @Left InitResult@ which will be +-- @InitFail why@. +start :: a + -> InitHandler a s + -> Behaviour s + -> Process (Either (InitResult s) TerminateReason) start args init behave = do ir <- init args case ir of - InitOk initState initDelay -> initLoop behave initState initDelay - InitFail why -> return $ TerminateOther why + InitOk s d -> initLoop behave s d >>= return . Right + f@(InitFail _) -> return $ Left f + +-- | A basic, stateless process definition, where the unhandled message policy +-- is set to 'Terminate', the default timeout handlers does nothing (i.e., the +-- same as calling @continue ()@ and the terminate handler is a no-op. +statelessProcess :: Behaviour () +statelessProcess = Behaviour { + dispatchers = [] + , infoHandlers = [] + , timeoutHandler = \s _ -> continue s + , terminateHandler = \_ _ -> return () + , unhandledMessagePolicy = Terminate + } -- | Make a syncrhonous call call :: forall a b . (Serializable a, Serializable b) @@ -219,7 +252,10 @@ replyWith msg state = return $ ProcessReply msg state -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) -continue s = return $ ProcessContinue s +continue = return . ProcessContinue + +continue_ :: (s -> Process (ProcessAction s)) +continue_ = return . ProcessContinue -- | Instructs the process to wait for incoming messages until 'TimeInterval' -- is exceeded. If no messages are handled during this period, the /timeout/ @@ -228,6 +264,10 @@ continue s = return $ ProcessContinue s timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) timeoutAfter d s = return $ ProcessTimeout d s +-- | Version of 'timeoutAfter' that ignores the process state. +timeoutAfter_ :: TimeInterval -> (s -> Process (ProcessAction s)) +timeoutAfter_ d = return . ProcessTimeout d + -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to calling @threadDelay@. @@ -235,21 +275,68 @@ timeoutAfter d s = return $ ProcessTimeout d s hibernate :: TimeInterval -> s -> Process (ProcessAction s) hibernate d s = return $ ProcessHibernate d s +hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) +hibernate_ d = return . ProcessHibernate d + -- | Instructs the process to cease, giving the supplied reason for termination. stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r --- wrapping /normal/ functions with Dispatcher +stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) +stop_ r _ = stop r + +-- wrapping /normal/ functions with matching functionality + +-- | Constructs a 'call' handler from a function in the 'Process' monad. +-- +-- > handleCall_ = handleCallIf_ (const True) +-- +handleCall_ :: (Serializable a, Serializable b) + => (a -> Process b) + -> Dispatcher s +handleCall_ = handleCallIf_ (const True) +-- | Constructs a 'call' handler from an ordinary function in the 'Process' +-- monad. This variant ignores the state argument present in 'handleCall' and +-- 'handleCallIf' and is therefore useful in a stateless server. Messages are +-- only dispatched to the handler if the supplied condition evaluates to @True@ +-- +handleCallIf_ :: (Serializable a, Serializable b) + => (a -> Bool) + -> (a -> Process b) + -> Dispatcher s +handleCallIf_ cond handler = DispatchIf { + dispatch = doHandle handler + , dispatchIf = doCheckCall cond + } + where doHandle :: (Serializable a, Serializable b) + => (a -> Process b) + -> s + -> Message a + -> Process (ProcessAction s) + doHandle h s (CallMessage p c) = (h p) >>= mkReply c s + doHandle _ _ _ = error "illegal input" + -- TODO: standard 'this cannot happen' error message + + -- handling 'reply-to' in the main process loop is awkward at best, + -- so we handle it here instead and return the 'action' to the loop + mkReply :: (Serializable b) + => Recipient -> s -> b -> Process (ProcessAction s) + mkReply c s m = sendTo c (CallResponse m) >> continue s + +-- | Constructs a 'call' handler from a function in the 'Process' monad. +-- > handleCall = handleCallIf (const True) +-- handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall handler = handleCallIf (const True) handler +handleCall = handleCallIf (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. +-- in a 'Behaviour' specification for the /GenProcess/. Messages are only +-- dispatched to the handler if the supplied condition evaluates to @True@ -- handleCallIf :: (Serializable a, Serializable b) => (a -> Bool) @@ -257,7 +344,7 @@ handleCallIf :: (Serializable a, Serializable b) -> Dispatcher s handleCallIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheck cond + , dispatchIf = doCheckCall cond } where doHandle :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) @@ -265,14 +352,9 @@ handleCallIf cond handler = DispatchIf { -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = error "illegal input" + doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - doCheck :: forall s a. (Serializable a) - => (a -> Bool) -> s -> Message a -> Bool - doCheck c _ (CallMessage m _) = c m - doCheck _ _ _ = False - -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -281,27 +363,99 @@ handleCallIf cond handler = DispatchIf { mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a -- | Constructs a 'cast' handler from an ordinary function in the 'Process' --- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, --- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. +-- monad. +-- > handleCast = handleCastIf (const True) -- handleCast :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCast h = Dispatch { dispatch = (\s (CastMessage p) -> h s p) } +handleCast = handleCastIf (const True) --- | Constructs a 'handleCast' handler, matching on the supplied condition. +-- | Constructs a 'cast' handler from an ordinary function in the 'Process' +-- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, +-- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion +-- in a 'Behaviour' specification for the /GenProcess/. -- handleCastIf :: (Serializable a) => (a -> Bool) -> (s -> a -> Process (ProcessAction s)) -> Dispatcher s handleCastIf cond h = DispatchIf { - dispatch = (\s (CastMessage p) -> h s p) + dispatch = (\s (CastMessage p) -> h s p) + , dispatchIf = \_ (CastMessage msg) -> cond msg + } + +-- | Version of 'handleCast' that ignores the server state. +-- +handleCast_ :: (Serializable a) + => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s +handleCast_ = handleCastIf_ (const True) + +-- | Version of 'handleCastIf' that ignores the server state. +-- +handleCastIf_ :: (Serializable a) + => (a -> Bool) + -> (a -> (s -> Process (ProcessAction s))) + -> Dispatcher s +handleCastIf_ cond h = DispatchIf { + dispatch = (\s (CastMessage p) -> h p $ s) , dispatchIf = \_ (CastMessage msg) -> cond msg } +-- | Constructs an /action/ handler. Like 'handleDispatch' this can handle both +-- 'cast' and 'call' messages and you won't know which you're dealing with. +-- This can be useful where certain inputs require a definite action, such as +-- stopping the server, without concern for the state (e.g., when stopping we +-- need only decide to stop, as the terminate handler can deal with state +-- cleanup etc). For example: +-- +-- > action (\MyStopSignal -> stop_ TerminateNormal) +-- +action :: forall s a . (Serializable a) + => (a -> (s -> Process (ProcessAction s))) + -> Dispatcher s +action h = handleDispatch perform + where perform :: (s -> a -> Process (ProcessAction s)) + perform s a = let f = h a in f s + +-- | Constructs a handler for both /call/ and /cast/ messages. +-- > handleDispatch = handleDispatchIf (const True) +-- +handleDispatch :: (Serializable a) + => (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleDispatch = handleDispatchIf (const True) + +-- | Constructs a handler for both /call/ and /cast/ messages. Messages are only +-- dispatched to the handler if the supplied condition evaluates to @True@. +-- +handleDispatchIf :: (Serializable a) + => (a -> Bool) + -> (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleDispatchIf cond handler = DispatchIf { + dispatch = doHandle handler + , dispatchIf = doCheck cond + } + where doHandle :: (Serializable a) + => (s -> a -> Process (ProcessAction s)) + -> s + -> Message a + -> Process (ProcessAction s) + doHandle h s msg = + case msg of + (CallMessage p _) -> (h s p) + (CastMessage p) -> (h s p) + + doCheck :: forall s a. (Serializable a) + => (a -> Bool) -> s -> Message a -> Bool + doCheck c _ (CallMessage m _) = c m + doCheck c _ (CastMessage m) = c m + -- wrapping /normal/ functions with InfoDispatcher +-- | Creates a generic input handler (i.e., for recieved messages that are /not/ +-- sent using the 'cast' or 'call' APIs) from an ordinary function in the +-- 'Process' monad. handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) -> InfoDispatcher s @@ -314,6 +468,11 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) +doCheckCall :: forall s a. (Serializable a) + => (a -> Bool) -> s -> Message a -> Bool +doCheckCall c _ (CallMessage m _) = c m +doCheckCall _ _ _ = False + -- Process Implementation applyPolicy :: s @@ -359,7 +518,7 @@ initLoop b s w = m <- dh st msg case m of Nothing -> applyPolicy st pol msg - Just act -> return act + Just act -> return act loop :: [Match (ProcessAction s)] -> TimeoutHandler s From fbeb515a13472fc027924820520fc12aa6b7de4a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:42:23 +0000 Subject: [PATCH 0714/2357] test cases for gen-process (and later for gen-server too) --- distributed-process-platform.cabal | 39 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b607282f..64573b96 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -78,7 +78,8 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, TestUtils, Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + TestGenServer extensions: CPP main-is: TestTimer.hs @@ -110,11 +111,12 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test + Control.Distributed.Process.Platform.Test, + TestGenServer extensions: CPP main-is: TestPrimitives.hs -test-suite AsyncTests +test-suite Tests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -137,6 +139,35 @@ test-suite AsyncTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils + TestUtils, + TestGenServer extensions: CPP main-is: TestAsync.hs + +test-suite GenServerTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, + test-framework >= 0.6 && < 0.9, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + TestUtils, + MathsDemo + extensions: CPP + main-is: TestGenServer.hs From 1976bab40ed0227b9c8ad951d880d49e0b17f696 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:42:23 +0000 Subject: [PATCH 0715/2357] test cases for gen-process (and later for gen-server too) --- distributed-process-platform.cabal | 39 ++++++++-- tests/TestGenServer.hs | 111 +++++++++++++++++++++++++++++ tests/TestUtils.hs | 9 ++- 3 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 tests/TestGenServer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b607282f..64573b96 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -78,7 +78,8 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, TestUtils, Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + TestGenServer extensions: CPP main-is: TestTimer.hs @@ -110,11 +111,12 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test + Control.Distributed.Process.Platform.Test, + TestGenServer extensions: CPP main-is: TestPrimitives.hs -test-suite AsyncTests +test-suite Tests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -137,6 +139,35 @@ test-suite AsyncTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils + TestUtils, + TestGenServer extensions: CPP main-is: TestAsync.hs + +test-suite GenServerTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, + test-framework >= 0.6 && < 0.9, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + TestUtils, + MathsDemo + extensions: CPP + main-is: TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs new file mode 100644 index 00000000..50079340 --- /dev/null +++ b/tests/TestGenServer.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +-- NB: this module contains tests for the GenProcess /and/ GenServer API. + +module Main where + +-- import Control.Concurrent.MVar +import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform.Test +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time + +import Data.Binary() +import Data.Typeable() + +import MathsDemo + +import Test.Framework (Test, testGroup) +import Test.Framework.Providers.HUnit (testCase) +import TestUtils + +import qualified Network.Transport as NT + +type OpExpr = (Double -> Double -> Double) +data Op = Add OpExpr | Div OpExpr + +opAdd :: Op +opAdd = Add (+) + +opDiv :: Op +opDiv = Div (/) + +expr :: Op -> OpExpr +expr (Add f) = f +expr (Div f) = f + +mathTest :: String + -> String + -> LocalNode + -> ProcessId + -> Double + -> Double + -> Op + -> Test +mathTest t n l sid x y op = let fn = expr op in do + testCase t (delayedAssertion n l (x `fn` y) (proc sid x y op)) + where proc s x' y' (Add _) result = add s x' y' >>= stash result + proc s x' y' (Div _) result = divide s x' y' >>= stash result + +testBasicCall :: TestResult (Maybe String) -> Process () +testBasicCall result = do + pid <- server + callTimeout pid "foo" (within 5 Seconds) >>= stash result + +testBasicCall_ :: TestResult (Maybe Int) -> Process () +testBasicCall_ result = do + pid <- server + callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result + +server :: Process ProcessId +server = + let s = statelessProcess { + dispatchers = [ + handleCall (\s' (m :: String) -> reply m s') -- state passed + , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" + + , handleCast (\s' ("ping", -- regular cast + pid :: ProcessId) -> + send pid "pong" >> continue s') + + -- "stateless" + , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") + (\("timeout", Delay d) -> timeoutAfter_ d) + + , action (\"stop" -> stop_ TerminateNormal) + ] + , infoHandlers = [] + , unhandledMessagePolicy = Terminate + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + } + in spawnLocal $ start () startup s >> return () + where startup :: InitHandler () () + startup _ = return $ InitOk () Infinity + +tests :: NT.Transport -> IO [Test] +tests transport = do + localNode <- newLocalNode transport initRemoteTable + -- mv <- newEmptyMVar + -- _ <- forkProcess localNode $ launchMathServer >>= stash mv + -- sid <- takeMVar mv + return [ + testGroup "Handling async results" [ +-- mathTest "simple addition" +-- "10 + 10 = 20" +-- localNode sid 10 10 opAdd + testCase "basic call" + (delayedAssertion + "expected a response from the server" + localNode (Just "foo") testBasicCall) + , testCase "basic call_" + (delayedAssertion + "expected n * 2 back from the server" + localNode (Just 4) testBasicCall_) + + ] + ] + +main :: IO () +main = testMain $ tests diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 17a00bdb..aa448364 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -21,6 +21,7 @@ module TestUtils , putLogMsg , stopLogger -- runners + , mkNode , tryRunProcess , testMain ) where @@ -57,7 +58,13 @@ import Test.Framework (Test, defaultMain) import Network.Transport.TCP import qualified Network.Transport as NT - + +mkNode :: String -> IO LocalNode +mkNode port = do + Right (transport1, _) <- createTransportExposeInternals + "127.0.0.1" port defaultTCPParameters + newLocalNode transport1 initRemoteTable + -- | Run the supplied @testProc@ using an @MVar@ to collect and assert -- against its result. Uses the supplied @note@ if the assertion fails. delayedAssertion :: (Eq a) => String -> LocalNode -> a -> From b96f3408b2f0895183d11d705aa592a91376f8fc Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:42:23 +0000 Subject: [PATCH 0716/2357] test cases for gen-process (and later for gen-server too) --- distributed-process-platform.cabal | 39 ++++++++-- tests/TestGenServer.hs | 111 +++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/TestGenServer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b607282f..64573b96 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -78,7 +78,8 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, TestUtils, Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + TestGenServer extensions: CPP main-is: TestTimer.hs @@ -110,11 +111,12 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test + Control.Distributed.Process.Platform.Test, + TestGenServer extensions: CPP main-is: TestPrimitives.hs -test-suite AsyncTests +test-suite Tests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -137,6 +139,35 @@ test-suite AsyncTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils + TestUtils, + TestGenServer extensions: CPP main-is: TestAsync.hs + +test-suite GenServerTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, + test-framework >= 0.6 && < 0.9, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + TestUtils, + MathsDemo + extensions: CPP + main-is: TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs new file mode 100644 index 00000000..50079340 --- /dev/null +++ b/tests/TestGenServer.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +-- NB: this module contains tests for the GenProcess /and/ GenServer API. + +module Main where + +-- import Control.Concurrent.MVar +import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform.Test +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time + +import Data.Binary() +import Data.Typeable() + +import MathsDemo + +import Test.Framework (Test, testGroup) +import Test.Framework.Providers.HUnit (testCase) +import TestUtils + +import qualified Network.Transport as NT + +type OpExpr = (Double -> Double -> Double) +data Op = Add OpExpr | Div OpExpr + +opAdd :: Op +opAdd = Add (+) + +opDiv :: Op +opDiv = Div (/) + +expr :: Op -> OpExpr +expr (Add f) = f +expr (Div f) = f + +mathTest :: String + -> String + -> LocalNode + -> ProcessId + -> Double + -> Double + -> Op + -> Test +mathTest t n l sid x y op = let fn = expr op in do + testCase t (delayedAssertion n l (x `fn` y) (proc sid x y op)) + where proc s x' y' (Add _) result = add s x' y' >>= stash result + proc s x' y' (Div _) result = divide s x' y' >>= stash result + +testBasicCall :: TestResult (Maybe String) -> Process () +testBasicCall result = do + pid <- server + callTimeout pid "foo" (within 5 Seconds) >>= stash result + +testBasicCall_ :: TestResult (Maybe Int) -> Process () +testBasicCall_ result = do + pid <- server + callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result + +server :: Process ProcessId +server = + let s = statelessProcess { + dispatchers = [ + handleCall (\s' (m :: String) -> reply m s') -- state passed + , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" + + , handleCast (\s' ("ping", -- regular cast + pid :: ProcessId) -> + send pid "pong" >> continue s') + + -- "stateless" + , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") + (\("timeout", Delay d) -> timeoutAfter_ d) + + , action (\"stop" -> stop_ TerminateNormal) + ] + , infoHandlers = [] + , unhandledMessagePolicy = Terminate + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + } + in spawnLocal $ start () startup s >> return () + where startup :: InitHandler () () + startup _ = return $ InitOk () Infinity + +tests :: NT.Transport -> IO [Test] +tests transport = do + localNode <- newLocalNode transport initRemoteTable + -- mv <- newEmptyMVar + -- _ <- forkProcess localNode $ launchMathServer >>= stash mv + -- sid <- takeMVar mv + return [ + testGroup "Handling async results" [ +-- mathTest "simple addition" +-- "10 + 10 = 20" +-- localNode sid 10 10 opAdd + testCase "basic call" + (delayedAssertion + "expected a response from the server" + localNode (Just "foo") testBasicCall) + , testCase "basic call_" + (delayedAssertion + "expected n * 2 back from the server" + localNode (Just 4) testBasicCall_) + + ] + ] + +main :: IO () +main = testMain $ tests From 3f32548325666506ac07a7923bd41537e3415982 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:42:23 +0000 Subject: [PATCH 0717/2357] test cases for gen-process (and later for gen-server too) --- distributed-process-platform.cabal | 39 ++++++++-- tests/MathsDemo.hs | 55 ++++++++++++++ tests/TestGenServer.hs | 111 +++++++++++++++++++++++++++++ tests/TestUtils.hs | 9 ++- 4 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 tests/MathsDemo.hs create mode 100644 tests/TestGenServer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index b607282f..64573b96 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -78,7 +78,8 @@ test-suite TimerTests Control.Distributed.Process.Platform.Timer, TestUtils, Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + TestGenServer extensions: CPP main-is: TestTimer.hs @@ -110,11 +111,12 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test + Control.Distributed.Process.Platform.Test, + TestGenServer extensions: CPP main-is: TestPrimitives.hs -test-suite AsyncTests +test-suite Tests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -137,6 +139,35 @@ test-suite AsyncTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils + TestUtils, + TestGenServer extensions: CPP main-is: TestAsync.hs + +test-suite GenServerTests + type: exitcode-stdio-1.0 + x-uses-tf: true + build-depends: + base >= 4.4 && < 5, + ansi-terminal >= 0.5 && < 0.6, + distributed-process, + derive, + network-transport >= 0.3 && < 0.4, + mtl, + network-transport-tcp >= 0.3 && < 0.4, + binary >= 0.5 && < 0.7, + network >= 2.3 && < 2.5, + HUnit >= 1.2 && < 2, + stm >= 2.3 && < 2.5, + test-framework >= 0.6 && < 0.9, + test-framework-hunit, + transformers + hs-source-dirs: + src, + tests + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind + other-modules: + TestUtils, + MathsDemo + extensions: CPP + main-is: TestGenServer.hs diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs new file mode 100644 index 00000000..f196b57e --- /dev/null +++ b/tests/MathsDemo.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE DeriveDataTypeable #-} + +module MathsDemo + ( add + , divide + , launchMathServer + ) where + +import Control.Applicative +import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time + +import Data.Binary (Binary(..)) +import Data.Typeable (Typeable) + +data Add = Add Double Double deriving (Typeable) +data Divide = Divide Double Double deriving (Typeable) +data DivByZero = DivByZero deriving (Typeable) + +instance Binary Add where + put (Add x y) = put x >> put y + get = Add <$> get <*> get + +instance Binary Divide where + put (Divide x y) = put x >> put y + get = Divide <$> get <*> get + +instance Binary DivByZero where + put DivByZero = return () + get = return DivByZero + +-- public API + +add :: ProcessId -> Double -> Double -> Process Double +add sid x y = call sid (Add x y) + +divide :: ProcessId -> Double -> Double -> Process Double +divide sid x y = call sid (Divide x y ) + +launchMathServer :: Process ProcessId +launchMathServer = + let server = statelessProcess { + dispatchers = [ + handleCall_ (\(Add x y) -> return (x + y)) + , handleCallIf_ (\(Divide _ y) -> y /= 0) + (\(Divide x y) -> return (x / y)) + , handleCall_ (\(Divide _ _) -> return DivByZero) + + , action (\"stop" -> stop_ TerminateNormal) + ] + } :: Behaviour () + in spawnLocal $ start () startup server >> return () + where startup :: InitHandler () () + startup _ = return $ InitOk () Infinity diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs new file mode 100644 index 00000000..50079340 --- /dev/null +++ b/tests/TestGenServer.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +-- NB: this module contains tests for the GenProcess /and/ GenServer API. + +module Main where + +-- import Control.Concurrent.MVar +import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Node +import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform.Test +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time + +import Data.Binary() +import Data.Typeable() + +import MathsDemo + +import Test.Framework (Test, testGroup) +import Test.Framework.Providers.HUnit (testCase) +import TestUtils + +import qualified Network.Transport as NT + +type OpExpr = (Double -> Double -> Double) +data Op = Add OpExpr | Div OpExpr + +opAdd :: Op +opAdd = Add (+) + +opDiv :: Op +opDiv = Div (/) + +expr :: Op -> OpExpr +expr (Add f) = f +expr (Div f) = f + +mathTest :: String + -> String + -> LocalNode + -> ProcessId + -> Double + -> Double + -> Op + -> Test +mathTest t n l sid x y op = let fn = expr op in do + testCase t (delayedAssertion n l (x `fn` y) (proc sid x y op)) + where proc s x' y' (Add _) result = add s x' y' >>= stash result + proc s x' y' (Div _) result = divide s x' y' >>= stash result + +testBasicCall :: TestResult (Maybe String) -> Process () +testBasicCall result = do + pid <- server + callTimeout pid "foo" (within 5 Seconds) >>= stash result + +testBasicCall_ :: TestResult (Maybe Int) -> Process () +testBasicCall_ result = do + pid <- server + callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result + +server :: Process ProcessId +server = + let s = statelessProcess { + dispatchers = [ + handleCall (\s' (m :: String) -> reply m s') -- state passed + , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" + + , handleCast (\s' ("ping", -- regular cast + pid :: ProcessId) -> + send pid "pong" >> continue s') + + -- "stateless" + , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") + (\("timeout", Delay d) -> timeoutAfter_ d) + + , action (\"stop" -> stop_ TerminateNormal) + ] + , infoHandlers = [] + , unhandledMessagePolicy = Terminate + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + } + in spawnLocal $ start () startup s >> return () + where startup :: InitHandler () () + startup _ = return $ InitOk () Infinity + +tests :: NT.Transport -> IO [Test] +tests transport = do + localNode <- newLocalNode transport initRemoteTable + -- mv <- newEmptyMVar + -- _ <- forkProcess localNode $ launchMathServer >>= stash mv + -- sid <- takeMVar mv + return [ + testGroup "Handling async results" [ +-- mathTest "simple addition" +-- "10 + 10 = 20" +-- localNode sid 10 10 opAdd + testCase "basic call" + (delayedAssertion + "expected a response from the server" + localNode (Just "foo") testBasicCall) + , testCase "basic call_" + (delayedAssertion + "expected n * 2 back from the server" + localNode (Just 4) testBasicCall_) + + ] + ] + +main :: IO () +main = testMain $ tests diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 17a00bdb..aa448364 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -21,6 +21,7 @@ module TestUtils , putLogMsg , stopLogger -- runners + , mkNode , tryRunProcess , testMain ) where @@ -57,7 +58,13 @@ import Test.Framework (Test, defaultMain) import Network.Transport.TCP import qualified Network.Transport as NT - + +mkNode :: String -> IO LocalNode +mkNode port = do + Right (transport1, _) <- createTransportExposeInternals + "127.0.0.1" port defaultTCPParameters + newLocalNode transport1 initRemoteTable + -- | Run the supplied @testProc@ using an @MVar@ to collect and assert -- against its result. Uses the supplied @note@ if the assertion fails. delayedAssertion :: (Eq a) => String -> LocalNode -> a -> From 0e9909f8992677fd84ff0feca77169b1941c6fa1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:55:42 +0000 Subject: [PATCH 0718/2357] strip trailing whitespace - stick to emacs next time --- .../Process/Platform/GenProcess.hs | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 32dbd050..7360bf3a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -3,7 +3,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Process.Platform.GenProcess +module Control.Distributed.Process.Platform.GenProcess ( -- exported data types ServerId(..) , Recipient(..) @@ -68,7 +68,7 @@ data ServerId = ServerId ProcessId | ServerName String data Recipient = SendToPid ProcessId | SendToService String - | SendToRemoteService String NodeId + | SendToRemoteService String NodeId deriving (Typeable) $(derive makeBinary ''Recipient) @@ -77,11 +77,11 @@ data Message a = | CallMessage a Recipient deriving (Typeable) $(derive makeBinary ''Message) - + data CallResponse a = CallResponse a deriving (Typeable) $(derive makeBinary ''CallResponse) - + -- | Terminate reason data TerminateReason = TerminateNormal @@ -99,11 +99,11 @@ data ProcessAction s = ProcessContinue s | ProcessTimeout TimeInterval s | ProcessHibernate TimeInterval s - | ProcessStop TerminateReason + | ProcessStop TerminateReason data ProcessReply s a = ProcessReply a (ProcessAction s) - | NoReply (ProcessAction s) + | NoReply (ProcessAction s) type InitHandler a s = a -> Process (InitResult s) type TerminateHandler s = s -> TerminateReason -> Process () @@ -167,7 +167,7 @@ start :: a -> Process (Either (InitResult s) TerminateReason) start args init behave = do ir <- init args - case ir of + case ir of InitOk s d -> initLoop behave s d >>= return . Right f@(InitFail _) -> return $ Left f @@ -178,12 +178,12 @@ statelessProcess :: Behaviour () statelessProcess = Behaviour { dispatchers = [] , infoHandlers = [] - , timeoutHandler = \s _ -> continue s + , timeoutHandler = \s _ -> continue s , terminateHandler = \_ _ -> return () , unhandledMessagePolicy = Terminate } --- | Make a syncrhonous call +-- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = do @@ -192,7 +192,9 @@ call sid msg = do Nothing -> fail "call failed" -- TODO: exit protocol !? Just ar -> return ar --- | Safe version of 'call' that returns 'Nothing' if the operation fails. +-- | Safe version of 'call' that returns 'Nothing' if the operation fails. If +-- you need information about *why* a call has failed then you should use +-- 'call' instead. safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Maybe b) safeCall s m = callAsync s m >>= wait >>= unpack @@ -202,12 +204,12 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- TODO: provide version of call that will throw/exit on failure callTimeout :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> TimeInterval -> Process (Maybe b) + => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) unpack Nothing = return Nothing unpack (Just (AsyncDone r)) = return $ Just r - unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate + unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate -- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 callAsync :: forall a b . (Serializable a, Serializable b) @@ -227,7 +229,7 @@ callAsync sid msg = do -- TODO: better failure API case r of Right m -> return m - Left err -> fail $ "call: remote process died: " ++ show err + Left err -> fail $ "call: remote process died: " ++ show err -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. @@ -237,18 +239,18 @@ cast sid msg = send sid (CastMessage msg) -- Constructing Handlers from *ordinary* functions --- | Instructs the process to send a reply and continue working. +-- | Instructs the process to send a reply and continue working. -- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r -- | Instructs the process to send a reply and evaluate the 'ProcessAction' --- thereafter. +-- thereafter. replyWith :: (Serializable m) => m -> ProcessAction s -> Process (ProcessReply s m) -replyWith msg state = return $ ProcessReply msg state +replyWith msg state = return $ ProcessReply msg state -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) @@ -260,7 +262,7 @@ continue_ = return . ProcessContinue -- | Instructs the process to wait for incoming messages until 'TimeInterval' -- is exceeded. If no messages are handled during this period, the /timeout/ -- handler will be called. Note that this alters the process timeout permanently --- such that the given @TimeInterval@ will remain in use until changed. +-- such that the given @TimeInterval@ will remain in use until changed. timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) timeoutAfter d s = return $ ProcessTimeout d s @@ -271,7 +273,7 @@ timeoutAfter_ d = return . ProcessTimeout d -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to calling @threadDelay@. --- +-- hibernate :: TimeInterval -> s -> Process (ProcessAction s) hibernate d s = return $ ProcessHibernate d s @@ -289,7 +291,7 @@ stop_ r _ = stop r -- | Constructs a 'call' handler from a function in the 'Process' monad. -- --- > handleCall_ = handleCallIf_ (const True) +-- > handleCall_ = handleCallIf_ (const True) -- handleCall_ :: (Serializable a, Serializable b) => (a -> Process b) @@ -315,9 +317,9 @@ handleCallIf_ cond handler = DispatchIf { -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ _ = error "illegal input" + doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - + -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -330,7 +332,7 @@ handleCallIf_ cond handler = DispatchIf { handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall = handleCallIf (const True) +handleCall = handleCallIf (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, @@ -354,7 +356,7 @@ handleCallIf cond handler = DispatchIf { doHandle h s (CallMessage p c) = (h s p) >>= mkReply c doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - + -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -364,7 +366,7 @@ handleCallIf cond handler = DispatchIf { -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. --- > handleCast = handleCastIf (const True) +-- > handleCast = handleCastIf (const True) -- handleCast :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s @@ -445,7 +447,7 @@ handleDispatchIf cond handler = DispatchIf { case msg of (CallMessage p _) -> (h s p) (CastMessage p) -> (h s p) - + doCheck :: forall s a. (Serializable a) => (a -> Bool) -> s -> Message a -> Bool doCheck c _ (CallMessage m _) = c m @@ -460,7 +462,7 @@ handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) -> InfoDispatcher s handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } - where + where doHandleInfo :: forall s2 a2. (Serializable a2) => (s2 -> a2 -> Process (ProcessAction s2)) -> s2 @@ -488,7 +490,7 @@ applyPolicy s p m = initLoop :: Behaviour s -> s -> Delay -> Process TerminateReason initLoop b s w = let p = unhandledMessagePolicy b - t = timeoutHandler b + t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) ms' = ms ++ addInfoAux p s (infoHandlers b) in loop ms' t s w @@ -497,8 +499,8 @@ initLoop b s w = -> s -> [InfoDispatcher s] -> [Match (ProcessAction s)] - addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] - + addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] + infoHandler :: UnhandledMessagePolicy -> s -> [InfoDispatcher s] @@ -506,7 +508,7 @@ initLoop b s w = -> Process (ProcessAction s) infoHandler pol st [] msg = applyPolicy st pol msg infoHandler pol st (d:ds :: [InfoDispatcher s]) msg - | length ds > 0 = let dh = dispatchInfo d in do + | length ds > 0 = let dh = dispatchInfo d in do -- NB: we *do not* want to terminate/dead-letter messages until -- we've exhausted all the possible info handlers m <- dh st msg @@ -519,7 +521,7 @@ initLoop b s w = case m of Nothing -> applyPolicy st pol msg Just act -> return act - + loop :: [Match (ProcessAction s)] -> TimeoutHandler s -> s @@ -552,7 +554,7 @@ processReceive ms h s t = do recv matches d = case d of Infinity -> receiveWait matches >>= return . Just - Delay t' -> receiveTimeout (asTimeout t') matches + Delay t' -> receiveTimeout (asTimeout t') matches -- internal/utility From d6a1d07b0586810bbbf004a21d7e75315c523d98 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:55:42 +0000 Subject: [PATCH 0719/2357] strip trailing whitespace - stick to emacs next time --- .../Process/Platform/GenProcess.hs | 66 ++++++++++--------- tests/TestGenServer.hs | 16 ++--- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 32dbd050..7360bf3a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -3,7 +3,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -module Control.Distributed.Process.Platform.GenProcess +module Control.Distributed.Process.Platform.GenProcess ( -- exported data types ServerId(..) , Recipient(..) @@ -68,7 +68,7 @@ data ServerId = ServerId ProcessId | ServerName String data Recipient = SendToPid ProcessId | SendToService String - | SendToRemoteService String NodeId + | SendToRemoteService String NodeId deriving (Typeable) $(derive makeBinary ''Recipient) @@ -77,11 +77,11 @@ data Message a = | CallMessage a Recipient deriving (Typeable) $(derive makeBinary ''Message) - + data CallResponse a = CallResponse a deriving (Typeable) $(derive makeBinary ''CallResponse) - + -- | Terminate reason data TerminateReason = TerminateNormal @@ -99,11 +99,11 @@ data ProcessAction s = ProcessContinue s | ProcessTimeout TimeInterval s | ProcessHibernate TimeInterval s - | ProcessStop TerminateReason + | ProcessStop TerminateReason data ProcessReply s a = ProcessReply a (ProcessAction s) - | NoReply (ProcessAction s) + | NoReply (ProcessAction s) type InitHandler a s = a -> Process (InitResult s) type TerminateHandler s = s -> TerminateReason -> Process () @@ -167,7 +167,7 @@ start :: a -> Process (Either (InitResult s) TerminateReason) start args init behave = do ir <- init args - case ir of + case ir of InitOk s d -> initLoop behave s d >>= return . Right f@(InitFail _) -> return $ Left f @@ -178,12 +178,12 @@ statelessProcess :: Behaviour () statelessProcess = Behaviour { dispatchers = [] , infoHandlers = [] - , timeoutHandler = \s _ -> continue s + , timeoutHandler = \s _ -> continue s , terminateHandler = \_ _ -> return () , unhandledMessagePolicy = Terminate } --- | Make a syncrhonous call +-- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = do @@ -192,7 +192,9 @@ call sid msg = do Nothing -> fail "call failed" -- TODO: exit protocol !? Just ar -> return ar --- | Safe version of 'call' that returns 'Nothing' if the operation fails. +-- | Safe version of 'call' that returns 'Nothing' if the operation fails. If +-- you need information about *why* a call has failed then you should use +-- 'call' instead. safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Maybe b) safeCall s m = callAsync s m >>= wait >>= unpack @@ -202,12 +204,12 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- TODO: provide version of call that will throw/exit on failure callTimeout :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> TimeInterval -> Process (Maybe b) + => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) unpack Nothing = return Nothing unpack (Just (AsyncDone r)) = return $ Just r - unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate + unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate -- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 callAsync :: forall a b . (Serializable a, Serializable b) @@ -227,7 +229,7 @@ callAsync sid msg = do -- TODO: better failure API case r of Right m -> return m - Left err -> fail $ "call: remote process died: " ++ show err + Left err -> fail $ "call: remote process died: " ++ show err -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. @@ -237,18 +239,18 @@ cast sid msg = send sid (CastMessage msg) -- Constructing Handlers from *ordinary* functions --- | Instructs the process to send a reply and continue working. +-- | Instructs the process to send a reply and continue working. -- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r -- | Instructs the process to send a reply and evaluate the 'ProcessAction' --- thereafter. +-- thereafter. replyWith :: (Serializable m) => m -> ProcessAction s -> Process (ProcessReply s m) -replyWith msg state = return $ ProcessReply msg state +replyWith msg state = return $ ProcessReply msg state -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) @@ -260,7 +262,7 @@ continue_ = return . ProcessContinue -- | Instructs the process to wait for incoming messages until 'TimeInterval' -- is exceeded. If no messages are handled during this period, the /timeout/ -- handler will be called. Note that this alters the process timeout permanently --- such that the given @TimeInterval@ will remain in use until changed. +-- such that the given @TimeInterval@ will remain in use until changed. timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) timeoutAfter d s = return $ ProcessTimeout d s @@ -271,7 +273,7 @@ timeoutAfter_ d = return . ProcessTimeout d -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to calling @threadDelay@. --- +-- hibernate :: TimeInterval -> s -> Process (ProcessAction s) hibernate d s = return $ ProcessHibernate d s @@ -289,7 +291,7 @@ stop_ r _ = stop r -- | Constructs a 'call' handler from a function in the 'Process' monad. -- --- > handleCall_ = handleCallIf_ (const True) +-- > handleCall_ = handleCallIf_ (const True) -- handleCall_ :: (Serializable a, Serializable b) => (a -> Process b) @@ -315,9 +317,9 @@ handleCallIf_ cond handler = DispatchIf { -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ _ = error "illegal input" + doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - + -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -330,7 +332,7 @@ handleCallIf_ cond handler = DispatchIf { handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall = handleCallIf (const True) +handleCall = handleCallIf (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, @@ -354,7 +356,7 @@ handleCallIf cond handler = DispatchIf { doHandle h s (CallMessage p c) = (h s p) >>= mkReply c doHandle _ _ _ = error "illegal input" -- TODO: standard 'this cannot happen' error message - + -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -364,7 +366,7 @@ handleCallIf cond handler = DispatchIf { -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. --- > handleCast = handleCastIf (const True) +-- > handleCast = handleCastIf (const True) -- handleCast :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s @@ -445,7 +447,7 @@ handleDispatchIf cond handler = DispatchIf { case msg of (CallMessage p _) -> (h s p) (CastMessage p) -> (h s p) - + doCheck :: forall s a. (Serializable a) => (a -> Bool) -> s -> Message a -> Bool doCheck c _ (CallMessage m _) = c m @@ -460,7 +462,7 @@ handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) -> InfoDispatcher s handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } - where + where doHandleInfo :: forall s2 a2. (Serializable a2) => (s2 -> a2 -> Process (ProcessAction s2)) -> s2 @@ -488,7 +490,7 @@ applyPolicy s p m = initLoop :: Behaviour s -> s -> Delay -> Process TerminateReason initLoop b s w = let p = unhandledMessagePolicy b - t = timeoutHandler b + t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) ms' = ms ++ addInfoAux p s (infoHandlers b) in loop ms' t s w @@ -497,8 +499,8 @@ initLoop b s w = -> s -> [InfoDispatcher s] -> [Match (ProcessAction s)] - addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] - + addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] + infoHandler :: UnhandledMessagePolicy -> s -> [InfoDispatcher s] @@ -506,7 +508,7 @@ initLoop b s w = -> Process (ProcessAction s) infoHandler pol st [] msg = applyPolicy st pol msg infoHandler pol st (d:ds :: [InfoDispatcher s]) msg - | length ds > 0 = let dh = dispatchInfo d in do + | length ds > 0 = let dh = dispatchInfo d in do -- NB: we *do not* want to terminate/dead-letter messages until -- we've exhausted all the possible info handlers m <- dh st msg @@ -519,7 +521,7 @@ initLoop b s w = case m of Nothing -> applyPolicy st pol msg Just act -> return act - + loop :: [Match (ProcessAction s)] -> TimeoutHandler s -> s @@ -552,7 +554,7 @@ processReceive ms h s t = do recv matches d = case d of Infinity -> receiveWait matches >>= return . Just - Delay t' -> receiveTimeout (asTimeout t') matches + Delay t' -> receiveTimeout (asTimeout t') matches -- internal/utility diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 50079340..24560034 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -34,7 +34,7 @@ opDiv = Div (/) expr :: Op -> OpExpr expr (Add f) = f -expr (Div f) = f +expr (Div f) = f mathTest :: String -> String @@ -65,20 +65,20 @@ server = dispatchers = [ handleCall (\s' (m :: String) -> reply m s') -- state passed , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - + , handleCast (\s' ("ping", -- regular cast pid :: ProcessId) -> send pid "pong" >> continue s') - + -- "stateless" , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) - - , action (\"stop" -> stop_ TerminateNormal) + + , action (\"stop" -> stop_ TerminateNormal) ] , infoHandlers = [] , unhandledMessagePolicy = Terminate - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } in spawnLocal $ start () startup s >> return () where startup :: InitHandler () () @@ -103,9 +103,9 @@ tests transport = do (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - + ] ] main :: IO () -main = testMain $ tests +main = testMain $ tests From 3efff8d0ee65f63461d8498b57b1bcd791ef61a9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:55:42 +0000 Subject: [PATCH 0720/2357] strip trailing whitespace - stick to emacs next time --- tests/TestGenServer.hs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 50079340..24560034 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -34,7 +34,7 @@ opDiv = Div (/) expr :: Op -> OpExpr expr (Add f) = f -expr (Div f) = f +expr (Div f) = f mathTest :: String -> String @@ -65,20 +65,20 @@ server = dispatchers = [ handleCall (\s' (m :: String) -> reply m s') -- state passed , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - + , handleCast (\s' ("ping", -- regular cast pid :: ProcessId) -> send pid "pong" >> continue s') - + -- "stateless" , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) - - , action (\"stop" -> stop_ TerminateNormal) + + , action (\"stop" -> stop_ TerminateNormal) ] , infoHandlers = [] , unhandledMessagePolicy = Terminate - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } in spawnLocal $ start () startup s >> return () where startup :: InitHandler () () @@ -103,9 +103,9 @@ tests transport = do (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - + ] ] main :: IO () -main = testMain $ tests +main = testMain $ tests From ee143b12af4c08ef6958e6b1eeff791952bf32b8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:55:42 +0000 Subject: [PATCH 0721/2357] strip trailing whitespace - stick to emacs next time --- tests/MathsDemo.hs | 8 ++++---- tests/TestGenServer.hs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index f196b57e..46319144 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -1,6 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} -module MathsDemo +module MathsDemo ( add , divide , launchMathServer @@ -33,20 +33,20 @@ instance Binary DivByZero where -- public API add :: ProcessId -> Double -> Double -> Process Double -add sid x y = call sid (Add x y) +add sid x y = call sid (Add x y) divide :: ProcessId -> Double -> Double -> Process Double divide sid x y = call sid (Divide x y ) launchMathServer :: Process ProcessId -launchMathServer = +launchMathServer = let server = statelessProcess { dispatchers = [ handleCall_ (\(Add x y) -> return (x + y)) , handleCallIf_ (\(Divide _ y) -> y /= 0) (\(Divide x y) -> return (x / y)) , handleCall_ (\(Divide _ _) -> return DivByZero) - + , action (\"stop" -> stop_ TerminateNormal) ] } :: Behaviour () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 50079340..24560034 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -34,7 +34,7 @@ opDiv = Div (/) expr :: Op -> OpExpr expr (Add f) = f -expr (Div f) = f +expr (Div f) = f mathTest :: String -> String @@ -65,20 +65,20 @@ server = dispatchers = [ handleCall (\s' (m :: String) -> reply m s') -- state passed , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - + , handleCast (\s' ("ping", -- regular cast pid :: ProcessId) -> send pid "pong" >> continue s') - + -- "stateless" , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) - - , action (\"stop" -> stop_ TerminateNormal) + + , action (\"stop" -> stop_ TerminateNormal) ] , infoHandlers = [] , unhandledMessagePolicy = Terminate - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } in spawnLocal $ start () startup s >> return () where startup :: InitHandler () () @@ -103,9 +103,9 @@ tests transport = do (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - + ] ] main :: IO () -main = testMain $ tests +main = testMain $ tests From d03f658c2e2f6fc76d35dfd9e3ec508c0b95c742 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:12 +0000 Subject: [PATCH 0722/2357] strip trailing whitespace - stick to emacs next time --- src/Control/Distributed/Process/Platform/Test.hs | 6 +++--- src/Control/Distributed/Process/Platform/Time.hs | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Test.hs b/src/Control/Distributed/Process/Platform/Test.hs index ec8f7c95..c6261fc2 100644 --- a/src/Control/Distributed/Process/Platform/Test.hs +++ b/src/Control/Distributed/Process/Platform/Test.hs @@ -4,7 +4,7 @@ ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.Platform.Test --- Copyright : (c) Tim Watson, Jeff Epstein 2013 +-- Copyright : (c) Tim Watson, Jeff Epstein 2013 -- License : BSD3 (see the file LICENSE) -- -- Maintainer : Tim Watson @@ -45,7 +45,7 @@ import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Exception (SomeException) -import Control.Monad (forever) +import Control.Monad (forever) import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) @@ -82,7 +82,7 @@ runTestProcess proc = forever $ do Go -> proc Report p -> receiveWait [matchAny (\m -> forward m p)] >> return () --- | Tell a /test process/ to continue executing +-- | Tell a /test process/ to continue executing testProcessGo :: ProcessId -> Process () testProcessGo pid = (say $ (show pid) ++ " go!") >> send pid Go diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index d43eb9d8..5e5a377d 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -21,7 +21,7 @@ -- corrolary to 'timeout' that works with these. ----------------------------------------------------------------------------- -module Control.Distributed.Process.Platform.Time +module Control.Distributed.Process.Platform.Time ( -- time interval handling microSeconds , milliSeconds @@ -35,7 +35,7 @@ module Control.Distributed.Process.Platform.Time , TimeInterval , TimeUnit(..) , Delay(..) - + -- timeouts , Timeout , TimeoutNotification(..) @@ -88,8 +88,8 @@ asTimeout :: TimeInterval -> Int asTimeout (TimeInterval u v) = timeToMs u v -- | Convenience for making timeouts; e.g., --- --- > receiveTimeout (after 3 Seconds) [ match (\"ok" -> return ()) ] +-- +-- > receiveTimeout (after 3 Seconds) [ match (\"ok" -> return ()) ] -- after :: Int -> TimeUnit -> Int after n m = timeToMs m n @@ -161,7 +161,6 @@ noWait = Just 0 -- | Sends the calling process @TimeoutNotification tag@ after @time@ microseconds timeout :: Int -> Tag -> ProcessId -> Process () timeout time tag p = - void $ spawnLocal $ + void $ spawnLocal $ do liftIO $ threadDelay time send p (TimeoutNotification tag) - \ No newline at end of file From ad35acfbc0132df9cadf54ef73829a176f91182a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:12 +0000 Subject: [PATCH 0723/2357] strip trailing whitespace - stick to emacs next time --- tests/TestUtils.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 17a00bdb..dd8c901f 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -45,7 +45,7 @@ import Control.Concurrent.MVar import Control.Exception import Control.Monad (forever) import Control.Monad.STM (atomically) - + import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() @@ -73,12 +73,12 @@ assertComplete msg mv a = do b <- takeMVar mv assertBool msg (a == b) --- synchronised logging +-- synchronised logging -data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String } +data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String } -- | Create a new Logger. --- Logger uses a 'TQueue' to receive and process messages on a worker thread. +-- Logger uses a 'TQueue' to receive and process messages on a worker thread. newLogger :: IO Logger newLogger = do tid <- liftIO $ myThreadId @@ -95,7 +95,7 @@ putLogMsg logger msg = liftIO $ atomically $ writeTQueue (msgs logger) msg -- | Stop the worker thread for the given Logger stopLogger :: Logger -> IO () -stopLogger = (flip throwTo) ThreadKilled . _tid +stopLogger = (flip throwTo) ThreadKilled . _tid -- | Given a @builder@ function, make and run a test suite on a single transport testMain :: (NT.Transport -> IO [Test]) -> IO () From c5a1fec1e820950a0ef2bb5dd3fa7f1d5ad6c640 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:12 +0000 Subject: [PATCH 0724/2357] strip trailing whitespace - stick to emacs next time --- src/Control/Distributed/Process/AsyncChan.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 71e4ce98..f4c15303 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -73,7 +73,7 @@ type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- other than the caller of 'async' - that is, this module provides asynchronous -- actions whose results are accessible *only* by the initiating process. This -- limitation is imposed becuase of the use of type channels, for which the --- @ReceivePort@ component is effectively /thread local/. +-- @ReceivePort@ component is effectively /thread local/. -- -- See 'async' data AsyncChan a = AsyncChan { @@ -226,7 +226,7 @@ waitCancelTimeout t hAsync = do r <- waitTimeout t hAsync case r of Nothing -> cancelWait hAsync - Just ar -> return ar + Just ar -> return ar -- | Wait for any of the supplied @AsyncChans@s to complete. If multiple -- 'Async's complete, then the value returned corresponds to the first From 458693bdcc48179a246a3c4ee10fb7228156870f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:12 +0000 Subject: [PATCH 0725/2357] strip trailing whitespace - stick to emacs next time --- tests/TestUtils.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 17a00bdb..dd8c901f 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -45,7 +45,7 @@ import Control.Concurrent.MVar import Control.Exception import Control.Monad (forever) import Control.Monad.STM (atomically) - + import Control.Distributed.Process import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() @@ -73,12 +73,12 @@ assertComplete msg mv a = do b <- takeMVar mv assertBool msg (a == b) --- synchronised logging +-- synchronised logging -data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String } +data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String } -- | Create a new Logger. --- Logger uses a 'TQueue' to receive and process messages on a worker thread. +-- Logger uses a 'TQueue' to receive and process messages on a worker thread. newLogger :: IO Logger newLogger = do tid <- liftIO $ myThreadId @@ -95,7 +95,7 @@ putLogMsg logger msg = liftIO $ atomically $ writeTQueue (msgs logger) msg -- | Stop the worker thread for the given Logger stopLogger :: Logger -> IO () -stopLogger = (flip throwTo) ThreadKilled . _tid +stopLogger = (flip throwTo) ThreadKilled . _tid -- | Given a @builder@ function, make and run a test suite on a single transport testMain :: (NT.Transport -> IO [Test]) -> IO () From 760c1a87bf576486503e2d019fc4e18319d7d01d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:25 +0000 Subject: [PATCH 0726/2357] make foo --- Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c69f6e8d..e30299a5 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ ## IN THE SOFTWARE. ## ---------------------------------------------------------------------------- +CONF=./dist/setup-config +CABAL=distributed-process-platform.cabal +BUILD_DEPENDS=$(CONF) $(CABAL) + .PHONY: all all: build @@ -33,9 +37,9 @@ build: configure cabal build .PHONY: configure -configure: ./dist/setup-config +configure: $(BUILD_DEPENDS) -./dist/setup-config: +$(BUILD_DEPENDS): cabal configure --enable-tests .PHONY: clean From d40ad6553fad4c22297b9d07a65c2a84ce4a22ca Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:25 +0000 Subject: [PATCH 0727/2357] make foo --- Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c69f6e8d..e30299a5 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ ## IN THE SOFTWARE. ## ---------------------------------------------------------------------------- +CONF=./dist/setup-config +CABAL=distributed-process-platform.cabal +BUILD_DEPENDS=$(CONF) $(CABAL) + .PHONY: all all: build @@ -33,9 +37,9 @@ build: configure cabal build .PHONY: configure -configure: ./dist/setup-config +configure: $(BUILD_DEPENDS) -./dist/setup-config: +$(BUILD_DEPENDS): cabal configure --enable-tests .PHONY: clean From a6c3e48487e983604d797a46526fc53c3237ef6f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 01:56:25 +0000 Subject: [PATCH 0728/2357] make foo --- Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c69f6e8d..e30299a5 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ ## IN THE SOFTWARE. ## ---------------------------------------------------------------------------- +CONF=./dist/setup-config +CABAL=distributed-process-platform.cabal +BUILD_DEPENDS=$(CONF) $(CABAL) + .PHONY: all all: build @@ -33,9 +37,9 @@ build: configure cabal build .PHONY: configure -configure: ./dist/setup-config +configure: $(BUILD_DEPENDS) -./dist/setup-config: +$(BUILD_DEPENDS): cabal configure --enable-tests .PHONY: clean From 4b9273e6c7197b5d16b6214448f15288426f9274 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 02:03:33 +0000 Subject: [PATCH 0729/2357] Merge branch 'master' into genproc2 --- src/Control/Distributed/Process/Platform/Timer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index 1ab63d38..dbdb1151 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -13,7 +13,7 @@ -- -- Provides an API for running code or sending messages, either after some -- initial delay or periodically, and for cancelling, re-setting and/or --- flushing pending /timers/. +-- flushing pending /timers/. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.Timer From 755e020b214c5a4ea4f9822bdd1c04198e709d95 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 10:45:49 +0000 Subject: [PATCH 0730/2357] test gen-process cast --- tests/TestGenServer.hs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 24560034..70a31125 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -59,6 +59,13 @@ testBasicCall_ result = do pid <- server callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result +testBasicCast :: TestResult (Maybe String) -> Process () +testBasicCast result = do + self <- getSelfPid + pid <- server + cast pid ("ping", self) + expectTimeout (after 3 Seconds) >>= stash result + server :: Process ProcessId server = let s = statelessProcess { @@ -103,7 +110,10 @@ tests transport = do (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - + , testCase "basic cast" + (delayedAssertion + "expected pong back from the server" + localNode (Just "pong") testBasicCast) ] ] From 5432926d563723c42add0d0ff50227118a142f2b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 10:45:49 +0000 Subject: [PATCH 0731/2357] test gen-process cast --- tests/TestGenServer.hs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 24560034..70a31125 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -59,6 +59,13 @@ testBasicCall_ result = do pid <- server callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result +testBasicCast :: TestResult (Maybe String) -> Process () +testBasicCast result = do + self <- getSelfPid + pid <- server + cast pid ("ping", self) + expectTimeout (after 3 Seconds) >>= stash result + server :: Process ProcessId server = let s = statelessProcess { @@ -103,7 +110,10 @@ tests transport = do (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - + , testCase "basic cast" + (delayedAssertion + "expected pong back from the server" + localNode (Just "pong") testBasicCast) ] ] From a81cff42ac8468cfc5ecfd5f5f0cb887a51b0dfd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 10:45:49 +0000 Subject: [PATCH 0732/2357] test gen-process cast --- tests/TestGenServer.hs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 24560034..70a31125 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -59,6 +59,13 @@ testBasicCall_ result = do pid <- server callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result +testBasicCast :: TestResult (Maybe String) -> Process () +testBasicCast result = do + self <- getSelfPid + pid <- server + cast pid ("ping", self) + expectTimeout (after 3 Seconds) >>= stash result + server :: Process ProcessId server = let s = statelessProcess { @@ -103,7 +110,10 @@ tests transport = do (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - + , testCase "basic cast" + (delayedAssertion + "expected pong back from the server" + localNode (Just "pong") testBasicCast) ] ] From f66986283700d7933385176ed0ae49aed87d368f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 12:24:46 +0000 Subject: [PATCH 0733/2357] cosmetic --- src/Control/Distributed/Process/Platform/Time.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index 5e5a377d..4ef83d06 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -147,7 +147,7 @@ secondsPerMinute = 60 {-# INLINE milliSecondsPerSecond #-} milliSecondsPerSecond :: Int milliSecondsPerSecond = 1000 - + -- timeouts/delays (microseconds) -- | Constructs an inifinite 'Timeout'. From 54751f7dea2bc1872c075f653068235db5c0fb8d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 12:25:25 +0000 Subject: [PATCH 0734/2357] Timer now sports {exit|kill}After API --- src/Control/Distributed/Process/Platform/Timer.hs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/Timer.hs b/src/Control/Distributed/Process/Platform/Timer.hs index dbdb1151..459c8efe 100644 --- a/src/Control/Distributed/Process/Platform/Timer.hs +++ b/src/Control/Distributed/Process/Platform/Timer.hs @@ -23,6 +23,8 @@ module Control.Distributed.Process.Platform.Timer , sleep , sendAfter , runAfter + , exitAfter + , killAfter , startTimer , ticker , periodically @@ -86,6 +88,18 @@ sendAfter t pid msg = runAfter t proc runAfter :: TimeInterval -> Process () -> Process TimerRef runAfter t p = spawnLocal $ runTimer t p True +-- | calls @exit pid reason@ after @t@ has elapsed +exitAfter :: (Serializable a) + => TimeInterval + -> ProcessId + -> a + -> Process TimerRef +exitAfter delay pid reason = runAfter delay $ exit pid reason + +-- | kills the specified process after @t@ has elapsed +killAfter :: TimeInterval -> ProcessId -> String -> Process TimerRef +killAfter delay pid why = runAfter delay $ kill pid why + -- | starts a timer that repeatedly sends the supplied message to the destination -- process each time the specified time interval elapses. To stop messages from -- being sent in future, 'cancelTimer' can be called. From eef9bd6419092eeffd4358f82e1d3e7544644f03 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 12:26:03 +0000 Subject: [PATCH 0735/2357] TerminateReason needs to be Serializable, testing server timeouts --- .../Process/Platform/GenProcess.hs | 10 +-- tests/TestGenServer.hs | 68 ++++++++++++------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 7360bf3a..4111a756 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -2,6 +2,8 @@ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE ImpredicativeTypes #-} module Control.Distributed.Process.Platform.GenProcess ( -- exported data types @@ -86,9 +88,9 @@ $(derive makeBinary ''CallResponse) data TerminateReason = TerminateNormal | TerminateShutdown - | forall r. (Serializable r) => - TerminateOther r - deriving (Typeable) + | TerminateOther String + deriving (Typeable, Eq, Show) +$(derive makeBinary ''TerminateReason) -- | Initialization data InitResult s = @@ -483,7 +485,7 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = case p of - Terminate -> stop (TerminateOther "unexpected-input") + Terminate -> stop $ TerminateOther "unexpected-input" DeadLetter pid -> forward m pid >> continue s Drop -> continue s diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 70a31125..b7da93da 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,16 +1,19 @@ {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE ImpredicativeTypes #-} -- NB: this module contains tests for the GenProcess /and/ GenServer API. module Main where --- import Control.Concurrent.MVar +import Control.Concurrent.MVar import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() -import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Platform.Timer + import Data.Binary() import Data.Typeable() @@ -51,69 +54,82 @@ mathTest t n l sid x y op = let fn = expr op in do testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do - pid <- server + (pid, _) <- server callTimeout pid "foo" (within 5 Seconds) >>= stash result testBasicCall_ :: TestResult (Maybe Int) -> Process () testBasicCall_ result = do - pid <- server + (pid, _) <- server callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result testBasicCast :: TestResult (Maybe String) -> Process () testBasicCast result = do self <- getSelfPid - pid <- server + (pid, _) <- server cast pid ("ping", self) expectTimeout (after 3 Seconds) >>= stash result -server :: Process ProcessId +testControlledTimeout :: TestResult (Maybe TerminateReason) -> Process () +testControlledTimeout result = do + self <- getSelfPid + (pid, exitReason) <- server + cast pid ("timeout", Delay $ within 1 Seconds) + + -- we *might* end up blocked here, so ensure the test suite doesn't jam! + killAfter (within 10 Seconds) self "testcast timed out" + + tr <- liftIO $ takeMVar exitReason + case tr of + Right r -> stash result (Just r) + _ -> stash result Nothing + +server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) server = let s = statelessProcess { dispatchers = [ - handleCall (\s' (m :: String) -> reply m s') -- state passed + -- note: state is passed here, as a 'stateless' server is a + -- server with state = () + handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - , handleCast (\s' ("ping", -- regular cast - pid :: ProcessId) -> - send pid "pong" >> continue s') - - -- "stateless" + , handleCast (\s' ("ping", pid :: ProcessId) -> + send pid "pong" >> continue s') , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) - - , action (\"stop" -> stop_ TerminateNormal) + , action (\("stop") -> stop_ TerminateNormal) ] - , infoHandlers = [] , unhandledMessagePolicy = Terminate - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } - in spawnLocal $ start () startup s >> return () + in do + exitReason <- liftIO $ newEmptyMVar + pid <- spawnLocal $ (start () startup s) >>= stash exitReason + return (pid, exitReason) where startup :: InitHandler () () startup _ = return $ InitOk () Infinity tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable - -- mv <- newEmptyMVar -- _ <- forkProcess localNode $ launchMathServer >>= stash mv - -- sid <- takeMVar mv return [ - testGroup "Handling async results" [ --- mathTest "simple addition" --- "10 + 10 = 20" --- localNode sid 10 10 opAdd - testCase "basic call" + testGroup "basic server functionality" [ + testCase "basic call with explicit server reply" (delayedAssertion "expected a response from the server" localNode (Just "foo") testBasicCall) - , testCase "basic call_" + , testCase "basic call with implicit server reply" (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - , testCase "basic cast" + , testCase "basic cast with manual send and explicit server continue" (delayedAssertion "expected pong back from the server" localNode (Just "pong") testBasicCast) + , testCase "cast and explicit server timeout" + (delayedAssertion + "expected the server to stop after the timeout" + localNode (Just (TerminateOther "timeout")) testControlledTimeout) ] ] From 7b4443f29a04dca0248f8d45e443ac992834557c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 12:26:03 +0000 Subject: [PATCH 0736/2357] TerminateReason needs to be Serializable, testing server timeouts --- tests/TestGenServer.hs | 68 ++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 70a31125..b7da93da 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,16 +1,19 @@ {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE ImpredicativeTypes #-} -- NB: this module contains tests for the GenProcess /and/ GenServer API. module Main where --- import Control.Concurrent.MVar +import Control.Concurrent.MVar import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() -import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Platform.Timer + import Data.Binary() import Data.Typeable() @@ -51,69 +54,82 @@ mathTest t n l sid x y op = let fn = expr op in do testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do - pid <- server + (pid, _) <- server callTimeout pid "foo" (within 5 Seconds) >>= stash result testBasicCall_ :: TestResult (Maybe Int) -> Process () testBasicCall_ result = do - pid <- server + (pid, _) <- server callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result testBasicCast :: TestResult (Maybe String) -> Process () testBasicCast result = do self <- getSelfPid - pid <- server + (pid, _) <- server cast pid ("ping", self) expectTimeout (after 3 Seconds) >>= stash result -server :: Process ProcessId +testControlledTimeout :: TestResult (Maybe TerminateReason) -> Process () +testControlledTimeout result = do + self <- getSelfPid + (pid, exitReason) <- server + cast pid ("timeout", Delay $ within 1 Seconds) + + -- we *might* end up blocked here, so ensure the test suite doesn't jam! + killAfter (within 10 Seconds) self "testcast timed out" + + tr <- liftIO $ takeMVar exitReason + case tr of + Right r -> stash result (Just r) + _ -> stash result Nothing + +server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) server = let s = statelessProcess { dispatchers = [ - handleCall (\s' (m :: String) -> reply m s') -- state passed + -- note: state is passed here, as a 'stateless' server is a + -- server with state = () + handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - , handleCast (\s' ("ping", -- regular cast - pid :: ProcessId) -> - send pid "pong" >> continue s') - - -- "stateless" + , handleCast (\s' ("ping", pid :: ProcessId) -> + send pid "pong" >> continue s') , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) - - , action (\"stop" -> stop_ TerminateNormal) + , action (\("stop") -> stop_ TerminateNormal) ] - , infoHandlers = [] , unhandledMessagePolicy = Terminate - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } - in spawnLocal $ start () startup s >> return () + in do + exitReason <- liftIO $ newEmptyMVar + pid <- spawnLocal $ (start () startup s) >>= stash exitReason + return (pid, exitReason) where startup :: InitHandler () () startup _ = return $ InitOk () Infinity tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable - -- mv <- newEmptyMVar -- _ <- forkProcess localNode $ launchMathServer >>= stash mv - -- sid <- takeMVar mv return [ - testGroup "Handling async results" [ --- mathTest "simple addition" --- "10 + 10 = 20" --- localNode sid 10 10 opAdd - testCase "basic call" + testGroup "basic server functionality" [ + testCase "basic call with explicit server reply" (delayedAssertion "expected a response from the server" localNode (Just "foo") testBasicCall) - , testCase "basic call_" + , testCase "basic call with implicit server reply" (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - , testCase "basic cast" + , testCase "basic cast with manual send and explicit server continue" (delayedAssertion "expected pong back from the server" localNode (Just "pong") testBasicCast) + , testCase "cast and explicit server timeout" + (delayedAssertion + "expected the server to stop after the timeout" + localNode (Just (TerminateOther "timeout")) testControlledTimeout) ] ] From 0f509e0c644cd3121ee9d07357572d41d2650c82 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 12:26:03 +0000 Subject: [PATCH 0737/2357] TerminateReason needs to be Serializable, testing server timeouts --- tests/MathsDemo.hs | 2 +- tests/TestGenServer.hs | 68 ++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index 46319144..fb541f7d 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -47,7 +47,7 @@ launchMathServer = (\(Divide x y) -> return (x / y)) , handleCall_ (\(Divide _ _) -> return DivByZero) - , action (\"stop" -> stop_ TerminateNormal) + , action (\("stop") -> stop_ TerminateNormal) ] } :: Behaviour () in spawnLocal $ start () startup server >> return () diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 70a31125..b7da93da 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,16 +1,19 @@ {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE ImpredicativeTypes #-} -- NB: this module contains tests for the GenProcess /and/ GenServer API. module Main where --- import Control.Concurrent.MVar +import Control.Concurrent.MVar import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() -import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Platform.Timer + import Data.Binary() import Data.Typeable() @@ -51,69 +54,82 @@ mathTest t n l sid x y op = let fn = expr op in do testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do - pid <- server + (pid, _) <- server callTimeout pid "foo" (within 5 Seconds) >>= stash result testBasicCall_ :: TestResult (Maybe Int) -> Process () testBasicCall_ result = do - pid <- server + (pid, _) <- server callTimeout pid (2 :: Int) (within 5 Seconds) >>= stash result testBasicCast :: TestResult (Maybe String) -> Process () testBasicCast result = do self <- getSelfPid - pid <- server + (pid, _) <- server cast pid ("ping", self) expectTimeout (after 3 Seconds) >>= stash result -server :: Process ProcessId +testControlledTimeout :: TestResult (Maybe TerminateReason) -> Process () +testControlledTimeout result = do + self <- getSelfPid + (pid, exitReason) <- server + cast pid ("timeout", Delay $ within 1 Seconds) + + -- we *might* end up blocked here, so ensure the test suite doesn't jam! + killAfter (within 10 Seconds) self "testcast timed out" + + tr <- liftIO $ takeMVar exitReason + case tr of + Right r -> stash result (Just r) + _ -> stash result Nothing + +server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) server = let s = statelessProcess { dispatchers = [ - handleCall (\s' (m :: String) -> reply m s') -- state passed + -- note: state is passed here, as a 'stateless' server is a + -- server with state = () + handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - , handleCast (\s' ("ping", -- regular cast - pid :: ProcessId) -> - send pid "pong" >> continue s') - - -- "stateless" + , handleCast (\s' ("ping", pid :: ProcessId) -> + send pid "pong" >> continue s') , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) - - , action (\"stop" -> stop_ TerminateNormal) + , action (\("stop") -> stop_ TerminateNormal) ] - , infoHandlers = [] , unhandledMessagePolicy = Terminate - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } - in spawnLocal $ start () startup s >> return () + in do + exitReason <- liftIO $ newEmptyMVar + pid <- spawnLocal $ (start () startup s) >>= stash exitReason + return (pid, exitReason) where startup :: InitHandler () () startup _ = return $ InitOk () Infinity tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable - -- mv <- newEmptyMVar -- _ <- forkProcess localNode $ launchMathServer >>= stash mv - -- sid <- takeMVar mv return [ - testGroup "Handling async results" [ --- mathTest "simple addition" --- "10 + 10 = 20" --- localNode sid 10 10 opAdd - testCase "basic call" + testGroup "basic server functionality" [ + testCase "basic call with explicit server reply" (delayedAssertion "expected a response from the server" localNode (Just "foo") testBasicCall) - , testCase "basic call_" + , testCase "basic call with implicit server reply" (delayedAssertion "expected n * 2 back from the server" localNode (Just 4) testBasicCall_) - , testCase "basic cast" + , testCase "basic cast with manual send and explicit server continue" (delayedAssertion "expected pong back from the server" localNode (Just "pong") testBasicCast) + , testCase "cast and explicit server timeout" + (delayedAssertion + "expected the server to stop after the timeout" + localNode (Just (TerminateOther "timeout")) testControlledTimeout) ] ] From fbb53364eed2dc03ec6f085109dcd9160dd70d99 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 12:26:03 +0000 Subject: [PATCH 0738/2357] TerminateReason needs to be Serializable, testing server timeouts --- src/Control/Distributed/Process/Platform/GenProcess.hs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 7360bf3a..4111a756 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -2,6 +2,8 @@ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE ImpredicativeTypes #-} module Control.Distributed.Process.Platform.GenProcess ( -- exported data types @@ -86,9 +88,9 @@ $(derive makeBinary ''CallResponse) data TerminateReason = TerminateNormal | TerminateShutdown - | forall r. (Serializable r) => - TerminateOther r - deriving (Typeable) + | TerminateOther String + deriving (Typeable, Eq, Show) +$(derive makeBinary ''TerminateReason) -- | Initialization data InitResult s = @@ -483,7 +485,7 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = case p of - Terminate -> stop (TerminateOther "unexpected-input") + Terminate -> stop $ TerminateOther "unexpected-input" DeadLetter pid -> forward m pid >> continue s Drop -> continue s From 855f38e50850a0fa0aefd4c5719f553130a425d9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:01:45 +0000 Subject: [PATCH 0739/2357] verify unhandledMessagePolicy Terminate kills the process --- .../Process/Platform/GenProcess.hs | 2 +- tests/TestGenServer.hs | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 4111a756..9d15eab2 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -485,7 +485,7 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = case p of - Terminate -> stop $ TerminateOther "unexpected-input" + Terminate -> stop $ TerminateOther "UNHANDLED_INPUT" DeadLetter pid -> forward m pid >> continue s Drop -> continue s diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index b7da93da..4b821dd0 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -17,7 +17,6 @@ import Control.Distributed.Process.Platform.Timer import Data.Binary() import Data.Typeable() - import MathsDemo import Test.Framework (Test, testGroup) @@ -83,6 +82,19 @@ testControlledTimeout result = do Right r -> stash result (Just r) _ -> stash result Nothing +testTerminatePolicy :: TestResult (Maybe TerminateReason) -> Process () +testTerminatePolicy result = do + self <- getSelfPid + (pid, exitReason) <- server + send pid ("UNSOLICITED_MAIL", 500 :: Int) + + killAfter (within 10 Seconds) self "testcase timed out" + + tr <- liftIO $ takeMVar exitReason + case tr of + Right r -> stash result (Just r) + Left _ -> stash result Nothing + server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) server = let s = statelessProcess { @@ -130,6 +142,11 @@ tests transport = do (delayedAssertion "expected the server to stop after the timeout" localNode (Just (TerminateOther "timeout")) testControlledTimeout) + , testCase "unhandled input when policy = Terminate" + (delayedAssertion + "expected the server to stop upon receiving unhandled input" + localNode (Just (TerminateOther "UNHANDLED_INPUT")) + testTerminatePolicy) ] ] From 5593af4f061ea9a20e7723e00d0d9c0f657f8e2d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:01:45 +0000 Subject: [PATCH 0740/2357] verify unhandledMessagePolicy Terminate kills the process --- tests/TestGenServer.hs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index b7da93da..4b821dd0 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -17,7 +17,6 @@ import Control.Distributed.Process.Platform.Timer import Data.Binary() import Data.Typeable() - import MathsDemo import Test.Framework (Test, testGroup) @@ -83,6 +82,19 @@ testControlledTimeout result = do Right r -> stash result (Just r) _ -> stash result Nothing +testTerminatePolicy :: TestResult (Maybe TerminateReason) -> Process () +testTerminatePolicy result = do + self <- getSelfPid + (pid, exitReason) <- server + send pid ("UNSOLICITED_MAIL", 500 :: Int) + + killAfter (within 10 Seconds) self "testcase timed out" + + tr <- liftIO $ takeMVar exitReason + case tr of + Right r -> stash result (Just r) + Left _ -> stash result Nothing + server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) server = let s = statelessProcess { @@ -130,6 +142,11 @@ tests transport = do (delayedAssertion "expected the server to stop after the timeout" localNode (Just (TerminateOther "timeout")) testControlledTimeout) + , testCase "unhandled input when policy = Terminate" + (delayedAssertion + "expected the server to stop upon receiving unhandled input" + localNode (Just (TerminateOther "UNHANDLED_INPUT")) + testTerminatePolicy) ] ] From 6acd8d8bf98af3a7f181e6e593759ac39f4acf13 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:01:45 +0000 Subject: [PATCH 0741/2357] verify unhandledMessagePolicy Terminate kills the process --- tests/TestGenServer.hs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index b7da93da..4b821dd0 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -17,7 +17,6 @@ import Control.Distributed.Process.Platform.Timer import Data.Binary() import Data.Typeable() - import MathsDemo import Test.Framework (Test, testGroup) @@ -83,6 +82,19 @@ testControlledTimeout result = do Right r -> stash result (Just r) _ -> stash result Nothing +testTerminatePolicy :: TestResult (Maybe TerminateReason) -> Process () +testTerminatePolicy result = do + self <- getSelfPid + (pid, exitReason) <- server + send pid ("UNSOLICITED_MAIL", 500 :: Int) + + killAfter (within 10 Seconds) self "testcase timed out" + + tr <- liftIO $ takeMVar exitReason + case tr of + Right r -> stash result (Just r) + Left _ -> stash result Nothing + server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) server = let s = statelessProcess { @@ -130,6 +142,11 @@ tests transport = do (delayedAssertion "expected the server to stop after the timeout" localNode (Just (TerminateOther "timeout")) testControlledTimeout) + , testCase "unhandled input when policy = Terminate" + (delayedAssertion + "expected the server to stop upon receiving unhandled input" + localNode (Just (TerminateOther "UNHANDLED_INPUT")) + testTerminatePolicy) ] ] From 4b5a84bf48706ac3cdf250a1dfc4931251c74c2c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:01:45 +0000 Subject: [PATCH 0742/2357] verify unhandledMessagePolicy Terminate kills the process --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 4111a756..9d15eab2 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -485,7 +485,7 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = case p of - Terminate -> stop $ TerminateOther "unexpected-input" + Terminate -> stop $ TerminateOther "UNHANDLED_INPUT" DeadLetter pid -> forward m pid >> continue s Drop -> continue s From 778a08e6d20a92c7cfdf01f80abe83451ba4c962 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:57:33 +0000 Subject: [PATCH 0743/2357] refactor the tests - add test case for GenProcess Drop policy --- tests/TestGenServer.hs | 59 ++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 4b821dd0..cc0723c5 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -70,33 +70,56 @@ testBasicCast result = do testControlledTimeout :: TestResult (Maybe TerminateReason) -> Process () testControlledTimeout result = do - self <- getSelfPid (pid, exitReason) <- server cast pid ("timeout", Delay $ within 1 Seconds) - - -- we *might* end up blocked here, so ensure the test suite doesn't jam! - killAfter (within 10 Seconds) self "testcast timed out" - - tr <- liftIO $ takeMVar exitReason - case tr of - Right r -> stash result (Just r) - _ -> stash result Nothing + waitForExit exitReason >>= stash result testTerminatePolicy :: TestResult (Maybe TerminateReason) -> Process () testTerminatePolicy result = do - self <- getSelfPid (pid, exitReason) <- server send pid ("UNSOLICITED_MAIL", 500 :: Int) + waitForExit exitReason >>= stash result - killAfter (within 10 Seconds) self "testcase timed out" +testDropPolicy :: TestResult (Maybe TerminateReason) -> Process () +testDropPolicy result = do + (pid, exitReason) <- mkServer Drop + send pid ("UNSOLICITED_MAIL", 500 :: Int) + + sleep $ seconds 1 + mref <- monitor pid + + cast pid "stop" + + r <- receiveTimeout (after 10 Seconds) [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\(ProcessMonitorNotification _ _ r) -> + case r of + DiedUnknownId -> stash result Nothing + _ -> waitForExit exitReason >>= stash result) + ] + case r of + Nothing -> stash result Nothing + _ -> return () + +waitForExit :: MVar (Either (InitResult ()) TerminateReason) + -> Process (Maybe TerminateReason) +waitForExit exitReason = do + -- we *might* end up blocked here, so ensure the test suite doesn't jam! + self <- getSelfPid + tref <- killAfter (within 10 Seconds) self "testcast timed out" tr <- liftIO $ takeMVar exitReason + cancelTimer tref case tr of - Right r -> stash result (Just r) - Left _ -> stash result Nothing + Right r -> return (Just r) + Left _ -> return Nothing server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) -server = +server = mkServer Terminate + +mkServer :: UnhandledMessagePolicy + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +mkServer policy = let s = statelessProcess { dispatchers = [ -- note: state is passed here, as a 'stateless' server is a @@ -110,7 +133,7 @@ server = (\("timeout", Delay d) -> timeoutAfter_ d) , action (\("stop") -> stop_ TerminateNormal) ] - , unhandledMessagePolicy = Terminate + , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } in do @@ -147,7 +170,11 @@ tests transport = do "expected the server to stop upon receiving unhandled input" localNode (Just (TerminateOther "UNHANDLED_INPUT")) testTerminatePolicy) - ] + , testCase "unhandled input when policy = Drop" + (delayedAssertion + "expected the server to ignore unhandled input and exit normally" + localNode (Just TerminateNormal) testDropPolicy) + ] ] main :: IO () From 51c32ca548495a1de39f17619357f2554c7bd7b9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:57:33 +0000 Subject: [PATCH 0744/2357] refactor the tests - add test case for GenProcess Drop policy --- tests/TestGenServer.hs | 59 ++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 4b821dd0..cc0723c5 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -70,33 +70,56 @@ testBasicCast result = do testControlledTimeout :: TestResult (Maybe TerminateReason) -> Process () testControlledTimeout result = do - self <- getSelfPid (pid, exitReason) <- server cast pid ("timeout", Delay $ within 1 Seconds) - - -- we *might* end up blocked here, so ensure the test suite doesn't jam! - killAfter (within 10 Seconds) self "testcast timed out" - - tr <- liftIO $ takeMVar exitReason - case tr of - Right r -> stash result (Just r) - _ -> stash result Nothing + waitForExit exitReason >>= stash result testTerminatePolicy :: TestResult (Maybe TerminateReason) -> Process () testTerminatePolicy result = do - self <- getSelfPid (pid, exitReason) <- server send pid ("UNSOLICITED_MAIL", 500 :: Int) + waitForExit exitReason >>= stash result - killAfter (within 10 Seconds) self "testcase timed out" +testDropPolicy :: TestResult (Maybe TerminateReason) -> Process () +testDropPolicy result = do + (pid, exitReason) <- mkServer Drop + send pid ("UNSOLICITED_MAIL", 500 :: Int) + + sleep $ seconds 1 + mref <- monitor pid + + cast pid "stop" + + r <- receiveTimeout (after 10 Seconds) [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\(ProcessMonitorNotification _ _ r) -> + case r of + DiedUnknownId -> stash result Nothing + _ -> waitForExit exitReason >>= stash result) + ] + case r of + Nothing -> stash result Nothing + _ -> return () + +waitForExit :: MVar (Either (InitResult ()) TerminateReason) + -> Process (Maybe TerminateReason) +waitForExit exitReason = do + -- we *might* end up blocked here, so ensure the test suite doesn't jam! + self <- getSelfPid + tref <- killAfter (within 10 Seconds) self "testcast timed out" tr <- liftIO $ takeMVar exitReason + cancelTimer tref case tr of - Right r -> stash result (Just r) - Left _ -> stash result Nothing + Right r -> return (Just r) + Left _ -> return Nothing server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) -server = +server = mkServer Terminate + +mkServer :: UnhandledMessagePolicy + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +mkServer policy = let s = statelessProcess { dispatchers = [ -- note: state is passed here, as a 'stateless' server is a @@ -110,7 +133,7 @@ server = (\("timeout", Delay d) -> timeoutAfter_ d) , action (\("stop") -> stop_ TerminateNormal) ] - , unhandledMessagePolicy = Terminate + , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } in do @@ -147,7 +170,11 @@ tests transport = do "expected the server to stop upon receiving unhandled input" localNode (Just (TerminateOther "UNHANDLED_INPUT")) testTerminatePolicy) - ] + , testCase "unhandled input when policy = Drop" + (delayedAssertion + "expected the server to ignore unhandled input and exit normally" + localNode (Just TerminateNormal) testDropPolicy) + ] ] main :: IO () From 92e3122d0fbac09f65a5267ace078264b356390d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 14:57:33 +0000 Subject: [PATCH 0745/2357] refactor the tests - add test case for GenProcess Drop policy --- tests/TestGenServer.hs | 59 ++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 4b821dd0..cc0723c5 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -70,33 +70,56 @@ testBasicCast result = do testControlledTimeout :: TestResult (Maybe TerminateReason) -> Process () testControlledTimeout result = do - self <- getSelfPid (pid, exitReason) <- server cast pid ("timeout", Delay $ within 1 Seconds) - - -- we *might* end up blocked here, so ensure the test suite doesn't jam! - killAfter (within 10 Seconds) self "testcast timed out" - - tr <- liftIO $ takeMVar exitReason - case tr of - Right r -> stash result (Just r) - _ -> stash result Nothing + waitForExit exitReason >>= stash result testTerminatePolicy :: TestResult (Maybe TerminateReason) -> Process () testTerminatePolicy result = do - self <- getSelfPid (pid, exitReason) <- server send pid ("UNSOLICITED_MAIL", 500 :: Int) + waitForExit exitReason >>= stash result - killAfter (within 10 Seconds) self "testcase timed out" +testDropPolicy :: TestResult (Maybe TerminateReason) -> Process () +testDropPolicy result = do + (pid, exitReason) <- mkServer Drop + send pid ("UNSOLICITED_MAIL", 500 :: Int) + + sleep $ seconds 1 + mref <- monitor pid + + cast pid "stop" + + r <- receiveTimeout (after 10 Seconds) [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\(ProcessMonitorNotification _ _ r) -> + case r of + DiedUnknownId -> stash result Nothing + _ -> waitForExit exitReason >>= stash result) + ] + case r of + Nothing -> stash result Nothing + _ -> return () + +waitForExit :: MVar (Either (InitResult ()) TerminateReason) + -> Process (Maybe TerminateReason) +waitForExit exitReason = do + -- we *might* end up blocked here, so ensure the test suite doesn't jam! + self <- getSelfPid + tref <- killAfter (within 10 Seconds) self "testcast timed out" tr <- liftIO $ takeMVar exitReason + cancelTimer tref case tr of - Right r -> stash result (Just r) - Left _ -> stash result Nothing + Right r -> return (Just r) + Left _ -> return Nothing server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) -server = +server = mkServer Terminate + +mkServer :: UnhandledMessagePolicy + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +mkServer policy = let s = statelessProcess { dispatchers = [ -- note: state is passed here, as a 'stateless' server is a @@ -110,7 +133,7 @@ server = (\("timeout", Delay d) -> timeoutAfter_ d) , action (\("stop") -> stop_ TerminateNormal) ] - , unhandledMessagePolicy = Terminate + , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" } in do @@ -147,7 +170,11 @@ tests transport = do "expected the server to stop upon receiving unhandled input" localNode (Just (TerminateOther "UNHANDLED_INPUT")) testTerminatePolicy) - ] + , testCase "unhandled input when policy = Drop" + (delayedAssertion + "expected the server to ignore unhandled input and exit normally" + localNode (Just TerminateNormal) testDropPolicy) + ] ] main :: IO () From 6d4f9c354a0e7dea6888cfc67ce31b5aeee48e56 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:02:04 +0000 Subject: [PATCH 0746/2357] make it easier to initialise a 'stateless' GP - refactor the tests accordingly --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++++ tests/TestGenServer.hs | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9d15eab2..ff9d743b 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -21,6 +21,7 @@ module Control.Distributed.Process.Platform.GenProcess -- interaction with the process , start , statelessProcess + , statelessInit , call , safeCall , callAsync @@ -185,6 +186,9 @@ statelessProcess = Behaviour { , unhandledMessagePolicy = Terminate } +statelessInit :: Delay -> InitHandler () () +statelessInit d () = return $ InitOk () d + -- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index cc0723c5..d81a8137 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -122,8 +122,8 @@ mkServer :: UnhandledMessagePolicy mkServer policy = let s = statelessProcess { dispatchers = [ - -- note: state is passed here, as a 'stateless' server is a - -- server with state = () + -- note: state is passed here, as a 'stateless' process is + -- in fact process definition whose state is () handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" @@ -138,10 +138,9 @@ mkServer policy = } in do exitReason <- liftIO $ newEmptyMVar - pid <- spawnLocal $ (start () startup s) >>= stash exitReason + pid <- spawnLocal $ do + start () (statelessInit Infinity) s >>= stash exitReason return (pid, exitReason) - where startup :: InitHandler () () - startup _ = return $ InitOk () Infinity tests :: NT.Transport -> IO [Test] tests transport = do From 87078101cf66a3aafaa4639cc1c30cb46133d8aa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:02:04 +0000 Subject: [PATCH 0747/2357] make it easier to initialise a 'stateless' GP - refactor the tests accordingly --- tests/TestGenServer.hs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index cc0723c5..d81a8137 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -122,8 +122,8 @@ mkServer :: UnhandledMessagePolicy mkServer policy = let s = statelessProcess { dispatchers = [ - -- note: state is passed here, as a 'stateless' server is a - -- server with state = () + -- note: state is passed here, as a 'stateless' process is + -- in fact process definition whose state is () handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" @@ -138,10 +138,9 @@ mkServer policy = } in do exitReason <- liftIO $ newEmptyMVar - pid <- spawnLocal $ (start () startup s) >>= stash exitReason + pid <- spawnLocal $ do + start () (statelessInit Infinity) s >>= stash exitReason return (pid, exitReason) - where startup :: InitHandler () () - startup _ = return $ InitOk () Infinity tests :: NT.Transport -> IO [Test] tests transport = do From 369dc2bff72d2b27aacb37362751f6ee0042def1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:02:04 +0000 Subject: [PATCH 0748/2357] make it easier to initialise a 'stateless' GP - refactor the tests accordingly --- tests/TestGenServer.hs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index cc0723c5..d81a8137 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -122,8 +122,8 @@ mkServer :: UnhandledMessagePolicy mkServer policy = let s = statelessProcess { dispatchers = [ - -- note: state is passed here, as a 'stateless' server is a - -- server with state = () + -- note: state is passed here, as a 'stateless' process is + -- in fact process definition whose state is () handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" @@ -138,10 +138,9 @@ mkServer policy = } in do exitReason <- liftIO $ newEmptyMVar - pid <- spawnLocal $ (start () startup s) >>= stash exitReason + pid <- spawnLocal $ do + start () (statelessInit Infinity) s >>= stash exitReason return (pid, exitReason) - where startup :: InitHandler () () - startup _ = return $ InitOk () Infinity tests :: NT.Transport -> IO [Test] tests transport = do From 119a4e8260ac5390508a5e14d085d9971de4fb72 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:02:04 +0000 Subject: [PATCH 0749/2357] make it easier to initialise a 'stateless' GP - refactor the tests accordingly --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 9d15eab2..ff9d743b 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -21,6 +21,7 @@ module Control.Distributed.Process.Platform.GenProcess -- interaction with the process , start , statelessProcess + , statelessInit , call , safeCall , callAsync @@ -185,6 +186,9 @@ statelessProcess = Behaviour { , unhandledMessagePolicy = Terminate } +statelessInit :: Delay -> InitHandler () () +statelessInit d () = return $ InitOk () d + -- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b From 26d69297c86b7f669f945f9279f1efd91e6afdae Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:29:44 +0000 Subject: [PATCH 0750/2357] test GP dead letter policy --- tests/TestGenServer.hs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index d81a8137..20ecf2ef 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -14,7 +14,6 @@ import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer - import Data.Binary() import Data.Typeable() import MathsDemo @@ -102,10 +101,22 @@ testDropPolicy result = do Nothing -> stash result Nothing _ -> return () +testDeadLetterPolicy :: TestResult (Maybe (String, Int)) -> Process () +testDeadLetterPolicy result = do + self <- getSelfPid + (pid, _) <- mkServer (DeadLetter self) + + send pid ("UNSOLICITED_MAIL", 500 :: Int) + cast pid "stop" + + receiveTimeout + (after 5 Seconds) + [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result + waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) waitForExit exitReason = do - -- we *might* end up blocked here, so ensure the test suite doesn't jam! + -- we *might* end up blocked here, so ensure the test doesn't jam up! self <- getSelfPid tref <- killAfter (within 10 Seconds) self "testcast timed out" tr <- liftIO $ takeMVar exitReason @@ -173,6 +184,11 @@ tests transport = do (delayedAssertion "expected the server to ignore unhandled input and exit normally" localNode (Just TerminateNormal) testDropPolicy) + , testCase "unhandled input when policy = Drop" + (delayedAssertion + "expected the server to forward unhandled messages" + localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) + testDeadLetterPolicy) ] ] From fa91cc8e4a860d95502a4b81a6a0c8eda78433a5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:29:44 +0000 Subject: [PATCH 0751/2357] test GP dead letter policy --- tests/TestGenServer.hs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index d81a8137..20ecf2ef 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -14,7 +14,6 @@ import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer - import Data.Binary() import Data.Typeable() import MathsDemo @@ -102,10 +101,22 @@ testDropPolicy result = do Nothing -> stash result Nothing _ -> return () +testDeadLetterPolicy :: TestResult (Maybe (String, Int)) -> Process () +testDeadLetterPolicy result = do + self <- getSelfPid + (pid, _) <- mkServer (DeadLetter self) + + send pid ("UNSOLICITED_MAIL", 500 :: Int) + cast pid "stop" + + receiveTimeout + (after 5 Seconds) + [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result + waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) waitForExit exitReason = do - -- we *might* end up blocked here, so ensure the test suite doesn't jam! + -- we *might* end up blocked here, so ensure the test doesn't jam up! self <- getSelfPid tref <- killAfter (within 10 Seconds) self "testcast timed out" tr <- liftIO $ takeMVar exitReason @@ -173,6 +184,11 @@ tests transport = do (delayedAssertion "expected the server to ignore unhandled input and exit normally" localNode (Just TerminateNormal) testDropPolicy) + , testCase "unhandled input when policy = Drop" + (delayedAssertion + "expected the server to forward unhandled messages" + localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) + testDeadLetterPolicy) ] ] From d3505d0c0c0d91a91b7a629903851aa96e34eb9b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 15:29:44 +0000 Subject: [PATCH 0752/2357] test GP dead letter policy --- tests/TestGenServer.hs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index d81a8137..20ecf2ef 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -14,7 +14,6 @@ import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer - import Data.Binary() import Data.Typeable() import MathsDemo @@ -102,10 +101,22 @@ testDropPolicy result = do Nothing -> stash result Nothing _ -> return () +testDeadLetterPolicy :: TestResult (Maybe (String, Int)) -> Process () +testDeadLetterPolicy result = do + self <- getSelfPid + (pid, _) <- mkServer (DeadLetter self) + + send pid ("UNSOLICITED_MAIL", 500 :: Int) + cast pid "stop" + + receiveTimeout + (after 5 Seconds) + [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result + waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) waitForExit exitReason = do - -- we *might* end up blocked here, so ensure the test suite doesn't jam! + -- we *might* end up blocked here, so ensure the test doesn't jam up! self <- getSelfPid tref <- killAfter (within 10 Seconds) self "testcast timed out" tr <- liftIO $ takeMVar exitReason @@ -173,6 +184,11 @@ tests transport = do (delayedAssertion "expected the server to ignore unhandled input and exit normally" localNode (Just TerminateNormal) testDropPolicy) + , testCase "unhandled input when policy = Drop" + (delayedAssertion + "expected the server to forward unhandled messages" + localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) + testDeadLetterPolicy) ] ] From a41c056a3a7df798097dbf165dcaabe9dca9f04c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 17:21:54 +0000 Subject: [PATCH 0753/2357] whitespace --- tests/TestGenServer.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 20ecf2ef..0e1678e6 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -105,13 +105,13 @@ testDeadLetterPolicy :: TestResult (Maybe (String, Int)) -> Process () testDeadLetterPolicy result = do self <- getSelfPid (pid, _) <- mkServer (DeadLetter self) - + send pid ("UNSOLICITED_MAIL", 500 :: Int) cast pid "stop" - + receiveTimeout (after 5 Seconds) - [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result + [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) @@ -184,7 +184,7 @@ tests transport = do (delayedAssertion "expected the server to ignore unhandled input and exit normally" localNode (Just TerminateNormal) testDropPolicy) - , testCase "unhandled input when policy = Drop" + , testCase "unhandled input when policy = DeadLetter" (delayedAssertion "expected the server to forward unhandled messages" localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) From 3ca88746e5f02d076f4df8774db4878f8d7a306e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 17:21:54 +0000 Subject: [PATCH 0754/2357] whitespace --- tests/TestGenServer.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 20ecf2ef..0e1678e6 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -105,13 +105,13 @@ testDeadLetterPolicy :: TestResult (Maybe (String, Int)) -> Process () testDeadLetterPolicy result = do self <- getSelfPid (pid, _) <- mkServer (DeadLetter self) - + send pid ("UNSOLICITED_MAIL", 500 :: Int) cast pid "stop" - + receiveTimeout (after 5 Seconds) - [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result + [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) @@ -184,7 +184,7 @@ tests transport = do (delayedAssertion "expected the server to ignore unhandled input and exit normally" localNode (Just TerminateNormal) testDropPolicy) - , testCase "unhandled input when policy = Drop" + , testCase "unhandled input when policy = DeadLetter" (delayedAssertion "expected the server to forward unhandled messages" localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) From 56eced299e450ba0737a08b8fc5dca874cb66de5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 17:21:54 +0000 Subject: [PATCH 0755/2357] whitespace --- tests/TestGenServer.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 20ecf2ef..0e1678e6 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -105,13 +105,13 @@ testDeadLetterPolicy :: TestResult (Maybe (String, Int)) -> Process () testDeadLetterPolicy result = do self <- getSelfPid (pid, _) <- mkServer (DeadLetter self) - + send pid ("UNSOLICITED_MAIL", 500 :: Int) cast pid "stop" - + receiveTimeout (after 5 Seconds) - [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result + [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) @@ -184,7 +184,7 @@ tests transport = do (delayedAssertion "expected the server to ignore unhandled input and exit normally" localNode (Just TerminateNormal) testDropPolicy) - , testCase "unhandled input when policy = Drop" + , testCase "unhandled input when policy = DeadLetter" (delayedAssertion "expected the server to forward unhandled messages" localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) From f0ceb68fa9d3aa8b503fcdabb780187a44af4797 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 18:50:00 +0000 Subject: [PATCH 0756/2357] tidy up the tests - only test MathDemo divide by zero --- tests/TestGenServer.hs | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 0e1678e6..e8287bc2 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -24,32 +24,6 @@ import TestUtils import qualified Network.Transport as NT -type OpExpr = (Double -> Double -> Double) -data Op = Add OpExpr | Div OpExpr - -opAdd :: Op -opAdd = Add (+) - -opDiv :: Op -opDiv = Div (/) - -expr :: Op -> OpExpr -expr (Add f) = f -expr (Div f) = f - -mathTest :: String - -> String - -> LocalNode - -> ProcessId - -> Double - -> Double - -> Op - -> Test -mathTest t n l sid x y op = let fn = expr op in do - testCase t (delayedAssertion n l (x `fn` y) (proc sid x y op)) - where proc s x' y' (Add _) result = add s x' y' >>= stash result - proc s x' y' (Div _) result = divide s x' y' >>= stash result - testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -113,6 +87,11 @@ testDeadLetterPolicy result = do (after 5 Seconds) [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result +testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () +testDivByZero pid result = divide pid 125 0 >>= stash result + +-- utilities + waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) waitForExit exitReason = do @@ -156,7 +135,9 @@ mkServer policy = tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable - -- _ <- forkProcess localNode $ launchMathServer >>= stash mv + mpid <- newEmptyMVar + _ <- forkProcess localNode $ launchMathServer >>= stash mpid + pid <- takeMVar mpid return [ testGroup "basic server functionality" [ testCase "basic call with explicit server reply" @@ -190,7 +171,14 @@ tests transport = do localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) testDeadLetterPolicy) ] + , testGroup "math server examples" [ + testCase "error (Left) returned from x / 0" + (delayedAssertion + "expected the server to return DivByZero" + localNode (Left DivByZero) (testDivByZero pid)) + ] ] main :: IO () main = testMain $ tests + From eac328d0b382a95775aa7ce6286d70d4ec8fe5af Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 18:50:00 +0000 Subject: [PATCH 0757/2357] tidy up the tests - only test MathDemo divide by zero --- tests/TestGenServer.hs | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 0e1678e6..e8287bc2 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -24,32 +24,6 @@ import TestUtils import qualified Network.Transport as NT -type OpExpr = (Double -> Double -> Double) -data Op = Add OpExpr | Div OpExpr - -opAdd :: Op -opAdd = Add (+) - -opDiv :: Op -opDiv = Div (/) - -expr :: Op -> OpExpr -expr (Add f) = f -expr (Div f) = f - -mathTest :: String - -> String - -> LocalNode - -> ProcessId - -> Double - -> Double - -> Op - -> Test -mathTest t n l sid x y op = let fn = expr op in do - testCase t (delayedAssertion n l (x `fn` y) (proc sid x y op)) - where proc s x' y' (Add _) result = add s x' y' >>= stash result - proc s x' y' (Div _) result = divide s x' y' >>= stash result - testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -113,6 +87,11 @@ testDeadLetterPolicy result = do (after 5 Seconds) [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result +testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () +testDivByZero pid result = divide pid 125 0 >>= stash result + +-- utilities + waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) waitForExit exitReason = do @@ -156,7 +135,9 @@ mkServer policy = tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable - -- _ <- forkProcess localNode $ launchMathServer >>= stash mv + mpid <- newEmptyMVar + _ <- forkProcess localNode $ launchMathServer >>= stash mpid + pid <- takeMVar mpid return [ testGroup "basic server functionality" [ testCase "basic call with explicit server reply" @@ -190,7 +171,14 @@ tests transport = do localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) testDeadLetterPolicy) ] + , testGroup "math server examples" [ + testCase "error (Left) returned from x / 0" + (delayedAssertion + "expected the server to return DivByZero" + localNode (Left DivByZero) (testDivByZero pid)) + ] ] main :: IO () main = testMain $ tests + From 3bde092706860a66036e524d8b5722fecd5f1886 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 18:50:00 +0000 Subject: [PATCH 0758/2357] tidy up the tests - only test MathDemo divide by zero --- tests/MathsDemo.hs | 18 ++++++++++++------ tests/TestGenServer.hs | 42 +++++++++++++++--------------------------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index fb541f7d..ebfdfa2c 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -4,6 +4,7 @@ module MathsDemo ( add , divide , launchMathServer + , DivByZero(..) ) where import Control.Applicative @@ -16,7 +17,7 @@ import Data.Typeable (Typeable) data Add = Add Double Double deriving (Typeable) data Divide = Divide Double Double deriving (Typeable) -data DivByZero = DivByZero deriving (Typeable) +data DivByZero = DivByZero deriving (Typeable, Eq) instance Binary Add where put (Add x y) = put x >> put y @@ -35,7 +36,8 @@ instance Binary DivByZero where add :: ProcessId -> Double -> Double -> Process Double add sid x y = call sid (Add x y) -divide :: ProcessId -> Double -> Double -> Process Double +divide :: ProcessId -> Double -> Double + -> Process (Either DivByZero Double) divide sid x y = call sid (Divide x y ) launchMathServer :: Process ProcessId @@ -43,13 +45,17 @@ launchMathServer = let server = statelessProcess { dispatchers = [ handleCall_ (\(Add x y) -> return (x + y)) - , handleCallIf_ (\(Divide _ y) -> y /= 0) - (\(Divide x y) -> return (x / y)) - , handleCall_ (\(Divide _ _) -> return DivByZero) - + , handleCallIf_ (\(Divide _ y) -> y /= 0) handleDivide + , handleCall_ (\(Divide _ _) -> divByZero) , action (\("stop") -> stop_ TerminateNormal) ] } :: Behaviour () in spawnLocal $ start () startup server >> return () where startup :: InitHandler () () startup _ = return $ InitOk () Infinity + + handleDivide :: Divide -> Process (Either DivByZero Double) + handleDivide (Divide x y) = return $ Right $ x / y + + divByZero :: Process (Either DivByZero Double) + divByZero = return $ Left DivByZero \ No newline at end of file diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 0e1678e6..e8287bc2 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -24,32 +24,6 @@ import TestUtils import qualified Network.Transport as NT -type OpExpr = (Double -> Double -> Double) -data Op = Add OpExpr | Div OpExpr - -opAdd :: Op -opAdd = Add (+) - -opDiv :: Op -opDiv = Div (/) - -expr :: Op -> OpExpr -expr (Add f) = f -expr (Div f) = f - -mathTest :: String - -> String - -> LocalNode - -> ProcessId - -> Double - -> Double - -> Op - -> Test -mathTest t n l sid x y op = let fn = expr op in do - testCase t (delayedAssertion n l (x `fn` y) (proc sid x y op)) - where proc s x' y' (Add _) result = add s x' y' >>= stash result - proc s x' y' (Div _) result = divide s x' y' >>= stash result - testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -113,6 +87,11 @@ testDeadLetterPolicy result = do (after 5 Seconds) [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result +testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () +testDivByZero pid result = divide pid 125 0 >>= stash result + +-- utilities + waitForExit :: MVar (Either (InitResult ()) TerminateReason) -> Process (Maybe TerminateReason) waitForExit exitReason = do @@ -156,7 +135,9 @@ mkServer policy = tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable - -- _ <- forkProcess localNode $ launchMathServer >>= stash mv + mpid <- newEmptyMVar + _ <- forkProcess localNode $ launchMathServer >>= stash mpid + pid <- takeMVar mpid return [ testGroup "basic server functionality" [ testCase "basic call with explicit server reply" @@ -190,7 +171,14 @@ tests transport = do localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) testDeadLetterPolicy) ] + , testGroup "math server examples" [ + testCase "error (Left) returned from x / 0" + (delayedAssertion + "expected the server to return DivByZero" + localNode (Left DivByZero) (testDivByZero pid)) + ] ] main :: IO () main = testMain $ tests + From ba718306a930a5cd828c64e3186719cee489a319 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 19:17:40 +0000 Subject: [PATCH 0759/2357] whitespace --- src/Control/Distributed/Process/Platform/GenProcess.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index ff9d743b..bd1db08d 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -568,3 +568,4 @@ sendTo :: (Serializable m) => Recipient -> m -> Process () sendTo (SendToPid p) m = send p m sendTo (SendToService s) m = nsend s m sendTo (SendToRemoteService s n) m = nsendRemote n s m + From 7d421849f832ed56115bfc3095931ca11b276e37 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 19:17:40 +0000 Subject: [PATCH 0760/2357] whitespace --- src/Control/Distributed/Process/Platform/GenProcess.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index ff9d743b..bd1db08d 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -568,3 +568,4 @@ sendTo :: (Serializable m) => Recipient -> m -> Process () sendTo (SendToPid p) m = send p m sendTo (SendToService s) m = nsend s m sendTo (SendToRemoteService s n) m = nsendRemote n s m + From 27b997958c312219d902e87e26e29b95b09539e3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 19:18:17 +0000 Subject: [PATCH 0761/2357] test hibernation blocks incoming messages --- tests/TestGenServer.hs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e8287bc2..dba92937 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -87,6 +87,25 @@ testDeadLetterPolicy result = do (after 5 Seconds) [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result +testHibernation :: TestResult Bool -> Process () +testHibernation result = do + (pid, _) <- server + mref <- monitor pid + + cast pid ("hibernate", (within 3 Seconds)) + cast pid "stop" + + -- the process mustn't stop whilst it's supposed to be hibernating + r <- receiveTimeout (after 2 Seconds) [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\_ -> return ()) + ] + case r of + Nothing -> kill pid "done" >> stash result True + Just _ -> stash result False + +-- MathDemo test + testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () testDivByZero pid result = divide pid 125 0 >>= stash result @@ -121,7 +140,9 @@ mkServer policy = send pid "pong" >> continue s') , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) + , action (\("stop") -> stop_ TerminateNormal) + , action (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" @@ -170,6 +191,10 @@ tests transport = do "expected the server to forward unhandled messages" localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) testDeadLetterPolicy) + , testCase "incoming messages are ignored whilst hibernating" + (delayedAssertion + "expected the server to remain in hibernation" + localNode True testHibernation) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 515cc86e1813103335116864f71313e2b359062d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 19:18:17 +0000 Subject: [PATCH 0762/2357] test hibernation blocks incoming messages --- tests/TestGenServer.hs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e8287bc2..dba92937 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -87,6 +87,25 @@ testDeadLetterPolicy result = do (after 5 Seconds) [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result +testHibernation :: TestResult Bool -> Process () +testHibernation result = do + (pid, _) <- server + mref <- monitor pid + + cast pid ("hibernate", (within 3 Seconds)) + cast pid "stop" + + -- the process mustn't stop whilst it's supposed to be hibernating + r <- receiveTimeout (after 2 Seconds) [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\_ -> return ()) + ] + case r of + Nothing -> kill pid "done" >> stash result True + Just _ -> stash result False + +-- MathDemo test + testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () testDivByZero pid result = divide pid 125 0 >>= stash result @@ -121,7 +140,9 @@ mkServer policy = send pid "pong" >> continue s') , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) + , action (\("stop") -> stop_ TerminateNormal) + , action (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" @@ -170,6 +191,10 @@ tests transport = do "expected the server to forward unhandled messages" localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) testDeadLetterPolicy) + , testCase "incoming messages are ignored whilst hibernating" + (delayedAssertion + "expected the server to remain in hibernation" + localNode True testHibernation) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 45f16715bb433d09230215b23a40cdb102efc617 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 16 Jan 2013 19:18:17 +0000 Subject: [PATCH 0763/2357] test hibernation blocks incoming messages --- tests/TestGenServer.hs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index e8287bc2..dba92937 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -87,6 +87,25 @@ testDeadLetterPolicy result = do (after 5 Seconds) [ match (\m@(_ :: String, _ :: Int) -> return m) ] >>= stash result +testHibernation :: TestResult Bool -> Process () +testHibernation result = do + (pid, _) <- server + mref <- monitor pid + + cast pid ("hibernate", (within 3 Seconds)) + cast pid "stop" + + -- the process mustn't stop whilst it's supposed to be hibernating + r <- receiveTimeout (after 2 Seconds) [ + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\_ -> return ()) + ] + case r of + Nothing -> kill pid "done" >> stash result True + Just _ -> stash result False + +-- MathDemo test + testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () testDivByZero pid result = divide pid 125 0 >>= stash result @@ -121,7 +140,9 @@ mkServer policy = send pid "pong" >> continue s') , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") (\("timeout", Delay d) -> timeoutAfter_ d) + , action (\("stop") -> stop_ TerminateNormal) + , action (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" @@ -170,6 +191,10 @@ tests transport = do "expected the server to forward unhandled messages" localNode (Just ("UNSOLICITED_MAIL", 500 :: Int)) testDeadLetterPolicy) + , testCase "incoming messages are ignored whilst hibernating" + (delayedAssertion + "expected the server to remain in hibernation" + localNode True testHibernation) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 39a45c8887652d5050ca39e8158463af53eea160 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 09:42:28 +0000 Subject: [PATCH 0764/2357] rename Behaviour to ProcessDefinition refactor MathsDemo to use statelessInit add test for cancelling in-flight calls --- tests/TestGenServer.hs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index dba92937..62062a19 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -9,6 +9,8 @@ import Control.Concurrent.MVar import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Async.AsyncChan import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time @@ -104,6 +106,15 @@ testHibernation result = do Nothing -> kill pid "done" >> stash result True Just _ -> stash result False +testKillMidCall :: TestResult Bool -> Process () +testKillMidCall result = do + (pid, _) <- server + cast pid ("hibernate", (within 3 Seconds)) + callAsync pid "hello-world" >>= cancelWait >>= unpack result pid + where unpack :: TestResult Bool -> ProcessId -> AsyncResult () -> Process () + unpack res sid AsyncCancelled = kill sid "stop" >> stash res True + unpack res sid _ = kill sid "stop" >> stash res False + -- MathDemo test testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () @@ -195,6 +206,9 @@ tests transport = do (delayedAssertion "expected the server to remain in hibernation" localNode True testHibernation) + , testCase "long running call cancellation" + (delayedAssertion "expected to get AsyncCancelled" + localNode True testKillMidCall) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 3ee7977f79c24f8acc4b1440b9218741afeffd57 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 09:42:28 +0000 Subject: [PATCH 0765/2357] rename Behaviour to ProcessDefinition refactor MathsDemo to use statelessInit add test for cancelling in-flight calls --- .../Process/Platform/GenProcess.hs | 104 +++++++++++++----- tests/TestGenServer.hs | 14 +++ 2 files changed, 90 insertions(+), 28 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index bd1db08d..a33fbbff 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -2,8 +2,28 @@ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE ImpredicativeTypes #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.GenProcess +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a high(er) level API for building complex 'Process' +-- implementations by abstracting out the management of the process' mailbox, +-- reply/response handling, timeouts, process hiberation, error handling +-- and shutdown/stop procedures. Whilst this API is intended to provide a +-- higher level of abstraction that vanilla Cloud Haskell, it is intended +-- for use primarilly as a building block. +-- +-- [API Overview] +-- +-- +----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess ( -- exported data types @@ -17,7 +37,7 @@ module Control.Distributed.Process.Platform.GenProcess , TerminateHandler , TimeoutHandler , UnhandledMessagePolicy(..) - , Behaviour(..) + , ProcessDefinition(..) -- interaction with the process , start , statelessProcess @@ -63,7 +83,7 @@ import Control.Distributed.Process.Platform.Async.AsyncChan import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable, typeOf) import Prelude hiding (init) data ServerId = ServerId ProcessId | ServerName String @@ -124,6 +144,7 @@ data Dispatcher s = , dispatchIf :: s -> Message a -> Bool } +-- | data InfoDispatcher s = InfoDispatcher { dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) } @@ -141,16 +162,23 @@ instance MessageMatcher Dispatcher where -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the -- 'handleInfo' handlers. data UnhandledMessagePolicy = - Terminate - | DeadLetter ProcessId - | Drop - -data Behaviour s = Behaviour { - dispatchers :: [Dispatcher s] - , infoHandlers :: [InfoDispatcher s] - , timeoutHandler :: TimeoutHandler s - , terminateHandler :: TerminateHandler s -- ^ termination handler - , unhandledMessagePolicy :: UnhandledMessagePolicy + Terminate -- ^ stop immediately, giving @TerminateOther "UNHANDLED_INPUT"@ as the reason + | DeadLetter ProcessId -- ^ forward the message to the given recipient + | Drop -- ^ dequeue and then drop/ignore the message + +-- | Stores the functions that determine runtime behaviour in response to +-- incoming messages and a policy for responding to unhandled messages. +data ProcessDefinition s = ProcessDefinition { + dispatchers + :: [Dispatcher s] -- ^ functions that handle call/cast messages + , infoHandlers + :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages + , timeoutHandler + :: TimeoutHandler s -- ^ a function that handles timeouts + , terminateHandler + :: TerminateHandler s -- ^ a function that is run just before the process exits + , unhandledMessagePolicy + :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages } -------------------------------------------------------------------------------- @@ -166,7 +194,7 @@ data Behaviour s = Behaviour { -- @InitFail why@. start :: a -> InitHandler a s - -> Behaviour s + -> ProcessDefinition s -> Process (Either (InitResult s) TerminateReason) start args init behave = do ir <- init args @@ -177,8 +205,8 @@ start args init behave = do -- | A basic, stateless process definition, where the unhandled message policy -- is set to 'Terminate', the default timeout handlers does nothing (i.e., the -- same as calling @continue ()@ and the terminate handler is a no-op. -statelessProcess :: Behaviour () -statelessProcess = Behaviour { +statelessProcess :: ProcessDefinition () +statelessProcess = ProcessDefinition { dispatchers = [] , infoHandlers = [] , timeoutHandler = \s _ -> continue s @@ -192,11 +220,11 @@ statelessInit d () = return $ InitOk () d -- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b -call sid msg = do - r <- safeCall sid msg - case r of - Nothing -> fail "call failed" -- TODO: exit protocol !? - Just ar -> return ar +call sid msg = callAsync sid msg >>= wait >>= unpack + where unpack :: AsyncResult b -> Process b + unpack (AsyncDone r) = return r + unpack (AsyncFailed r) = die $ "CALL_FAILED;" ++ show r + unpack ar = die $ show (typeOf ar) -- | Safe version of 'call' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -218,6 +246,12 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate -- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 +-- | Performs a synchronous 'call' to the the given server address, however the +-- call is made /out of band/ and an async handle is returned immediately. This +-- can be passed to functions in the /Async/ API in order to obtain the result. +-- +-- see 'Control.Distributed.Process.Platform.Async' +-- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncChan b) callAsync sid msg = do @@ -238,7 +272,9 @@ callAsync sid msg = do Left err -> fail $ "call: remote process died: " ++ show err -- | Sends a /cast/ message to the server identified by 'ServerId'. The server --- will not send a response. +-- will not send a response. Like Cloud Haskell's 'send' primitive, cast is +-- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent +-- (e.g., dead) process will not generate any errors. cast :: forall a . (Serializable a) => ProcessId -> a -> Process () cast sid msg = send sid (CastMessage msg) @@ -250,8 +286,7 @@ cast sid msg = send sid (CastMessage msg) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r --- | Instructs the process to send a reply and evaluate the 'ProcessAction' --- thereafter. +-- | Instructs the process to send a reply /and/ evaluate the 'ProcessAction'. replyWith :: (Serializable m) => m -> ProcessAction s @@ -262,6 +297,8 @@ replyWith msg state = return $ ProcessReply msg state continue :: s -> Process (ProcessAction s) continue = return . ProcessContinue +-- | Version of 'continue' that can be used in handlers that ignore process state. +-- continue_ :: (s -> Process (ProcessAction s)) continue_ = return . ProcessContinue @@ -272,7 +309,10 @@ continue_ = return . ProcessContinue timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) timeoutAfter d s = return $ ProcessTimeout d s --- | Version of 'timeoutAfter' that ignores the process state. +-- | Version of 'timeoutAfter' that can be used in handlers that ignore process state. +-- +-- > action (\(TimeoutPlease duration) -> timeoutAfter_ duration) +-- timeoutAfter_ :: TimeInterval -> (s -> Process (ProcessAction s)) timeoutAfter_ d = return . ProcessTimeout d @@ -283,6 +323,10 @@ timeoutAfter_ d = return . ProcessTimeout d hibernate :: TimeInterval -> s -> Process (ProcessAction s) hibernate d s = return $ ProcessHibernate d s +-- | Version of 'hibernate' that can be used in handlers that ignore process state. +-- +-- > action (\(HibernatePlease delay) -> hibernate_ delay) +-- hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) hibernate_ d = return . ProcessHibernate d @@ -290,6 +334,10 @@ hibernate_ d = return . ProcessHibernate d stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r +-- | Version of 'stop' that can be used in handlers that ignore process state. +-- +-- > action (\ClientError -> stop_ TerminateNormal) +-- stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r @@ -416,7 +464,7 @@ handleCastIf_ cond h = DispatchIf { -- need only decide to stop, as the terminate handler can deal with state -- cleanup etc). For example: -- --- > action (\MyStopSignal -> stop_ TerminateNormal) +-- > action (\MyCriticalErrorSignal -> stop_ TerminateNormal) -- action :: forall s a . (Serializable a) => (a -> (s -> Process (ProcessAction s))) @@ -493,7 +541,7 @@ applyPolicy s p m = DeadLetter pid -> forward m pid >> continue s Drop -> continue s -initLoop :: Behaviour s -> s -> Delay -> Process TerminateReason +initLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason initLoop b s w = let p = unhandledMessagePolicy b t = timeoutHandler b diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index dba92937..62062a19 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -9,6 +9,8 @@ import Control.Concurrent.MVar import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Async.AsyncChan import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time @@ -104,6 +106,15 @@ testHibernation result = do Nothing -> kill pid "done" >> stash result True Just _ -> stash result False +testKillMidCall :: TestResult Bool -> Process () +testKillMidCall result = do + (pid, _) <- server + cast pid ("hibernate", (within 3 Seconds)) + callAsync pid "hello-world" >>= cancelWait >>= unpack result pid + where unpack :: TestResult Bool -> ProcessId -> AsyncResult () -> Process () + unpack res sid AsyncCancelled = kill sid "stop" >> stash res True + unpack res sid _ = kill sid "stop" >> stash res False + -- MathDemo test testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () @@ -195,6 +206,9 @@ tests transport = do (delayedAssertion "expected the server to remain in hibernation" localNode True testHibernation) + , testCase "long running call cancellation" + (delayedAssertion "expected to get AsyncCancelled" + localNode True testKillMidCall) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From b1198fcacef2fa1a62e5b57ff76db3e120808ba1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 09:42:28 +0000 Subject: [PATCH 0766/2357] rename Behaviour to ProcessDefinition refactor MathsDemo to use statelessInit add test for cancelling in-flight calls --- .../Process/Platform/GenProcess.hs | 104 +++++++++++++----- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index bd1db08d..a33fbbff 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -2,8 +2,28 @@ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE ImpredicativeTypes #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Control.Distributed.Process.Platform.GenProcess +-- Copyright : (c) Tim Watson 2012 +-- License : BSD3 (see the file LICENSE) +-- +-- Maintainer : Tim Watson +-- Stability : experimental +-- Portability : non-portable (requires concurrency) +-- +-- This module provides a high(er) level API for building complex 'Process' +-- implementations by abstracting out the management of the process' mailbox, +-- reply/response handling, timeouts, process hiberation, error handling +-- and shutdown/stop procedures. Whilst this API is intended to provide a +-- higher level of abstraction that vanilla Cloud Haskell, it is intended +-- for use primarilly as a building block. +-- +-- [API Overview] +-- +-- +----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess ( -- exported data types @@ -17,7 +37,7 @@ module Control.Distributed.Process.Platform.GenProcess , TerminateHandler , TimeoutHandler , UnhandledMessagePolicy(..) - , Behaviour(..) + , ProcessDefinition(..) -- interaction with the process , start , statelessProcess @@ -63,7 +83,7 @@ import Control.Distributed.Process.Platform.Async.AsyncChan import Data.Binary import Data.DeriveTH -import Data.Typeable (Typeable) +import Data.Typeable (Typeable, typeOf) import Prelude hiding (init) data ServerId = ServerId ProcessId | ServerName String @@ -124,6 +144,7 @@ data Dispatcher s = , dispatchIf :: s -> Message a -> Bool } +-- | data InfoDispatcher s = InfoDispatcher { dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) } @@ -141,16 +162,23 @@ instance MessageMatcher Dispatcher where -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the -- 'handleInfo' handlers. data UnhandledMessagePolicy = - Terminate - | DeadLetter ProcessId - | Drop - -data Behaviour s = Behaviour { - dispatchers :: [Dispatcher s] - , infoHandlers :: [InfoDispatcher s] - , timeoutHandler :: TimeoutHandler s - , terminateHandler :: TerminateHandler s -- ^ termination handler - , unhandledMessagePolicy :: UnhandledMessagePolicy + Terminate -- ^ stop immediately, giving @TerminateOther "UNHANDLED_INPUT"@ as the reason + | DeadLetter ProcessId -- ^ forward the message to the given recipient + | Drop -- ^ dequeue and then drop/ignore the message + +-- | Stores the functions that determine runtime behaviour in response to +-- incoming messages and a policy for responding to unhandled messages. +data ProcessDefinition s = ProcessDefinition { + dispatchers + :: [Dispatcher s] -- ^ functions that handle call/cast messages + , infoHandlers + :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages + , timeoutHandler + :: TimeoutHandler s -- ^ a function that handles timeouts + , terminateHandler + :: TerminateHandler s -- ^ a function that is run just before the process exits + , unhandledMessagePolicy + :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages } -------------------------------------------------------------------------------- @@ -166,7 +194,7 @@ data Behaviour s = Behaviour { -- @InitFail why@. start :: a -> InitHandler a s - -> Behaviour s + -> ProcessDefinition s -> Process (Either (InitResult s) TerminateReason) start args init behave = do ir <- init args @@ -177,8 +205,8 @@ start args init behave = do -- | A basic, stateless process definition, where the unhandled message policy -- is set to 'Terminate', the default timeout handlers does nothing (i.e., the -- same as calling @continue ()@ and the terminate handler is a no-op. -statelessProcess :: Behaviour () -statelessProcess = Behaviour { +statelessProcess :: ProcessDefinition () +statelessProcess = ProcessDefinition { dispatchers = [] , infoHandlers = [] , timeoutHandler = \s _ -> continue s @@ -192,11 +220,11 @@ statelessInit d () = return $ InitOk () d -- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b -call sid msg = do - r <- safeCall sid msg - case r of - Nothing -> fail "call failed" -- TODO: exit protocol !? - Just ar -> return ar +call sid msg = callAsync sid msg >>= wait >>= unpack + where unpack :: AsyncResult b -> Process b + unpack (AsyncDone r) = return r + unpack (AsyncFailed r) = die $ "CALL_FAILED;" ++ show r + unpack ar = die $ show (typeOf ar) -- | Safe version of 'call' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -218,6 +246,12 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate -- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 +-- | Performs a synchronous 'call' to the the given server address, however the +-- call is made /out of band/ and an async handle is returned immediately. This +-- can be passed to functions in the /Async/ API in order to obtain the result. +-- +-- see 'Control.Distributed.Process.Platform.Async' +-- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncChan b) callAsync sid msg = do @@ -238,7 +272,9 @@ callAsync sid msg = do Left err -> fail $ "call: remote process died: " ++ show err -- | Sends a /cast/ message to the server identified by 'ServerId'. The server --- will not send a response. +-- will not send a response. Like Cloud Haskell's 'send' primitive, cast is +-- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent +-- (e.g., dead) process will not generate any errors. cast :: forall a . (Serializable a) => ProcessId -> a -> Process () cast sid msg = send sid (CastMessage msg) @@ -250,8 +286,7 @@ cast sid msg = send sid (CastMessage msg) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r --- | Instructs the process to send a reply and evaluate the 'ProcessAction' --- thereafter. +-- | Instructs the process to send a reply /and/ evaluate the 'ProcessAction'. replyWith :: (Serializable m) => m -> ProcessAction s @@ -262,6 +297,8 @@ replyWith msg state = return $ ProcessReply msg state continue :: s -> Process (ProcessAction s) continue = return . ProcessContinue +-- | Version of 'continue' that can be used in handlers that ignore process state. +-- continue_ :: (s -> Process (ProcessAction s)) continue_ = return . ProcessContinue @@ -272,7 +309,10 @@ continue_ = return . ProcessContinue timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) timeoutAfter d s = return $ ProcessTimeout d s --- | Version of 'timeoutAfter' that ignores the process state. +-- | Version of 'timeoutAfter' that can be used in handlers that ignore process state. +-- +-- > action (\(TimeoutPlease duration) -> timeoutAfter_ duration) +-- timeoutAfter_ :: TimeInterval -> (s -> Process (ProcessAction s)) timeoutAfter_ d = return . ProcessTimeout d @@ -283,6 +323,10 @@ timeoutAfter_ d = return . ProcessTimeout d hibernate :: TimeInterval -> s -> Process (ProcessAction s) hibernate d s = return $ ProcessHibernate d s +-- | Version of 'hibernate' that can be used in handlers that ignore process state. +-- +-- > action (\(HibernatePlease delay) -> hibernate_ delay) +-- hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) hibernate_ d = return . ProcessHibernate d @@ -290,6 +334,10 @@ hibernate_ d = return . ProcessHibernate d stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r +-- | Version of 'stop' that can be used in handlers that ignore process state. +-- +-- > action (\ClientError -> stop_ TerminateNormal) +-- stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r @@ -416,7 +464,7 @@ handleCastIf_ cond h = DispatchIf { -- need only decide to stop, as the terminate handler can deal with state -- cleanup etc). For example: -- --- > action (\MyStopSignal -> stop_ TerminateNormal) +-- > action (\MyCriticalErrorSignal -> stop_ TerminateNormal) -- action :: forall s a . (Serializable a) => (a -> (s -> Process (ProcessAction s))) @@ -493,7 +541,7 @@ applyPolicy s p m = DeadLetter pid -> forward m pid >> continue s Drop -> continue s -initLoop :: Behaviour s -> s -> Delay -> Process TerminateReason +initLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason initLoop b s w = let p = unhandledMessagePolicy b t = timeoutHandler b From 7179f20d109a50b1b40d5e7975b4075f3a148c01 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 09:42:28 +0000 Subject: [PATCH 0767/2357] rename Behaviour to ProcessDefinition refactor MathsDemo to use statelessInit add test for cancelling in-flight calls --- tests/MathsDemo.hs | 9 +++------ tests/TestGenServer.hs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index ebfdfa2c..7f564a8f 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -49,12 +49,9 @@ launchMathServer = , handleCall_ (\(Divide _ _) -> divByZero) , action (\("stop") -> stop_ TerminateNormal) ] - } :: Behaviour () - in spawnLocal $ start () startup server >> return () - where startup :: InitHandler () () - startup _ = return $ InitOk () Infinity - - handleDivide :: Divide -> Process (Either DivByZero Double) + } + in spawnLocal $ start () (statelessInit Infinity) server >> return () + where handleDivide :: Divide -> Process (Either DivByZero Double) handleDivide (Divide x y) = return $ Right $ x / y divByZero :: Process (Either DivByZero Double) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index dba92937..62062a19 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -9,6 +9,8 @@ import Control.Concurrent.MVar import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Async.AsyncChan import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time @@ -104,6 +106,15 @@ testHibernation result = do Nothing -> kill pid "done" >> stash result True Just _ -> stash result False +testKillMidCall :: TestResult Bool -> Process () +testKillMidCall result = do + (pid, _) <- server + cast pid ("hibernate", (within 3 Seconds)) + callAsync pid "hello-world" >>= cancelWait >>= unpack result pid + where unpack :: TestResult Bool -> ProcessId -> AsyncResult () -> Process () + unpack res sid AsyncCancelled = kill sid "stop" >> stash res True + unpack res sid _ = kill sid "stop" >> stash res False + -- MathDemo test testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () @@ -195,6 +206,9 @@ tests transport = do (delayedAssertion "expected the server to remain in hibernation" localNode True testHibernation) + , testCase "long running call cancellation" + (delayedAssertion "expected to get AsyncCancelled" + localNode True testKillMidCall) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 0fa3e298f8dcd0bdf8b3455c304f7a669b03b410 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 09:44:08 +0000 Subject: [PATCH 0768/2357] DRY comments --- src/Control/Distributed/Process/Async.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index a7eda821..cad7ec19 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -42,7 +42,7 @@ import Data.DeriveTH import Data.Typeable (Typeable) -------------------------------------------------------------------------------- --- Cloud Haskell Async Process API -- +-- API -- -------------------------------------------------------------------------------- -- | A reference to an asynchronous action From e97521c7b5741946085b35cf91134abeb7746fa1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 13:43:20 +0000 Subject: [PATCH 0769/2357] use AsyncSTM to handle GenProcess 'call' --- src/Control/Distributed/Process/Platform/GenProcess.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index a33fbbff..c179cb17 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -78,8 +78,8 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time -import Control.Distributed.Process.Platform.Async (asyncDo) -import Control.Distributed.Process.Platform.Async.AsyncChan +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Async.AsyncSTM import Data.Binary import Data.DeriveTH @@ -253,7 +253,7 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- see 'Control.Distributed.Process.Platform.Async' -- callAsync :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (AsyncChan b) + => ProcessId -> a -> Process (AsyncSTM b) callAsync sid msg = do -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 From 6ab3aea3728ef688a7de775fa0c7543c87581272 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 13:43:20 +0000 Subject: [PATCH 0770/2357] use AsyncSTM to handle GenProcess 'call' --- src/Control/Distributed/Process/Platform/GenProcess.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index a33fbbff..c179cb17 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -78,8 +78,8 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Time -import Control.Distributed.Process.Platform.Async (asyncDo) -import Control.Distributed.Process.Platform.Async.AsyncChan +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Async.AsyncSTM import Data.Binary import Data.DeriveTH @@ -253,7 +253,7 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- see 'Control.Distributed.Process.Platform.Async' -- callAsync :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (AsyncChan b) + => ProcessId -> a -> Process (AsyncSTM b) callAsync sid msg = do -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 From dee1c8e42622e48bb306c0e7bf81a8cade58ab8c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 13:44:04 +0000 Subject: [PATCH 0771/2357] document the reason for using Async to implement GenProcess calls --- .../Process/Platform/GenProcess.hs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c179cb17..d5ccaf02 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -220,7 +220,7 @@ statelessInit d () = return $ InitOk () d -- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b -call sid msg = callAsync sid msg >>= wait >>= unpack +call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r unpack (AsyncFailed r) = die $ "CALL_FAILED;" ++ show r @@ -231,7 +231,7 @@ call sid msg = callAsync sid msg >>= wait >>= unpack -- 'call' instead. safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Maybe b) -safeCall s m = callAsync s m >>= wait >>= unpack +safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing @@ -257,7 +257,7 @@ callAsync :: forall a b . (Serializable a, Serializable b) callAsync sid msg = do -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 - async $ asyncDo $ do + async $ asyncDo $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) @@ -271,6 +271,25 @@ callAsync sid msg = do Right m -> return m Left err -> fail $ "call: remote process died: " ++ show err +-- note [call using async] +-- One problem with using plain expect/receive primitives to perform a +-- synchronous (round trip) call is that a reply matching the expected type +-- could come from anywhere! The Call.hs module uses a unique integer tag to +-- distinguish between inputs but this is easy to forge, as is tagging the +-- response with the sender's pid. +-- +-- The approach we take here is to rely on AsyncSTM to insulate us from +-- erroneous incoming messages without the need for tagging. The /async handle/ +-- returned uses an @STM (AsyncResult a)@ field to handle the response /and/ +-- the implementation spawns a new process to perform the actual call and +-- await the reply before atomically updating the result. Whilst in theory, +-- given a hypothetical 'listAllProcesses' primitive, it might be possible for +-- malacious code to obtain the ProcessId of the worker and send a false reply, +-- the likelihood of this is small enough that it seems reasonable to assume +-- we've solved the problem without the need for tags or globally unique +-- identifiers. +-- + -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent From e4aaa9b7bc1c6b7099f05586e9fc48a3c8af595f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 13:44:04 +0000 Subject: [PATCH 0772/2357] document the reason for using Async to implement GenProcess calls --- .../Process/Platform/GenProcess.hs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c179cb17..d5ccaf02 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -220,7 +220,7 @@ statelessInit d () = return $ InitOk () d -- | Make a syncrhonous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b -call sid msg = callAsync sid msg >>= wait >>= unpack +call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r unpack (AsyncFailed r) = die $ "CALL_FAILED;" ++ show r @@ -231,7 +231,7 @@ call sid msg = callAsync sid msg >>= wait >>= unpack -- 'call' instead. safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Maybe b) -safeCall s m = callAsync s m >>= wait >>= unpack +safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing @@ -257,7 +257,7 @@ callAsync :: forall a b . (Serializable a, Serializable b) callAsync sid msg = do -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 - async $ asyncDo $ do + async $ asyncDo $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) @@ -271,6 +271,25 @@ callAsync sid msg = do Right m -> return m Left err -> fail $ "call: remote process died: " ++ show err +-- note [call using async] +-- One problem with using plain expect/receive primitives to perform a +-- synchronous (round trip) call is that a reply matching the expected type +-- could come from anywhere! The Call.hs module uses a unique integer tag to +-- distinguish between inputs but this is easy to forge, as is tagging the +-- response with the sender's pid. +-- +-- The approach we take here is to rely on AsyncSTM to insulate us from +-- erroneous incoming messages without the need for tagging. The /async handle/ +-- returned uses an @STM (AsyncResult a)@ field to handle the response /and/ +-- the implementation spawns a new process to perform the actual call and +-- await the reply before atomically updating the result. Whilst in theory, +-- given a hypothetical 'listAllProcesses' primitive, it might be possible for +-- malacious code to obtain the ProcessId of the worker and send a false reply, +-- the likelihood of this is small enough that it seems reasonable to assume +-- we've solved the problem without the need for tags or globally unique +-- identifiers. +-- + -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent From 126aa9fc4d8aaee7300a261b5c5ff6bc87092255 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:02:33 +0000 Subject: [PATCH 0773/2357] oops - we're using AsyncSTM in the GenProcess tests, not AsyncChan --- tests/TestGenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 62062a19..c3a1d853 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -10,7 +10,7 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncChan +import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time From 7eb89d5105e121273f92114a36709e43cf880e06 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:02:33 +0000 Subject: [PATCH 0774/2357] oops - we're using AsyncSTM in the GenProcess tests, not AsyncChan --- tests/TestGenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 62062a19..c3a1d853 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -10,7 +10,7 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncChan +import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time From 08ddf94d5e8b74bf316d8ed6bd9fae21d4c352e0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:02:33 +0000 Subject: [PATCH 0775/2357] oops - we're using AsyncSTM in the GenProcess tests, not AsyncChan --- tests/TestGenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 62062a19..c3a1d853 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -10,7 +10,7 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncChan +import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time From 02bb3f5a6eb489151cab9f4d6991ec2802b0a2b1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:04:07 +0000 Subject: [PATCH 0776/2357] use 'die' in GenProcess 'callTimeout' and resolve a TODO --- src/Control/Distributed/Process/Platform/GenProcess.hs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index d5ccaf02..443c7812 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -235,16 +235,13 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing --- TODO: provide version of call that will throw/exit on failure - callTimeout :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) unpack Nothing = return Nothing unpack (Just (AsyncDone r)) = return $ Just r - unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate --- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 + unpack (Just other) = die other -- | Performs a synchronous 'call' to the the given server address, however the -- call is made /out of band/ and an async handle is returned immediately. This From c684e1044e104e4d522a5f1c3fadb4e4a03f2a3f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:04:07 +0000 Subject: [PATCH 0777/2357] use 'die' in GenProcess 'callTimeout' and resolve a TODO --- src/Control/Distributed/Process/Platform/GenProcess.hs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index d5ccaf02..443c7812 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -235,16 +235,13 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing --- TODO: provide version of call that will throw/exit on failure - callTimeout :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) unpack Nothing = return Nothing unpack (Just (AsyncDone r)) = return $ Just r - unpack (Just other) = getSelfPid >>= (flip exit) other >> terminate --- TODO: https://github.com/haskell-distributed/distributed-process/issues/110 + unpack (Just other) = die other -- | Performs a synchronous 'call' to the the given server address, however the -- call is made /out of band/ and an async handle is returned immediately. This From 515c256d09b1cae3e66b5d66b21a6fa17229cdf1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:05:43 +0000 Subject: [PATCH 0778/2357] document GenProcess callTimeout properly --- src/Control/Distributed/Process/Platform/GenProcess.hs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 443c7812..5a5c891a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -217,7 +217,7 @@ statelessProcess = ProcessDefinition { statelessInit :: Delay -> InitHandler () () statelessInit d () = return $ InitOk () d --- | Make a syncrhonous call - will block until a reply is received. +-- | Make a synchronous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] @@ -235,6 +235,13 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing +-- | Make a synchronous calls, but timeout and return @Nothing@ if the reply +-- is not received within the specified time interval. The reply may be sent +-- later on, or the call can be cancelled using the async @cancel@ API. +-- +-- If the 'AsyncResult' for the call indicates a failure (or cancellation) then +-- the calling process will exit, with the 'AsyncResult' given as the reason. +-- callTimeout :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack From 3113bf715068bc747e7ab8a05333a891df9f90c3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 17 Jan 2013 21:05:43 +0000 Subject: [PATCH 0779/2357] document GenProcess callTimeout properly --- src/Control/Distributed/Process/Platform/GenProcess.hs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 443c7812..5a5c891a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -217,7 +217,7 @@ statelessProcess = ProcessDefinition { statelessInit :: Delay -> InitHandler () () statelessInit d () = return $ InitOk () d --- | Make a syncrhonous call - will block until a reply is received. +-- | Make a synchronous call - will block until a reply is received. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] @@ -235,6 +235,13 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing +-- | Make a synchronous calls, but timeout and return @Nothing@ if the reply +-- is not received within the specified time interval. The reply may be sent +-- later on, or the call can be cancelled using the async @cancel@ API. +-- +-- If the 'AsyncResult' for the call indicates a failure (or cancellation) then +-- the calling process will exit, with the 'AsyncResult' given as the reason. +-- callTimeout :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack From cca97e7bb66a0c0565329ca44daa326c60f9d8de Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 18:03:33 +0000 Subject: [PATCH 0780/2357] use less TH; move TerminateReason; add tryCall and document GenProcess --- src/Control/Distributed/Process/Platform.hs | 11 +- .../Process/Platform/GenProcess.hs | 326 ++++++++++++------ .../Process/Platform/Internal/Primitives.hs | 2 +- .../Process/Platform/Internal/Types.hs | 52 ++- 4 files changed, 286 insertions(+), 105 deletions(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 78d97945..de17d4b2 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -20,14 +20,23 @@ module Control.Distributed.Process.Platform , newTagPool , getTag + -- common type + , TerminateReason + -- remote call table , __remoteTable ) where import Control.Distributed.Process import Control.Distributed.Process.Platform.Internal.Types + ( TerminateReason + , Tag + , TagPool + , newTagPool + , getTag + ) import Control.Distributed.Process.Platform.Internal.Primitives hiding (__remoteTable) -import qualified Control.Distributed.Process.Platform.Internal.Primitives (__remoteTable) +import qualified Control.Distributed.Process.Platform.Internal.Primitives (__remoteTable) -- remote table diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5a5c891a..21dd6ce5 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -22,54 +22,150 @@ -- -- [API Overview] -- --- +-- Once started, a generic process will consume messages from its mailbox and +-- pass them on to user defined /handlers/ based on the types received (mapped +-- to those accepted by the handlers). Each handler returns a 'ProcessAction', +-- which specifies how we should proceed. If none of the handlers is able to +-- process a message (because their types are incompatible) then the process +-- 'unhandledMessagePolicy' will be applied. +-- +-- The 'ProcessAction' type defines the ways in which a process can respond +-- to its inputs, either by continuing to wait for incoming messages (with an +-- optional timeout), sleeping (i.e., @threadDelay@ for a while) then waiting +-- or by stopping. If a handler returns @ProcessTimeout@ and no messages are +-- received within the time window, a specific 'timeoutHandler' is called, +-- which by default instructs the process to go back to waiting without a +-- timeout. +-- +-- To instruct a process to stop unless messages are received within a given +-- time window, a simple timeout handler would look something like this: +-- +-- > \_state _lastTimeWindow -> stop $ TerminateOther "timeout" +-- +-- Generic processes are defined by the 'ProcessDefinition' type, using record +-- syntax. The 'ProcessDefinition' fields contain handlers (or lists of them) +-- for specific tasks. The @timeoutHandler@ and @terminateHandler@ are called +-- when the process handles these respectively. The other handlers are split +-- into two groups: /dispatchers/ and /infoHandlers/. +-- +-- [The Cast/Call Protocol] +-- +-- Client interactions with the process will usually fall into one of two +-- categories. A 'cast' interaction involves the client sending a message +-- asynchronously and the server handling this input. No reply is sent to +-- the client. On the other hand, a 'call' interaction is a kind of /rpc/ +-- where the client sends a message and waits for a reply. +-- +-- The expressions given /dispatchers/ have to conform to the /cast|call/ +-- protocol. The details of this are, however, hidden from the user. A set +-- of API functions for creating /dispatchers/ are given instead, which +-- take expressions (i.e., a function or lambda expression) and create the +-- appropriate @Dispatcher@ for handling the cast (or call). +-- +-- The cast/call protocol handlers deal with /expected/ inputs. These form +-- the explicit public API for the process, and will usually be exposed by +-- providing module level functions that defer to the cast/call API. For +-- example: +-- +-- @ +-- add :: ProcessId -> Double -> Double -> Double +-- add pid x y = call pid (Add x y) +-- @ +-- +-- [Handling Info Messages] +-- +-- An explicit protocol for communicating with the process can be +-- configured using 'cast' and 'call', but it is not possible to prevent +-- other kinds of messages from being sent to the process mailbox. When +-- any message arrives for which there are no handlers able to process +-- its content, the 'UnhandledMessagePolicy' will be applied. Sometimes +-- it is desireable to process incoming messages which aren't part of the +-- protocol, rather than let the policy deal with them. This is particularly +-- true when incoming messages are important to the process, but their point +-- of origin is outside the developer's control. Handling /signals/ such as +-- 'ProcessMonitorNotification' is a typical example of this: +-- +-- > handleInfo_ (\(ProcessMonitorNotification _ _ r) -> say $ show r >> continue_) +-- +-- [Handling Process State] +-- +-- The 'ProcessDefinition' is parameterised by the type of state it maintains. +-- A process that has no state will have the type @ProcessDefinition ()@ and can +-- be bootstrapped by evaluating 'statelessProcess'. +-- +-- All call/cast handlers come in two flavours, those which take the process +-- state as an input and those which do not. Handlers that ignore the process +-- state have to return a section that takes the state and returns the required +-- action. Versions of the various action generating functions ending in an +-- underscore are provided to simplify this: +-- +-- @ +-- statelessProcess { +-- dispatchers = [ +-- handleCall_ (\(n :: Int) -> return (n * 2)) +-- , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") +-- (\("timeout", Delay d) -> timeoutAfter_ d) +-- ] +-- , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" +-- } +-- @ +-- +-- [Handling Errors] +-- +-- Error handling appears in several contexts and process definitions can +-- hook into these with relative ease. Only process failures as a result of +-- asynchronous exceptions are supported by the API, so /error/ handling +-- code is the responsibility of the programmer. +-- +-- The API provides several scopes for error handling. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess - ( -- exported data types + ( -- * Exported data types ServerId(..) , Recipient(..) , TerminateReason(..) , InitResult(..) - , ProcessAction + , ProcessAction(..) , ProcessReply , InitHandler , TerminateHandler , TimeoutHandler , UnhandledMessagePolicy(..) , ProcessDefinition(..) - -- interaction with the process + -- * Client interaction with the process , start , statelessProcess , statelessInit , call , safeCall + , tryCall , callAsync , callTimeout , cast - -- interaction inside the process + -- * Handler interaction inside the process , reply , replyWith , continue + , continue_ , timeoutAfter + , timeoutAfter_ , hibernate + , hibernate_ , stop - -- callback creation + , stop_ + -- * Handler callback creation , handleCall , handleCallIf , handleCast , handleCastIf , handleInfo - -- stateless handlers + -- * Stateless handlers , action , handleCall_ , handleCallIf_ , handleCast_ , handleCastIf_ - , continue_ - , timeoutAfter_ - , hibernate_ - , stop_ -- lower level handlers , handleDispatch ) where @@ -77,15 +173,21 @@ module Control.Distributed.Process.Platform.GenProcess import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.Async.AsyncSTM +import Control.Distributed.Process.Platform.Internal.Types + ( TerminateReason(..)) +import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable, typeOf) import Prelude hiding (init) +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + data ServerId = ServerId ProcessId | ServerName String data Recipient = @@ -105,36 +207,44 @@ data CallResponse a = CallResponse a deriving (Typeable) $(derive makeBinary ''CallResponse) --- | Terminate reason -data TerminateReason = - TerminateNormal - | TerminateShutdown - | TerminateOther String - deriving (Typeable, Eq, Show) -$(derive makeBinary ''TerminateReason) - --- | Initialization +-- | Return type for and 'InitHandler' expression. data InitResult s = - InitOk s Delay - | forall r. (Serializable r) => InitFail r - + InitOk s Delay {- + ^ denotes successful initialisation, initial state and timeout -} + | forall r. (Serializable r) + => InitFail r -- ^ denotes failed initialisation and the reason + +-- | The action taken by a process after a handler has run and its updated state. +-- See 'continue' +-- 'timeoutAfter' +-- 'hibernate' +-- 'stop' +-- data ProcessAction s = - ProcessContinue s - | ProcessTimeout TimeInterval s - | ProcessHibernate TimeInterval s - | ProcessStop TerminateReason - + ProcessContinue s -- ^ continue with (possibly new) state + | ProcessTimeout TimeInterval s -- ^ timeout if no messages are received + | ProcessHibernate TimeInterval s -- ^ hibernate for /delay/ + | ProcessStop TerminateReason -- ^ stop the process, giving @TerminateReason@ + +-- | Returned from handlers for the synchronous 'call' protocol, encapsulates +-- the reply data /and/ the action to take after sending the reply. A handler +-- can return @NoReply@ if they wish to ignore the call. data ProcessReply s a = ProcessReply a (ProcessAction s) | NoReply (ProcessAction s) -type InitHandler a s = a -> Process (InitResult s) -type TerminateHandler s = s -> TerminateReason -> Process () -type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) +-- | An expression used to initialise a process with its state. +type InitHandler a s = a -> Process (InitResult s) + +-- | An expression used to handle process termination. +type TerminateHandler s = s -> TerminateReason -> Process () + +-- | An expression used to handle process timeouts. +type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) -- dispatching to implementation callbacks --- | this type defines dispatch from abstract messages to a typed handler +-- | Provides dispatch from cast and call messages to a typed handler. data Dispatcher s = forall a . (Serializable a) => Dispatch { dispatch :: s -> Message a -> Process (ProcessAction s) @@ -144,19 +254,17 @@ data Dispatcher s = , dispatchIf :: s -> Message a -> Bool } --- | +-- | Provides dispatch for any input, returns 'Nothing' for unhandled messages. data InfoDispatcher s = InfoDispatcher { dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) } --- | matches messages of specific types using a dispatcher class MessageMatcher d where matchMessage :: UnhandledMessagePolicy -> s -> d s -> Match (ProcessAction s) --- | matches messages to a MessageDispatcher instance MessageMatcher Dispatcher where - matchMessage _ s (Dispatch d) = match (d s) - matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) + matchMessage _ s (Dispatch d) = match (d s) + matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) -- | Policy for handling unexpected messages, i.e., messages which are not -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the @@ -167,22 +275,17 @@ data UnhandledMessagePolicy = | Drop -- ^ dequeue and then drop/ignore the message -- | Stores the functions that determine runtime behaviour in response to --- incoming messages and a policy for responding to unhandled messages. +-- incoming messages and a policy for responding to unhandled messages. data ProcessDefinition s = ProcessDefinition { - dispatchers - :: [Dispatcher s] -- ^ functions that handle call/cast messages - , infoHandlers - :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages - , timeoutHandler - :: TimeoutHandler s -- ^ a function that handles timeouts - , terminateHandler - :: TerminateHandler s -- ^ a function that is run just before the process exits - , unhandledMessagePolicy - :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages + dispatchers :: [Dispatcher s] -- ^ functions that handle call/cast messages + , infoHandlers :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages + , timeoutHandler :: TimeoutHandler s -- ^ a function that handles timeouts + , terminateHandler :: TerminateHandler s -- ^ a function that is run just before the process exits + , unhandledMessagePolicy :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages } -------------------------------------------------------------------------------- --- Cloud Haskell Generic Process API -- +-- Client facing API functions -- -------------------------------------------------------------------------------- -- TODO: automatic registration @@ -214,31 +317,44 @@ statelessProcess = ProcessDefinition { , unhandledMessagePolicy = Terminate } +-- | A basic, state /unaware/ 'InitHandler' that can be used with +-- 'statelessProcess'. statelessInit :: Delay -> InitHandler () () statelessInit d () = return $ InitOk () d -- | Make a synchronous call - will block until a reply is received. +-- The calling process will exit with 'TerminateReason' if the calls fails. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ "CALL_FAILED;" ++ show r - unpack ar = die $ show (typeOf ar) + unpack (AsyncFailed r) = die $ TerminateOther $ "CALL_FAILED;" ++ show r + unpack ar = die $ TerminateOther $ show (typeOf ar) --- | Safe version of 'call' that returns 'Nothing' if the operation fails. If --- you need information about *why* a call has failed then you should use --- 'call' instead. +-- | Safe version of 'call' that returns information about the error +-- if the operation fails. If an error occurs then the explanation will be +-- will be stashed away as @(TerminateOther String)@. safeCall :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Maybe b) + => ProcessId -> a -> Process (Either TerminateReason b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] + where unpack (AsyncDone r) = return $ Right r + unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r + unpack ar = return $ Left $ TerminateOther $ show (typeOf ar) + +-- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If +-- you need information about *why* a call has failed then you should use +-- 'safeCall' or combine @catchExit@ and @call@ instead. +tryCall :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process (Maybe b) +tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing -- | Make a synchronous calls, but timeout and return @Nothing@ if the reply -- is not received within the specified time interval. The reply may be sent -- later on, or the call can be cancelled using the async @cancel@ API. --- +-- -- If the 'AsyncResult' for the call indicates a failure (or cancellation) then -- the calling process will exit, with the 'AsyncResult' given as the reason. -- @@ -254,8 +370,8 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- call is made /out of band/ and an async handle is returned immediately. This -- can be passed to functions in the /Async/ API in order to obtain the result. -- --- see 'Control.Distributed.Process.Platform.Async' --- +-- see "Control.Distributed.Process.Platform.Async" +-- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncSTM b) callAsync sid msg = do @@ -280,7 +396,7 @@ callAsync sid msg = do -- synchronous (round trip) call is that a reply matching the expected type -- could come from anywhere! The Call.hs module uses a unique integer tag to -- distinguish between inputs but this is easy to forge, as is tagging the --- response with the sender's pid. +-- response with the sender's pid. -- -- The approach we take here is to rely on AsyncSTM to insulate us from -- erroneous incoming messages without the need for tagging. The /async handle/ @@ -288,11 +404,10 @@ callAsync sid msg = do -- the implementation spawns a new process to perform the actual call and -- await the reply before atomically updating the result. Whilst in theory, -- given a hypothetical 'listAllProcesses' primitive, it might be possible for --- malacious code to obtain the ProcessId of the worker and send a false reply, +-- malacious code to obtain the ProcessId of the worker and send a false reply, -- the likelihood of this is small enough that it seems reasonable to assume -- we've solved the problem without the need for tags or globally unique -- identifiers. --- -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is @@ -302,9 +417,11 @@ cast :: forall a . (Serializable a) => ProcessId -> a -> Process () cast sid msg = send sid (CastMessage msg) --- Constructing Handlers from *ordinary* functions +-------------------------------------------------------------------------------- +-- Producing ProcessAction and ProcessReply from inside handler expressions -- +-------------------------------------------------------------------------------- --- | Instructs the process to send a reply and continue working. +-- | Instructs the process to send a reply and continue running. -- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r @@ -321,7 +438,7 @@ continue :: s -> Process (ProcessAction s) continue = return . ProcessContinue -- | Version of 'continue' that can be used in handlers that ignore process state. --- +-- continue_ :: (s -> Process (ProcessAction s)) continue_ = return . ProcessContinue @@ -348,7 +465,7 @@ hibernate d s = return $ ProcessHibernate d s -- | Version of 'hibernate' that can be used in handlers that ignore process state. -- --- > action (\(HibernatePlease delay) -> hibernate_ delay) +-- > action (\(HibernatePlease delay) -> hibernate_ delay) -- hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) hibernate_ d = return . ProcessHibernate d @@ -359,14 +476,18 @@ stop r = return $ ProcessStop r -- | Version of 'stop' that can be used in handlers that ignore process state. -- --- > action (\ClientError -> stop_ TerminateNormal) +-- > action (\ClientError -> stop_ TerminateNormal) -- stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r --- wrapping /normal/ functions with matching functionality +-------------------------------------------------------------------------------- +-- Wrapping handler expressions in Dispatcher and InfoDispatcher -- +-------------------------------------------------------------------------------- -- | Constructs a 'call' handler from a function in the 'Process' monad. +-- The handler expression returns the reply, and the action will be +-- set to 'continue'. -- -- > handleCall_ = handleCallIf_ (const True) -- @@ -380,12 +501,15 @@ handleCall_ = handleCallIf_ (const True) -- 'handleCallIf' and is therefore useful in a stateless server. Messages are -- only dispatched to the handler if the supplied condition evaluates to @True@ -- +-- See 'handleCall' handleCallIf_ :: (Serializable a, Serializable b) => (a -> Bool) -> (a -> Process b) -> Dispatcher s -handleCallIf_ cond handler = DispatchIf { - dispatch = doHandle handler +handleCallIf_ cond -- ^ predicate that must be satisfied for the handler to run + handler -- ^ a function from an input message to a reply + = DispatchIf { + dispatch = doHandle handler , dispatchIf = doCheckCall cond } where doHandle :: (Serializable a, Serializable b) @@ -421,8 +545,10 @@ handleCallIf :: (Serializable a, Serializable b) => (a -> Bool) -> (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCallIf cond handler = DispatchIf { - dispatch = doHandle handler +handleCallIf cond -- ^ predicate that must be satisfied for the handler to run + handler -- ^ a reply yielding function over the process state and input message + = DispatchIf { + dispatch = doHandle handler , dispatchIf = doCheckCall cond } where doHandle :: (Serializable a, Serializable b) @@ -458,7 +584,9 @@ handleCastIf :: (Serializable a) => (a -> Bool) -> (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCastIf cond h = DispatchIf { +handleCastIf cond -- ^ predicate that must be satisfied for the handler to run + h -- ^ an action yielding function over the process state and input message + = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) , dispatchIf = \_ (CastMessage msg) -> cond msg } @@ -475,7 +603,9 @@ handleCastIf_ :: (Serializable a) => (a -> Bool) -> (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -handleCastIf_ cond h = DispatchIf { +handleCastIf_ cond -- ^ predicate that must be satisfied for the handler to run + h -- ^ a function from the input message to a /stateless action/, cf 'continue_' + = DispatchIf { dispatch = (\s (CastMessage p) -> h p $ s) , dispatchIf = \_ (CastMessage msg) -> cond msg } @@ -487,17 +617,18 @@ handleCastIf_ cond h = DispatchIf { -- need only decide to stop, as the terminate handler can deal with state -- cleanup etc). For example: -- --- > action (\MyCriticalErrorSignal -> stop_ TerminateNormal) +-- @action (\MyCriticalErrorSignal -> stop_ TerminateNormal)@ -- action :: forall s a . (Serializable a) => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -action h = handleDispatch perform +action h -- ^ a function from the input message to a /stateless action/, cf 'continue_' + = handleDispatch perform where perform :: (s -> a -> Process (ProcessAction s)) perform s a = let f = h a in f s -- | Constructs a handler for both /call/ and /cast/ messages. --- > handleDispatch = handleDispatchIf (const True) +-- @handleDispatch = handleDispatchIf (const True)@ -- handleDispatch :: (Serializable a) => (s -> a -> Process (ProcessAction s)) @@ -552,7 +683,9 @@ doCheckCall :: forall s a. (Serializable a) doCheckCall c _ (CallMessage m _) = c m doCheckCall _ _ _ = False --- Process Implementation +-------------------------------------------------------------------------------- +-- Process Implementation -- +-------------------------------------------------------------------------------- applyPolicy :: s -> UnhandledMessagePolicy @@ -567,10 +700,9 @@ applyPolicy s p m = initLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason initLoop b s w = let p = unhandledMessagePolicy b - t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) ms' = ms ++ addInfoAux p s (infoHandlers b) - in loop ms' t s w + in loop ms' b s w where addInfoAux :: UnhandledMessagePolicy -> s @@ -600,36 +732,37 @@ initLoop b s w = Just act -> return act loop :: [Match (ProcessAction s)] - -> TimeoutHandler s + -> ProcessDefinition s -> s -> Delay -> Process TerminateReason -loop ms h s t = do - ac <- processReceive ms h s t +loop ms def state t = + let handleTimeout = timeoutHandler def + handleStop = terminateHandler def + in do + ac <- processReceive ms handleTimeout state t case ac of - (ProcessContinue s') -> loop ms h s' t - (ProcessTimeout t' s') -> loop ms h s' (Delay t') - (ProcessHibernate d' s') -> block d' >> loop ms h s' t - (ProcessStop r) -> return (r :: TerminateReason) + (ProcessContinue s') -> loop ms def s' t + (ProcessTimeout t' s') -> loop ms def s' (Delay t') + (ProcessHibernate d' s') -> block d' >> loop ms def s' t + (ProcessStop r) -> handleStop state r >> return (r :: TerminateReason) where block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) processReceive :: [Match (ProcessAction s)] - -> TimeoutHandler s - -> s - -> Delay - -> Process (ProcessAction s) -processReceive ms h s t = do - next <- recv ms t + -> TimeoutHandler s -> s + -> Delay -> Process (ProcessAction s) +processReceive ms handleTimeout state d = do + next <- recv ms d case next of - Nothing -> h s t + Nothing -> handleTimeout state d Just pa -> return pa where recv :: [Match (ProcessAction s)] -> Delay -> Process (Maybe (ProcessAction s)) - recv matches d = - case d of + recv matches d' = + case d' of Infinity -> receiveWait matches >>= return . Just Delay t' -> receiveTimeout (asTimeout t') matches @@ -639,4 +772,3 @@ sendTo :: (Serializable m) => Recipient -> m -> Process () sendTo (SendToPid p) m = send p m sendTo (SendToService s) m = nsend s m sendTo (SendToRemoteService s n) m = nsendRemote n s m - diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index 0b7e22ff..d96a45c5 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -86,7 +86,7 @@ linkOnFailure them = do DiedNormal -> return () _ -> liftIO $ throwTo tid (ProcessLinkException us reason) --- | Returns the pid of the process that has been registered +-- | Returns the pid of the process that has been registered -- under the given name. This refers to a local, per-node registration, -- not @global@ registration. If that name is unregistered, a process -- is started. This is a handy way to start per-node named servers. diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index 94780ddc..f33446e8 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -1,5 +1,5 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveDataTypeable #-} + -- | Types used throughout the Cloud Haskell framework -- module Control.Distributed.Process.Platform.Internal.Types @@ -10,14 +10,20 @@ module Control.Distributed.Process.Platform.Internal.Types , RegisterSelf(..) , CancelWait(..) , Channel + , TerminateReason(..) ) where -import Control.Distributed.Process +import Control.Applicative ((<$>)) import Control.Concurrent.MVar (MVar, newMVar, modifyMVar) +import Control.Distributed.Process +import Control.Distributed.Process.Serializable () import Data.Binary -import Data.DeriveTH + ( Binary(put, get) + , putWord8 + , getWord8) import Data.Typeable (Typeable) +-- | Simple representation of a channel. type Channel a = (SendPort a, ReceivePort a) -- | Used internally in whereisOrStart. Send as (RegisterSelf,ProcessId). @@ -46,6 +52,40 @@ newTagPool = liftIO $ newMVar 0 getTag :: TagPool -> Process Tag getTag tp = liftIO $ modifyMVar tp (\tag -> return (tag+1,tag)) +-- | Wait cancellation message. data CancelWait = CancelWait - deriving (Typeable) -$(derive makeBinary ''CancelWait) + deriving (Eq, Show, Typeable) + +instance Binary CancelWait where + put CancelWait = return () + get = return CancelWait + +-- | A ubiquitous /shutdown signal/ that can be used +-- to maintain a consistent shutdown/stop protocol for +-- any process that wishes to handle it. +data Shutdown = Shutdown + deriving (Typeable, Show, Eq) + +instance Binary Shutdown where + get = return Shutdown + put _ = return () + +-- | Provides a /reason/ for process termination. +data TerminateReason = + TerminateNormal -- ^ indicates normal exit + | TerminateShutdown -- ^ normal response to a 'Shutdown' + | TerminateOther String -- ^ abnormal (error) shutdown + deriving (Typeable, Eq, Show) + +instance Binary TerminateReason where + put TerminateNormal = putWord8 1 + put TerminateShutdown = putWord8 2 + put (TerminateOther s) = putWord8 3 >> put s + + get = do + header <- getWord8 + case header of + 1 -> return TerminateNormal + 2 -> return TerminateShutdown + 3 -> TerminateOther <$> get + _ -> fail "TerminateReason.get: invalid" From f401c9b90f375c08362b9b3ffc220b1ba33afd74 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 18:03:33 +0000 Subject: [PATCH 0781/2357] use less TH; move TerminateReason; add tryCall and document GenProcess --- .../Process/Platform/GenProcess.hs | 326 ++++++++++++------ 1 file changed, 229 insertions(+), 97 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5a5c891a..21dd6ce5 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -22,54 +22,150 @@ -- -- [API Overview] -- --- +-- Once started, a generic process will consume messages from its mailbox and +-- pass them on to user defined /handlers/ based on the types received (mapped +-- to those accepted by the handlers). Each handler returns a 'ProcessAction', +-- which specifies how we should proceed. If none of the handlers is able to +-- process a message (because their types are incompatible) then the process +-- 'unhandledMessagePolicy' will be applied. +-- +-- The 'ProcessAction' type defines the ways in which a process can respond +-- to its inputs, either by continuing to wait for incoming messages (with an +-- optional timeout), sleeping (i.e., @threadDelay@ for a while) then waiting +-- or by stopping. If a handler returns @ProcessTimeout@ and no messages are +-- received within the time window, a specific 'timeoutHandler' is called, +-- which by default instructs the process to go back to waiting without a +-- timeout. +-- +-- To instruct a process to stop unless messages are received within a given +-- time window, a simple timeout handler would look something like this: +-- +-- > \_state _lastTimeWindow -> stop $ TerminateOther "timeout" +-- +-- Generic processes are defined by the 'ProcessDefinition' type, using record +-- syntax. The 'ProcessDefinition' fields contain handlers (or lists of them) +-- for specific tasks. The @timeoutHandler@ and @terminateHandler@ are called +-- when the process handles these respectively. The other handlers are split +-- into two groups: /dispatchers/ and /infoHandlers/. +-- +-- [The Cast/Call Protocol] +-- +-- Client interactions with the process will usually fall into one of two +-- categories. A 'cast' interaction involves the client sending a message +-- asynchronously and the server handling this input. No reply is sent to +-- the client. On the other hand, a 'call' interaction is a kind of /rpc/ +-- where the client sends a message and waits for a reply. +-- +-- The expressions given /dispatchers/ have to conform to the /cast|call/ +-- protocol. The details of this are, however, hidden from the user. A set +-- of API functions for creating /dispatchers/ are given instead, which +-- take expressions (i.e., a function or lambda expression) and create the +-- appropriate @Dispatcher@ for handling the cast (or call). +-- +-- The cast/call protocol handlers deal with /expected/ inputs. These form +-- the explicit public API for the process, and will usually be exposed by +-- providing module level functions that defer to the cast/call API. For +-- example: +-- +-- @ +-- add :: ProcessId -> Double -> Double -> Double +-- add pid x y = call pid (Add x y) +-- @ +-- +-- [Handling Info Messages] +-- +-- An explicit protocol for communicating with the process can be +-- configured using 'cast' and 'call', but it is not possible to prevent +-- other kinds of messages from being sent to the process mailbox. When +-- any message arrives for which there are no handlers able to process +-- its content, the 'UnhandledMessagePolicy' will be applied. Sometimes +-- it is desireable to process incoming messages which aren't part of the +-- protocol, rather than let the policy deal with them. This is particularly +-- true when incoming messages are important to the process, but their point +-- of origin is outside the developer's control. Handling /signals/ such as +-- 'ProcessMonitorNotification' is a typical example of this: +-- +-- > handleInfo_ (\(ProcessMonitorNotification _ _ r) -> say $ show r >> continue_) +-- +-- [Handling Process State] +-- +-- The 'ProcessDefinition' is parameterised by the type of state it maintains. +-- A process that has no state will have the type @ProcessDefinition ()@ and can +-- be bootstrapped by evaluating 'statelessProcess'. +-- +-- All call/cast handlers come in two flavours, those which take the process +-- state as an input and those which do not. Handlers that ignore the process +-- state have to return a section that takes the state and returns the required +-- action. Versions of the various action generating functions ending in an +-- underscore are provided to simplify this: +-- +-- @ +-- statelessProcess { +-- dispatchers = [ +-- handleCall_ (\(n :: Int) -> return (n * 2)) +-- , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") +-- (\("timeout", Delay d) -> timeoutAfter_ d) +-- ] +-- , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" +-- } +-- @ +-- +-- [Handling Errors] +-- +-- Error handling appears in several contexts and process definitions can +-- hook into these with relative ease. Only process failures as a result of +-- asynchronous exceptions are supported by the API, so /error/ handling +-- code is the responsibility of the programmer. +-- +-- The API provides several scopes for error handling. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess - ( -- exported data types + ( -- * Exported data types ServerId(..) , Recipient(..) , TerminateReason(..) , InitResult(..) - , ProcessAction + , ProcessAction(..) , ProcessReply , InitHandler , TerminateHandler , TimeoutHandler , UnhandledMessagePolicy(..) , ProcessDefinition(..) - -- interaction with the process + -- * Client interaction with the process , start , statelessProcess , statelessInit , call , safeCall + , tryCall , callAsync , callTimeout , cast - -- interaction inside the process + -- * Handler interaction inside the process , reply , replyWith , continue + , continue_ , timeoutAfter + , timeoutAfter_ , hibernate + , hibernate_ , stop - -- callback creation + , stop_ + -- * Handler callback creation , handleCall , handleCallIf , handleCast , handleCastIf , handleInfo - -- stateless handlers + -- * Stateless handlers , action , handleCall_ , handleCallIf_ , handleCast_ , handleCastIf_ - , continue_ - , timeoutAfter_ - , hibernate_ - , stop_ -- lower level handlers , handleDispatch ) where @@ -77,15 +173,21 @@ module Control.Distributed.Process.Platform.GenProcess import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.Async.AsyncSTM +import Control.Distributed.Process.Platform.Internal.Types + ( TerminateReason(..)) +import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable, typeOf) import Prelude hiding (init) +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + data ServerId = ServerId ProcessId | ServerName String data Recipient = @@ -105,36 +207,44 @@ data CallResponse a = CallResponse a deriving (Typeable) $(derive makeBinary ''CallResponse) --- | Terminate reason -data TerminateReason = - TerminateNormal - | TerminateShutdown - | TerminateOther String - deriving (Typeable, Eq, Show) -$(derive makeBinary ''TerminateReason) - --- | Initialization +-- | Return type for and 'InitHandler' expression. data InitResult s = - InitOk s Delay - | forall r. (Serializable r) => InitFail r - + InitOk s Delay {- + ^ denotes successful initialisation, initial state and timeout -} + | forall r. (Serializable r) + => InitFail r -- ^ denotes failed initialisation and the reason + +-- | The action taken by a process after a handler has run and its updated state. +-- See 'continue' +-- 'timeoutAfter' +-- 'hibernate' +-- 'stop' +-- data ProcessAction s = - ProcessContinue s - | ProcessTimeout TimeInterval s - | ProcessHibernate TimeInterval s - | ProcessStop TerminateReason - + ProcessContinue s -- ^ continue with (possibly new) state + | ProcessTimeout TimeInterval s -- ^ timeout if no messages are received + | ProcessHibernate TimeInterval s -- ^ hibernate for /delay/ + | ProcessStop TerminateReason -- ^ stop the process, giving @TerminateReason@ + +-- | Returned from handlers for the synchronous 'call' protocol, encapsulates +-- the reply data /and/ the action to take after sending the reply. A handler +-- can return @NoReply@ if they wish to ignore the call. data ProcessReply s a = ProcessReply a (ProcessAction s) | NoReply (ProcessAction s) -type InitHandler a s = a -> Process (InitResult s) -type TerminateHandler s = s -> TerminateReason -> Process () -type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) +-- | An expression used to initialise a process with its state. +type InitHandler a s = a -> Process (InitResult s) + +-- | An expression used to handle process termination. +type TerminateHandler s = s -> TerminateReason -> Process () + +-- | An expression used to handle process timeouts. +type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) -- dispatching to implementation callbacks --- | this type defines dispatch from abstract messages to a typed handler +-- | Provides dispatch from cast and call messages to a typed handler. data Dispatcher s = forall a . (Serializable a) => Dispatch { dispatch :: s -> Message a -> Process (ProcessAction s) @@ -144,19 +254,17 @@ data Dispatcher s = , dispatchIf :: s -> Message a -> Bool } --- | +-- | Provides dispatch for any input, returns 'Nothing' for unhandled messages. data InfoDispatcher s = InfoDispatcher { dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) } --- | matches messages of specific types using a dispatcher class MessageMatcher d where matchMessage :: UnhandledMessagePolicy -> s -> d s -> Match (ProcessAction s) --- | matches messages to a MessageDispatcher instance MessageMatcher Dispatcher where - matchMessage _ s (Dispatch d) = match (d s) - matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) + matchMessage _ s (Dispatch d) = match (d s) + matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) -- | Policy for handling unexpected messages, i.e., messages which are not -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the @@ -167,22 +275,17 @@ data UnhandledMessagePolicy = | Drop -- ^ dequeue and then drop/ignore the message -- | Stores the functions that determine runtime behaviour in response to --- incoming messages and a policy for responding to unhandled messages. +-- incoming messages and a policy for responding to unhandled messages. data ProcessDefinition s = ProcessDefinition { - dispatchers - :: [Dispatcher s] -- ^ functions that handle call/cast messages - , infoHandlers - :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages - , timeoutHandler - :: TimeoutHandler s -- ^ a function that handles timeouts - , terminateHandler - :: TerminateHandler s -- ^ a function that is run just before the process exits - , unhandledMessagePolicy - :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages + dispatchers :: [Dispatcher s] -- ^ functions that handle call/cast messages + , infoHandlers :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages + , timeoutHandler :: TimeoutHandler s -- ^ a function that handles timeouts + , terminateHandler :: TerminateHandler s -- ^ a function that is run just before the process exits + , unhandledMessagePolicy :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages } -------------------------------------------------------------------------------- --- Cloud Haskell Generic Process API -- +-- Client facing API functions -- -------------------------------------------------------------------------------- -- TODO: automatic registration @@ -214,31 +317,44 @@ statelessProcess = ProcessDefinition { , unhandledMessagePolicy = Terminate } +-- | A basic, state /unaware/ 'InitHandler' that can be used with +-- 'statelessProcess'. statelessInit :: Delay -> InitHandler () () statelessInit d () = return $ InitOk () d -- | Make a synchronous call - will block until a reply is received. +-- The calling process will exit with 'TerminateReason' if the calls fails. call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ "CALL_FAILED;" ++ show r - unpack ar = die $ show (typeOf ar) + unpack (AsyncFailed r) = die $ TerminateOther $ "CALL_FAILED;" ++ show r + unpack ar = die $ TerminateOther $ show (typeOf ar) --- | Safe version of 'call' that returns 'Nothing' if the operation fails. If --- you need information about *why* a call has failed then you should use --- 'call' instead. +-- | Safe version of 'call' that returns information about the error +-- if the operation fails. If an error occurs then the explanation will be +-- will be stashed away as @(TerminateOther String)@. safeCall :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Maybe b) + => ProcessId -> a -> Process (Either TerminateReason b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] + where unpack (AsyncDone r) = return $ Right r + unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r + unpack ar = return $ Left $ TerminateOther $ show (typeOf ar) + +-- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If +-- you need information about *why* a call has failed then you should use +-- 'safeCall' or combine @catchExit@ and @call@ instead. +tryCall :: forall a b . (Serializable a, Serializable b) + => ProcessId -> a -> Process (Maybe b) +tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing -- | Make a synchronous calls, but timeout and return @Nothing@ if the reply -- is not received within the specified time interval. The reply may be sent -- later on, or the call can be cancelled using the async @cancel@ API. --- +-- -- If the 'AsyncResult' for the call indicates a failure (or cancellation) then -- the calling process will exit, with the 'AsyncResult' given as the reason. -- @@ -254,8 +370,8 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- call is made /out of band/ and an async handle is returned immediately. This -- can be passed to functions in the /Async/ API in order to obtain the result. -- --- see 'Control.Distributed.Process.Platform.Async' --- +-- see "Control.Distributed.Process.Platform.Async" +-- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (AsyncSTM b) callAsync sid msg = do @@ -280,7 +396,7 @@ callAsync sid msg = do -- synchronous (round trip) call is that a reply matching the expected type -- could come from anywhere! The Call.hs module uses a unique integer tag to -- distinguish between inputs but this is easy to forge, as is tagging the --- response with the sender's pid. +-- response with the sender's pid. -- -- The approach we take here is to rely on AsyncSTM to insulate us from -- erroneous incoming messages without the need for tagging. The /async handle/ @@ -288,11 +404,10 @@ callAsync sid msg = do -- the implementation spawns a new process to perform the actual call and -- await the reply before atomically updating the result. Whilst in theory, -- given a hypothetical 'listAllProcesses' primitive, it might be possible for --- malacious code to obtain the ProcessId of the worker and send a false reply, +-- malacious code to obtain the ProcessId of the worker and send a false reply, -- the likelihood of this is small enough that it seems reasonable to assume -- we've solved the problem without the need for tags or globally unique -- identifiers. --- -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is @@ -302,9 +417,11 @@ cast :: forall a . (Serializable a) => ProcessId -> a -> Process () cast sid msg = send sid (CastMessage msg) --- Constructing Handlers from *ordinary* functions +-------------------------------------------------------------------------------- +-- Producing ProcessAction and ProcessReply from inside handler expressions -- +-------------------------------------------------------------------------------- --- | Instructs the process to send a reply and continue working. +-- | Instructs the process to send a reply and continue running. -- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r @@ -321,7 +438,7 @@ continue :: s -> Process (ProcessAction s) continue = return . ProcessContinue -- | Version of 'continue' that can be used in handlers that ignore process state. --- +-- continue_ :: (s -> Process (ProcessAction s)) continue_ = return . ProcessContinue @@ -348,7 +465,7 @@ hibernate d s = return $ ProcessHibernate d s -- | Version of 'hibernate' that can be used in handlers that ignore process state. -- --- > action (\(HibernatePlease delay) -> hibernate_ delay) +-- > action (\(HibernatePlease delay) -> hibernate_ delay) -- hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) hibernate_ d = return . ProcessHibernate d @@ -359,14 +476,18 @@ stop r = return $ ProcessStop r -- | Version of 'stop' that can be used in handlers that ignore process state. -- --- > action (\ClientError -> stop_ TerminateNormal) +-- > action (\ClientError -> stop_ TerminateNormal) -- stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r --- wrapping /normal/ functions with matching functionality +-------------------------------------------------------------------------------- +-- Wrapping handler expressions in Dispatcher and InfoDispatcher -- +-------------------------------------------------------------------------------- -- | Constructs a 'call' handler from a function in the 'Process' monad. +-- The handler expression returns the reply, and the action will be +-- set to 'continue'. -- -- > handleCall_ = handleCallIf_ (const True) -- @@ -380,12 +501,15 @@ handleCall_ = handleCallIf_ (const True) -- 'handleCallIf' and is therefore useful in a stateless server. Messages are -- only dispatched to the handler if the supplied condition evaluates to @True@ -- +-- See 'handleCall' handleCallIf_ :: (Serializable a, Serializable b) => (a -> Bool) -> (a -> Process b) -> Dispatcher s -handleCallIf_ cond handler = DispatchIf { - dispatch = doHandle handler +handleCallIf_ cond -- ^ predicate that must be satisfied for the handler to run + handler -- ^ a function from an input message to a reply + = DispatchIf { + dispatch = doHandle handler , dispatchIf = doCheckCall cond } where doHandle :: (Serializable a, Serializable b) @@ -421,8 +545,10 @@ handleCallIf :: (Serializable a, Serializable b) => (a -> Bool) -> (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCallIf cond handler = DispatchIf { - dispatch = doHandle handler +handleCallIf cond -- ^ predicate that must be satisfied for the handler to run + handler -- ^ a reply yielding function over the process state and input message + = DispatchIf { + dispatch = doHandle handler , dispatchIf = doCheckCall cond } where doHandle :: (Serializable a, Serializable b) @@ -458,7 +584,9 @@ handleCastIf :: (Serializable a) => (a -> Bool) -> (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCastIf cond h = DispatchIf { +handleCastIf cond -- ^ predicate that must be satisfied for the handler to run + h -- ^ an action yielding function over the process state and input message + = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) , dispatchIf = \_ (CastMessage msg) -> cond msg } @@ -475,7 +603,9 @@ handleCastIf_ :: (Serializable a) => (a -> Bool) -> (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -handleCastIf_ cond h = DispatchIf { +handleCastIf_ cond -- ^ predicate that must be satisfied for the handler to run + h -- ^ a function from the input message to a /stateless action/, cf 'continue_' + = DispatchIf { dispatch = (\s (CastMessage p) -> h p $ s) , dispatchIf = \_ (CastMessage msg) -> cond msg } @@ -487,17 +617,18 @@ handleCastIf_ cond h = DispatchIf { -- need only decide to stop, as the terminate handler can deal with state -- cleanup etc). For example: -- --- > action (\MyCriticalErrorSignal -> stop_ TerminateNormal) +-- @action (\MyCriticalErrorSignal -> stop_ TerminateNormal)@ -- action :: forall s a . (Serializable a) => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -action h = handleDispatch perform +action h -- ^ a function from the input message to a /stateless action/, cf 'continue_' + = handleDispatch perform where perform :: (s -> a -> Process (ProcessAction s)) perform s a = let f = h a in f s -- | Constructs a handler for both /call/ and /cast/ messages. --- > handleDispatch = handleDispatchIf (const True) +-- @handleDispatch = handleDispatchIf (const True)@ -- handleDispatch :: (Serializable a) => (s -> a -> Process (ProcessAction s)) @@ -552,7 +683,9 @@ doCheckCall :: forall s a. (Serializable a) doCheckCall c _ (CallMessage m _) = c m doCheckCall _ _ _ = False --- Process Implementation +-------------------------------------------------------------------------------- +-- Process Implementation -- +-------------------------------------------------------------------------------- applyPolicy :: s -> UnhandledMessagePolicy @@ -567,10 +700,9 @@ applyPolicy s p m = initLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason initLoop b s w = let p = unhandledMessagePolicy b - t = timeoutHandler b ms = map (matchMessage p s) (dispatchers b) ms' = ms ++ addInfoAux p s (infoHandlers b) - in loop ms' t s w + in loop ms' b s w where addInfoAux :: UnhandledMessagePolicy -> s @@ -600,36 +732,37 @@ initLoop b s w = Just act -> return act loop :: [Match (ProcessAction s)] - -> TimeoutHandler s + -> ProcessDefinition s -> s -> Delay -> Process TerminateReason -loop ms h s t = do - ac <- processReceive ms h s t +loop ms def state t = + let handleTimeout = timeoutHandler def + handleStop = terminateHandler def + in do + ac <- processReceive ms handleTimeout state t case ac of - (ProcessContinue s') -> loop ms h s' t - (ProcessTimeout t' s') -> loop ms h s' (Delay t') - (ProcessHibernate d' s') -> block d' >> loop ms h s' t - (ProcessStop r) -> return (r :: TerminateReason) + (ProcessContinue s') -> loop ms def s' t + (ProcessTimeout t' s') -> loop ms def s' (Delay t') + (ProcessHibernate d' s') -> block d' >> loop ms def s' t + (ProcessStop r) -> handleStop state r >> return (r :: TerminateReason) where block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) processReceive :: [Match (ProcessAction s)] - -> TimeoutHandler s - -> s - -> Delay - -> Process (ProcessAction s) -processReceive ms h s t = do - next <- recv ms t + -> TimeoutHandler s -> s + -> Delay -> Process (ProcessAction s) +processReceive ms handleTimeout state d = do + next <- recv ms d case next of - Nothing -> h s t + Nothing -> handleTimeout state d Just pa -> return pa where recv :: [Match (ProcessAction s)] -> Delay -> Process (Maybe (ProcessAction s)) - recv matches d = - case d of + recv matches d' = + case d' of Infinity -> receiveWait matches >>= return . Just Delay t' -> receiveTimeout (asTimeout t') matches @@ -639,4 +772,3 @@ sendTo :: (Serializable m) => Recipient -> m -> Process () sendTo (SendToPid p) m = send p m sendTo (SendToService s) m = nsend s m sendTo (SendToRemoteService s n) m = nsendRemote n s m - From 0cbee6e98ab90951dd86fccac9ec2b3c561bd5aa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 18:03:33 +0000 Subject: [PATCH 0782/2357] use less TH; move TerminateReason; add tryCall and document GenProcess --- src/Control/Distributed/Process/AsyncChan.hs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index f4c15303..8979350d 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -19,7 +19,20 @@ -- result of one or more asynchronously running (and potentially distributed) -- processes. -- +-- The async handles returned by this module cannot be used by processes other +-- than the caller of 'async', and are not 'Serializable'. Specifically, calls +-- that block until an async worker completes (i.e., all variants of 'wait') +-- will /never return/ if called from a different process. -- +-- > h <- newEmptyMVar +-- > outer <- spawnLocal $ async runMyAsyncTask >>= liftIO $ putMVar h +-- > hAsync <- liftIO $ takeMVar h +-- > say "this expression will never return, because hAsync belongs to 'outer'" +-- > wait hAsync +-- +-- As with 'Control.Distributed.Platform.Async.AsyncChan', workers can be +-- started on a local or remote node. +-- See 'Control.Distributed.Platform.Async.AsyncTask'. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.Async.AsyncChan From 36fc355a260e16f5fca16eb139f43b682c815b31 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 18:04:39 +0000 Subject: [PATCH 0783/2357] cosmetic --- src/Control/Distributed/Process/Platform.hs | 2 +- .../Process/Platform/Internal/Primitives.hs | 28 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index de17d4b2..db264b64 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -41,5 +41,5 @@ import qualified Control.Distributed.Process.Platform.Internal.Primitives (__rem -- remote table __remoteTable :: RemoteTable -> RemoteTable -__remoteTable = +__remoteTable = Control.Distributed.Process.Platform.Internal.Primitives.__remoteTable diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index d96a45c5..bd2e3395 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -1,9 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} --- Common Entities used throughout -platform. --- NB: Please DO NOT use this module as a dumping ground. - ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.Platform.Internal.Primitives @@ -23,14 +20,14 @@ module Control.Distributed.Process.Platform.Internal.Primitives spawnLinkLocal , spawnMonitorLocal , linkOnFailure - + -- registration/start , whereisOrStart , whereisOrStartRemote - + -- matching , matchCond - + -- remote table , __remoteTable ) @@ -95,9 +92,9 @@ whereisOrStart name proc = do mpid <- whereis name case mpid of Just pid -> return pid - Nothing -> + Nothing -> do caller <- getSelfPid - pid <- spawnLocal $ + pid <- spawnLocal $ do self <- getSelfPid register name self send caller (RegisterSelf,self) @@ -112,7 +109,7 @@ whereisOrStart name proc = ] case ret of Nothing -> whereisOrStart name proc - Just somepid -> + Just somepid -> do unmonitor ref send somepid () return somepid @@ -134,7 +131,7 @@ whereisOrStartRemote :: NodeId -> String -> Closure (Process ()) -> Process (May whereisOrStartRemote nid name proc = do mRef <- monitorNode nid whereisRemoteAsync nid name - res <- receiveWait + res <- receiveWait [ matchIf (\(WhereIsReply label _) -> label == name) (\(WhereIsReply _ mPid) -> return (Just mPid)), matchIf (\(NodeMonitorNotification aref _ _) -> aref == mRef) @@ -143,14 +140,14 @@ whereisOrStartRemote nid name proc = case res of Nothing -> return Nothing Just (Just pid) -> unmonitor mRef >> return (Just pid) - Just Nothing -> + Just Nothing -> do self <- getSelfPid - sRef <- spawnAsync nid ($(mkClosure 'registerSelf) (name,self) `seqCP` proc) + sRef <- spawnAsync nid ($(mkClosure 'registerSelf) (name,self) `seqCP` proc) ret <- receiveWait [ matchIf (\(NodeMonitorNotification ref _ _) -> ref == mRef) (\(NodeMonitorNotification _ _ _) -> return Nothing), matchIf (\(DidSpawn ref _) -> ref==sRef ) - (\(DidSpawn _ pid) -> + (\(DidSpawn _ pid) -> do pRef <- monitor pid receiveWait [ matchIf (\(RegisterSelf, apid) -> apid == pid) @@ -166,15 +163,14 @@ whereisOrStartRemote nid name proc = unmonitor mRef case ret of Nothing -> whereisOrStartRemote nid name proc - Just pid -> return $ Just pid + Just pid -> return $ Just pid -- advanced messaging/matching -- | An alternative to 'matchIf' that allows both predicate and action -- to be expressed in one parameter. matchCond :: (Serializable a) => (a -> Maybe (Process b)) -> Match b -matchCond cond = +matchCond cond = let v n = (isJust n, fromJust n) res = v . cond in matchIf (fst . res) (snd . res) - From 1308156834c77976f4a9c6e5319158c623ad1966 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 20:10:46 +0000 Subject: [PATCH 0784/2357] tidy up the GenProcess error handling docs a bit, just 'til it's ready --- src/Control/Distributed/Process/Platform/GenProcess.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 21dd6ce5..e5a9b7c0 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -117,7 +117,12 @@ -- asynchronous exceptions are supported by the API, so /error/ handling -- code is the responsibility of the programmer. -- --- The API provides several scopes for error handling. +-- The API provides several scopes for error handling. There is obviously +-- nothing to stop the programmer from catching exceptions in various +-- handlers, and this is fine, as is using the 'catchExit' API from +-- 'Control.Distributed.Process'. +-- +-- ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess From 508ea9b9bf3bb6b719c80a16be06d14ddb5eca49 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 20:10:46 +0000 Subject: [PATCH 0785/2357] tidy up the GenProcess error handling docs a bit, just 'til it's ready --- src/Control/Distributed/Process/Platform/GenProcess.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 21dd6ce5..e5a9b7c0 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -117,7 +117,12 @@ -- asynchronous exceptions are supported by the API, so /error/ handling -- code is the responsibility of the programmer. -- --- The API provides several scopes for error handling. +-- The API provides several scopes for error handling. There is obviously +-- nothing to stop the programmer from catching exceptions in various +-- handlers, and this is fine, as is using the 'catchExit' API from +-- 'Control.Distributed.Process'. +-- +-- ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess From 6cd79c97d343ea59fa677c18ea407985801f7ef8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 01:13:53 +0000 Subject: [PATCH 0786/2357] oops - fix parameter doc comments --- .../Process/Platform/GenProcess.hs | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index e5a9b7c0..f03b7a69 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -508,11 +508,10 @@ handleCall_ = handleCallIf_ (const True) -- -- See 'handleCall' handleCallIf_ :: (Serializable a, Serializable b) - => (a -> Bool) - -> (a -> Process b) - -> Dispatcher s -handleCallIf_ cond -- ^ predicate that must be satisfied for the handler to run - handler -- ^ a function from an input message to a reply + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (a -> Process b) -- ^ a function from an input message to a reply + -> Dispatcher s +handleCallIf_ cond handler = DispatchIf { dispatch = doHandle handler , dispatchIf = doCheckCall cond @@ -547,11 +546,11 @@ handleCall = handleCallIf (const True) -- dispatched to the handler if the supplied condition evaluates to @True@ -- handleCallIf :: (Serializable a, Serializable b) - => (a -> Bool) - -> (s -> a -> Process (ProcessReply s b)) - -> Dispatcher s -handleCallIf cond -- ^ predicate that must be satisfied for the handler to run - handler -- ^ a reply yielding function over the process state and input message + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (s -> a -> Process (ProcessReply s b)) + -- ^ a reply yielding function over the process state and input message + -> Dispatcher s +handleCallIf cond handler = DispatchIf { dispatch = doHandle handler , dispatchIf = doCheckCall cond @@ -586,11 +585,11 @@ handleCast = handleCastIf (const True) -- in a 'Behaviour' specification for the /GenProcess/. -- handleCastIf :: (Serializable a) - => (a -> Bool) - -> (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleCastIf cond -- ^ predicate that must be satisfied for the handler to run - h -- ^ an action yielding function over the process state and input message + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (s -> a -> Process (ProcessAction s)) + -- ^ an action yielding function over the process state and input message + -> Dispatcher s +handleCastIf cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) , dispatchIf = \_ (CastMessage msg) -> cond msg @@ -605,11 +604,11 @@ handleCast_ = handleCastIf_ (const True) -- | Version of 'handleCastIf' that ignores the server state. -- handleCastIf_ :: (Serializable a) - => (a -> Bool) - -> (a -> (s -> Process (ProcessAction s))) - -> Dispatcher s -handleCastIf_ cond -- ^ predicate that must be satisfied for the handler to run - h -- ^ a function from the input message to a /stateless action/, cf 'continue_' + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (a -> (s -> Process (ProcessAction s))) + -- ^ a function from the input message to a /stateless action/, cf 'continue_' + -> Dispatcher s +handleCastIf_ cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h p $ s) , dispatchIf = \_ (CastMessage msg) -> cond msg @@ -625,10 +624,10 @@ handleCastIf_ cond -- ^ predicate that must be satisfied for the handler to run -- @action (\MyCriticalErrorSignal -> stop_ TerminateNormal)@ -- action :: forall s a . (Serializable a) - => (a -> (s -> Process (ProcessAction s))) - -> Dispatcher s -action h -- ^ a function from the input message to a /stateless action/, cf 'continue_' - = handleDispatch perform + => (a -> (s -> Process (ProcessAction s))) + -- ^ a function from the input message to a /stateless action/, cf 'continue_' + -> Dispatcher s +action h = handleDispatch perform where perform :: (s -> a -> Process (ProcessAction s)) perform s a = let f = h a in f s From b60468823e8d91546ecddc0deeba1f88deb33e3f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 01:13:53 +0000 Subject: [PATCH 0787/2357] oops - fix parameter doc comments --- .../Process/Platform/GenProcess.hs | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index e5a9b7c0..f03b7a69 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -508,11 +508,10 @@ handleCall_ = handleCallIf_ (const True) -- -- See 'handleCall' handleCallIf_ :: (Serializable a, Serializable b) - => (a -> Bool) - -> (a -> Process b) - -> Dispatcher s -handleCallIf_ cond -- ^ predicate that must be satisfied for the handler to run - handler -- ^ a function from an input message to a reply + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (a -> Process b) -- ^ a function from an input message to a reply + -> Dispatcher s +handleCallIf_ cond handler = DispatchIf { dispatch = doHandle handler , dispatchIf = doCheckCall cond @@ -547,11 +546,11 @@ handleCall = handleCallIf (const True) -- dispatched to the handler if the supplied condition evaluates to @True@ -- handleCallIf :: (Serializable a, Serializable b) - => (a -> Bool) - -> (s -> a -> Process (ProcessReply s b)) - -> Dispatcher s -handleCallIf cond -- ^ predicate that must be satisfied for the handler to run - handler -- ^ a reply yielding function over the process state and input message + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (s -> a -> Process (ProcessReply s b)) + -- ^ a reply yielding function over the process state and input message + -> Dispatcher s +handleCallIf cond handler = DispatchIf { dispatch = doHandle handler , dispatchIf = doCheckCall cond @@ -586,11 +585,11 @@ handleCast = handleCastIf (const True) -- in a 'Behaviour' specification for the /GenProcess/. -- handleCastIf :: (Serializable a) - => (a -> Bool) - -> (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleCastIf cond -- ^ predicate that must be satisfied for the handler to run - h -- ^ an action yielding function over the process state and input message + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (s -> a -> Process (ProcessAction s)) + -- ^ an action yielding function over the process state and input message + -> Dispatcher s +handleCastIf cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) , dispatchIf = \_ (CastMessage msg) -> cond msg @@ -605,11 +604,11 @@ handleCast_ = handleCastIf_ (const True) -- | Version of 'handleCastIf' that ignores the server state. -- handleCastIf_ :: (Serializable a) - => (a -> Bool) - -> (a -> (s -> Process (ProcessAction s))) - -> Dispatcher s -handleCastIf_ cond -- ^ predicate that must be satisfied for the handler to run - h -- ^ a function from the input message to a /stateless action/, cf 'continue_' + => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run + -> (a -> (s -> Process (ProcessAction s))) + -- ^ a function from the input message to a /stateless action/, cf 'continue_' + -> Dispatcher s +handleCastIf_ cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h p $ s) , dispatchIf = \_ (CastMessage msg) -> cond msg @@ -625,10 +624,10 @@ handleCastIf_ cond -- ^ predicate that must be satisfied for the handler to run -- @action (\MyCriticalErrorSignal -> stop_ TerminateNormal)@ -- action :: forall s a . (Serializable a) - => (a -> (s -> Process (ProcessAction s))) - -> Dispatcher s -action h -- ^ a function from the input message to a /stateless action/, cf 'continue_' - = handleDispatch perform + => (a -> (s -> Process (ProcessAction s))) + -- ^ a function from the input message to a /stateless action/, cf 'continue_' + -> Dispatcher s +action h = handleDispatch perform where perform :: (s -> a -> Process (ProcessAction s)) perform s a = let f = h a in f s From e62b69937866a8ead6efe73b5524e1511767ca67 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 23:02:12 +0000 Subject: [PATCH 0788/2357] Provide a common API for Async implementations We allow for importing either Async or one of the implementations, which can be done quite transparently except that using the 'top level' API means you've got to spawn a little differently. --- distributed-process-platform.cabal | 15 +- src/Control/Distributed/Process/Async.hs | 177 +++++++++++++++---- src/Control/Distributed/Process/AsyncChan.hs | 7 +- tests/TestAsync.hs | 142 ++++++++++++++- tests/TestGenServer.hs | 1 - 5 files changed, 293 insertions(+), 49 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64573b96..6dcfac73 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -46,7 +46,8 @@ library Control.Distributed.Process.Platform.Timer other-modules: Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Async.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -79,7 +80,8 @@ test-suite TimerTests TestUtils, Control.Distributed.Process.Platform.Test Control.Distributed.Process.Platform.Internal.Types, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -112,7 +114,8 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Time, TestUtils, Control.Distributed.Process.Platform.Test, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestPrimitives.hs @@ -140,7 +143,8 @@ test-suite Tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestAsync.hs @@ -168,6 +172,7 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - MathsDemo + MathsDemo, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index cad7ec19..e61371c0 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -1,6 +1,8 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ExistentialQuantification #-} ----------------------------------------------------------------------------- -- | @@ -30,49 +32,152 @@ module Control.Distributed.Process.Platform.Async ( -- types/data - AsyncRef + Async(asyncWorker) + , AsyncRef , AsyncTask(..) , AsyncResult(..) + -- functions for starting/spawning + , async + , asyncLinked + , asyncSTM , asyncDo + -- and stopping/killing + , cancel + , cancelWait + , cancelWith + , cancelKill + -- functions to query an async-result + , poll + , check + , wait +-- , waitAny +-- , waitAnyTimeout + , waitTimeout + , waitCheckTimeout ) where + import Control.Distributed.Process -import Control.Distributed.Process.Serializable (SerializableDict) -import Data.Binary -import Data.DeriveTH -import Data.Typeable (Typeable) +import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Platform.Async.Types + ( Async(..) + , AsyncRef + , AsyncTask(..) + , AsyncResult(..) + ) +import qualified Control.Distributed.Process.Platform.Async.AsyncSTM as AsyncSTM +-- import qualified Control.Distributed.Process.Platform.Async.AsyncChan as AsyncChan +import Control.Distributed.Process.Platform.Time -------------------------------------------------------------------------------- -- API -- -------------------------------------------------------------------------------- --- | A reference to an asynchronous action -type AsyncRef = ProcessId - --- | A task to be performed asynchronously. This can either take the --- form of an action that runs over some type @a@ in the @Process@ monad, --- or a static 'SerializableDict' and @Closure (Process a)@ neccessary for the --- task to be spawned on a remote node. -data AsyncTask a = - AsyncTask { asyncTask :: Process a } - | AsyncRemoteTask { - asyncTaskDict :: Static (SerializableDict a) - , asyncTaskNode :: NodeId - , asyncTaskProc :: Closure (Process a) - } - --- | Represents the result of an asynchronous action, which can be in one of --- several states at any given time. -data AsyncResult a = - AsyncDone a -- ^ a completed action and its result - | AsyncFailed DiedReason -- ^ a failed action and the failure reason - | AsyncLinkFailed DiedReason -- ^ a link failure and the reason - | AsyncCancelled -- ^ a cancelled action - | AsyncPending -- ^ a pending action (that is still running) - deriving (Typeable) -$(derive makeBinary ''AsyncResult) - -deriving instance Eq a => Eq (AsyncResult a) -deriving instance Show a => Show (AsyncResult a) +async :: (Serializable a) => Process a -> Process (Async a) +async t = asyncSTM (AsyncTask t) + +asyncLinked :: (Serializable a) => Process a -> Process (Async a) +asyncLinked p = AsyncSTM.newAsync AsyncSTM.asyncLinked (AsyncTask p) + +asyncSTM :: (Serializable a) => AsyncTask a -> Process (Async a) +asyncSTM = AsyncSTM.newAsync AsyncSTM.async asyncDo :: Process a -> AsyncTask a asyncDo = AsyncTask + +-- | Check whether an 'AsyncSTM' has completed yet. The status of the +-- action is encoded in the returned 'AsyncResult'. If the action has not +-- completed, the result will be 'AsyncPending', or one of the other +-- constructors otherwise. This function does not block waiting for the result. +-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. +-- See 'Async'. +{-# INLINE poll #-} +poll :: (Serializable a) => Async a -> Process (AsyncResult a) +poll = h_poll + +-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. +-- See 'poll'. +{-# INLINE check #-} +check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) +check = h_check + +-- | Wait for an asynchronous operation to complete or timeout. This variant +-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the +-- result has not been made available, otherwise one of the other constructors. +{-# INLINE waitCheckTimeout #-} +waitCheckTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (AsyncResult a) +waitCheckTimeout = flip h_waitCheckTimeout + +-- | Wait for an asynchronous action to complete, and return its +-- value. The result (which can include failure and/or cancellation) is +-- encoded by the 'AsyncResult' type. +-- +-- > wait = liftIO . atomically . waitSTM +-- +{-# INLINE wait #-} +wait :: Async a -> Process (AsyncResult a) +wait = h_wait + +-- | Wait for an asynchronous operation to complete or timeout. Returns +-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within +-- the specified delay, otherwise @Just asyncResult@ is returned. If you want +-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then +-- consider using 'wait' or 'waitCheckTimeout' instead. +{-# INLINE waitTimeout #-} +waitTimeout :: (Serializable a) => + TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) +waitTimeout = flip h_waitTimeout + +-- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. +-- To wait for cancellation to complete, use 'cancelWait' instead. The notes +-- about the asynchronous nature of 'cancelWait' apply here also. +-- +-- See 'Control.Distributed.Process' +{-# INLINE cancel #-} +cancel :: Async a -> Process () +cancel = h_cancel + +-- | Cancel an asynchronous operation and wait for the cancellation to complete. +-- Because of the asynchronous nature of message passing, the instruction to +-- cancel will race with the asynchronous worker, so it is /entirely possible/ +-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For +-- example, the worker may complete its task after this function is called, but +-- before the cancellation instruction is acted upon. +-- +-- If you wish to stop an asychronous operation /immediately/ (with caveats) +-- then consider using 'cancelWith' or 'cancelKill' instead. +-- +{-# INLINE cancelWait #-} +cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) +cancelWait = h_cancelWait + +-- | Cancel an asynchronous operation immediately. +-- This operation is performed by sending an /exit signal/ to the asynchronous +-- worker, which leads to the following semantics: +-- +-- 1. if the worker already completed, this function has no effect +-- 2. the worker might complete after this call, but before the signal arrives +-- 3. the worker might ignore the exit signal using @catchExit@ +-- +-- In case of (3), this function has no effect. You should use 'cancel' +-- if you need to guarantee that the asynchronous task is unable to ignore +-- the cancellation instruction. +-- +-- You should also consider that when sending exit signals to a process, the +-- definition of 'immediately' is somewhat vague and a scheduler might take +-- time to handle the request, which can lead to situations similar to (1) as +-- listed above, if the scheduler to which the calling process' thread is bound +-- decides to GC whilst another scheduler on which the worker is running is able +-- to continue. +-- +-- See 'Control.Distributed.Process.exit' +{-# INLINE cancelWith #-} +cancelWith :: (Serializable b) => b -> Async a -> Process () +cancelWith = flip h_cancelWith + +-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. +-- +-- See 'Control.Distributed.Process.kill' +{-# INLINE cancelKill #-} +cancelKill :: String -> Async a -> Process () +cancelKill = flip h_cancelKill diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 8979350d..2d163d00 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -28,7 +28,7 @@ -- > outer <- spawnLocal $ async runMyAsyncTask >>= liftIO $ putMVar h -- > hAsync <- liftIO $ takeMVar h -- > say "this expression will never return, because hAsync belongs to 'outer'" --- > wait hAsync +-- > wait hAsync -- -- As with 'Control.Distributed.Platform.Async.AsyncChan', workers can be -- started on a local or remote node. @@ -41,6 +41,7 @@ module Control.Distributed.Process.Platform.Async.AsyncChan , AsyncTask , AsyncChan(worker) , AsyncResult(..) + , Async(asyncWorker) -- functions for starting/spawning , async , asyncLinked @@ -60,10 +61,10 @@ module Control.Distributed.Process.Platform.Async.AsyncChan , waitCheckTimeout ) where -import Control.Distributed.Process.Platform.Async hiding (asyncDo) +import Control.Distributed.Process +import Control.Distributed.Process.Platform.Async.Types import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Internal.Types -import Control.Distributed.Process import Control.Distributed.Process.Serializable import Data.Maybe ( fromMaybe diff --git a/tests/TestAsync.hs b/tests/TestAsync.hs index 843d6e06..3b129b34 100644 --- a/tests/TestAsync.hs +++ b/tests/TestAsync.hs @@ -1,18 +1,152 @@ +{-# LANGUAGE ScopedTypeVariables #-} + module Main where +import Control.Concurrent.MVar (MVar, takeMVar, newEmptyMVar) +import Control.Distributed.Process +import Control.Distributed.Process.Node +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.Test +import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Platform.Timer + import Test.Framework (Test, testGroup) +import Test.Framework.Providers.HUnit (testCase) import qualified Network.Transport as NT -import TestAsyncChan -import TestAsyncSTM +import TestAsyncChan (asyncChanTests) +import TestAsyncSTM (asyncStmTests) import TestUtils +testAsyncPoll :: TestResult (AsyncResult ()) -> Process () +testAsyncPoll result = do + hAsync <- async $ do "go" <- expect; say "running" >> return () + ar <- poll hAsync + case ar of + AsyncPending -> + send (asyncWorker hAsync) "go" >> wait hAsync >>= stash result + _ -> stash result ar >> return () + +testAsyncCancel :: TestResult (AsyncResult ()) -> Process () +testAsyncCancel result = do + hAsync <- async $ runTestProcess $ say "running" >> return () + sleep $ milliSeconds 100 + + p <- poll hAsync -- nasty kind of assertion: use assertEquals? + case p of + AsyncPending -> cancel hAsync >> wait hAsync >>= stash result + _ -> say (show p) >> stash result p + +testAsyncCancelWait :: TestResult (Maybe (AsyncResult ())) -> Process () +testAsyncCancelWait result = do + testPid <- getSelfPid + p <- spawnLocal $ do + hAsync <- async $ runTestProcess $ sleep $ seconds 60 + sleep $ milliSeconds 100 + + send testPid "running" + + AsyncPending <- poll hAsync + cancelWait hAsync >>= send testPid + + "running" <- expect + d <- expectTimeout (asTimeout $ seconds 5) + case d of + Nothing -> kill p "timed out" >> stash result Nothing + Just ar -> stash result (Just ar) + +testAsyncWaitTimeout :: TestResult (Maybe (AsyncResult ())) -> Process () +testAsyncWaitTimeout result = + let delay = seconds 1 + in do + hAsync <- async $ sleep $ seconds 20 + waitTimeout delay hAsync >>= stash result + cancelWait hAsync >> return () + +testAsyncWaitTimeoutCompletes :: TestResult (Maybe (AsyncResult ())) + -> Process () +testAsyncWaitTimeoutCompletes result = + let delay = seconds 1 + in do + hAsync <- async $ sleep $ seconds 20 + waitTimeout delay hAsync >>= stash result + cancelWait hAsync >> return () + +testAsyncLinked :: TestResult Bool -> Process () +testAsyncLinked result = do + mv :: MVar (Async ()) <- liftIO $ newEmptyMVar + pid <- spawnLocal $ do + -- NB: async == asyncLinked for AsyncChan + h <- asyncLinked $ do + "waiting" <- expect + return () + stash mv h + "sleeping" <- expect + return () + + hAsync <- liftIO $ takeMVar mv + + mref <- monitor $ asyncWorker hAsync + exit pid "stop" + + _ <- receiveTimeout (after 5 Seconds) [ + matchIf (\(ProcessMonitorNotification mref' _ _) -> mref == mref') + (\_ -> return ()) + ] + + -- since the initial caller died and we used 'asyncLinked', the async should + -- pick up on the exit signal and set the result accordingly. trying to match + -- on 'DiedException String' is pointless though, as the *string* is highly + -- context dependent. + r <- waitTimeout (within 3 Seconds) hAsync + case r of + Nothing -> stash result True + Just _ -> stash result False + +testAsyncCancelWith :: TestResult Bool -> Process () +testAsyncCancelWith result = do + p1 <- async $ do { s :: String <- expect; return s } + cancelWith "foo" p1 + AsyncFailed (DiedException _) <- wait p1 + stash result True + allAsyncTests :: NT.Transport -> IO [Test] allAsyncTests transport = do chanTestGroup <- asyncChanTests transport stmTestGroup <- asyncStmTests transport + localNode <- newLocalNode transport initRemoteTable return [ - testGroup "AsyncChan" chanTestGroup - , testGroup "AsyncSTM" stmTestGroup ] + testGroup "Async Channel" chanTestGroup + , testGroup "Async STM" stmTestGroup + , testGroup "Async Common API" [ + testCase "Async Common API cancel" + (delayedAssertion + "expected async task to have been cancelled" + localNode (AsyncCancelled) testAsyncCancel) + , testCase "Async Common API poll" + (delayedAssertion + "expected poll to return a valid AsyncResult" + localNode (AsyncDone ()) testAsyncPoll) + , testCase "Async Common API cancelWait" + (delayedAssertion + "expected cancelWait to complete some time" + localNode (Just AsyncCancelled) testAsyncCancelWait) + , testCase "Async Common API waitTimeout" + (delayedAssertion + "expected waitTimeout to return Nothing when it times out" + localNode (Nothing) testAsyncWaitTimeout) + , testCase "Async Common API waitTimeout completion" + (delayedAssertion + "expected waitTimeout to return a value" + localNode Nothing testAsyncWaitTimeoutCompletes) + , testCase "Async Common API asyncLinked" + (delayedAssertion + "expected linked process to die with originator" + localNode True testAsyncLinked) + , testCase "Async Common API cancelWith" + (delayedAssertion + "expected the worker to have been killed with the given signal" + localNode True testAsyncCancelWith) + ] ] main :: IO () main = testMain $ allAsyncTests diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index c3a1d853..b33758e1 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -10,7 +10,6 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time From 3446ffdab2f5747a2212b5bba593545f3966e114 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 01:24:53 +0000 Subject: [PATCH 0789/2357] improve/tidy documentation --- src/Control/Distributed/Process/Async.hs | 53 +++++++++----- src/Control/Distributed/Process/AsyncChan.hs | 77 +++++++------------- 2 files changed, 61 insertions(+), 69 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index e61371c0..3f73d7c3 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -14,29 +14,34 @@ -- Stability : experimental -- Portability : non-portable (requires concurrency) -- --- The modules in the @Async@ package provide operations for spawning Processes, --- waiting for their results, cancelling them and various other utilities. The --- two primary implementation are @AsyncChan@ which provides an API which is --- scoped to the calling process, and @Async@ which provides a mechanism that --- can be used by (i.e., shared across) multiple processes either locally or --- situation on remote nodes. +-- The /async/ APIs provided by distributed-process-platform provide means +-- for spawning asynchronous operations, waiting for their results, cancelling +-- them and various other utilities. The two primary implementation are +-- @AsyncChan@ which provides a handle which is scoped to the calling process, +-- and @AsyncSTM@, whose async mechanism can be used by (i.e., shared across) +-- multiple local processes. -- -- Both abstractions can run asynchronous operations on remote nodes. -- --- Despite providing an API at a higher level than the basic primitives in --- distributed-process, this API is still quite low level and it is --- recommended that you read the documentation carefully to understand its --- constraints. For a much higher level API, consider using the --- 'Control.Distributed.Platform.Task' layer. +-- There is an implicit contract for async workers; Workers must exit +-- normally (i.e., should not call the 'exit', 'die' or 'terminate' +-- Cloud Haskell primitives), otherwise the 'AsyncResult' will end up being +-- @AsyncFailed DiedException@ instead of containing the result. +-- +-- See "Control.Distributed.Process.Platform.Async.AsyncSTM", +-- "Control.Distributed.Process.Platform.Async.AsyncChan". +-- +-- See "Control.Distributed.Platform.Task" for a high level layer built +-- on these capabilities. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.Async - ( -- types/data + ( -- * Exported Types Async(asyncWorker) , AsyncRef , AsyncTask(..) , AsyncResult(..) - -- functions for starting/spawning + -- * Spawning asynchronous operations , async , asyncLinked , asyncSTM @@ -46,7 +51,7 @@ module Control.Distributed.Process.Platform.Async , cancelWait , cancelWith , cancelKill - -- functions to query an async-result + -- * Querying for results , poll , check , wait @@ -72,12 +77,22 @@ import Control.Distributed.Process.Platform.Time -- API -- -------------------------------------------------------------------------------- +-- | Spawn an 'AsyncTask' and return the 'Async' handle to it. +-- See 'asyncSTM'. async :: (Serializable a) => Process a -> Process (Async a) async t = asyncSTM (AsyncTask t) +-- | Spawn an 'AsyncTask' (linked to the calling process) and +-- return the 'Async' handle to it. +-- See 'asyncSTM'. asyncLinked :: (Serializable a) => Process a -> Process (Async a) asyncLinked p = AsyncSTM.newAsync AsyncSTM.asyncLinked (AsyncTask p) +-- | Spawn an 'AsyncTask' and return the 'Async' handle to it. +-- Uses the STM implementation, whose handles can be read by other +-- processes, though they're not @Serializable@. +-- +-- See 'Control.Distributed.Process.Platform.Async.AsyncSTM'. asyncSTM :: (Serializable a) => AsyncTask a -> Process (Async a) asyncSTM = AsyncSTM.newAsync AsyncSTM.async @@ -155,9 +170,11 @@ cancelWait = h_cancelWait -- This operation is performed by sending an /exit signal/ to the asynchronous -- worker, which leads to the following semantics: -- --- 1. if the worker already completed, this function has no effect --- 2. the worker might complete after this call, but before the signal arrives --- 3. the worker might ignore the exit signal using @catchExit@ +-- 1. If the worker already completed, this function has no effect. +-- +-- 2. The worker might complete after this call, but before the signal arrives. +-- +-- 3. The worker might ignore the exit signal using @catchExit@. -- -- In case of (3), this function has no effect. You should use 'cancel' -- if you need to guarantee that the asynchronous task is unable to ignore @@ -175,7 +192,7 @@ cancelWait = h_cancelWait cancelWith :: (Serializable b) => b -> Async a -> Process () cancelWith = flip h_cancelWith --- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. +-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit signal. -- -- See 'Control.Distributed.Process.kill' {-# INLINE cancelKill #-} diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 2d163d00..17a3103f 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -30,15 +30,16 @@ -- > say "this expression will never return, because hAsync belongs to 'outer'" -- > wait hAsync -- --- As with 'Control.Distributed.Platform.Async.AsyncChan', workers can be +-- As with 'Control.Distributed.Platform.Async.Async', workers can be -- started on a local or remote node. --- See 'Control.Distributed.Platform.Async.AsyncTask'. +-- +-- See "Control.Distributed.Platform.Async". ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.Async.AsyncChan - ( -- types/data + ( -- * Exported types AsyncRef - , AsyncTask + , AsyncTask(..) , AsyncChan(worker) , AsyncResult(..) , Async(asyncWorker) @@ -50,11 +51,12 @@ module Control.Distributed.Process.Platform.Async.AsyncChan , cancelWait , cancelWith , cancelKill - -- functions to query an async-result + -- * Querying for results , poll , check , wait , waitAny + -- * Waiting with timeouts , waitAnyTimeout , waitTimeout , waitCancelTimeout @@ -139,6 +141,7 @@ asyncDo shouldLink (AsyncTask proc) = do , channel = chan } +-- private API spawnWorkers :: (Serializable a) => Process a -> Bool @@ -187,27 +190,25 @@ spawnWorkers task shouldLink = do _ -> sendChan replyTo (AsyncFailed d) | otherwise -> kill wpid "linkFailed" --- | Check whether an 'AsyncChan' has completed yet. The status of the --- action is encoded in the returned 'AsyncResult'. If the action has not --- completed, the result will be 'AsyncPending', or one of the other --- constructors otherwise. This function does not block waiting for the result. --- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. --- See 'Async'. +-- | Check whether an 'AsyncChan' has completed yet. +-- +-- See "Control.Distributed.Process.Platform.Async". poll :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) poll hAsync = do r <- receiveChanTimeout 0 $ snd (channel hAsync) return $ fromMaybe (AsyncPending) r -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. --- See 'poll'. +-- +-- See "Control.Distributed.Process.Platform.Async". check :: (Serializable a) => AsyncChan a -> Process (Maybe (AsyncResult a)) check hAsync = poll hAsync >>= \r -> case r of AsyncPending -> return Nothing ar -> return (Just ar) --- | Wait for an asynchronous operation to complete or timeout. This variant --- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the --- result has not been made available, otherwise one of the other constructors. +-- | Wait for an asynchronous operation to complete or timeout. +-- +-- See "Control.Distributed.Process.Platform.Async". waitCheckTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (AsyncResult a) waitCheckTimeout t hAsync = @@ -216,14 +217,13 @@ waitCheckTimeout t hAsync = -- | Wait for an asynchronous action to complete, and return its -- value. The outcome of the action is encoded as an 'AsyncResult'. -- +-- See "Control.Distributed.Process.Platform.Async". wait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) wait hAsync = receiveChan $ snd (channel hAsync) --- | Wait for an asynchronous operation to complete or timeout. Returns --- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within --- the specified delay, otherwise @Just asyncResult@ is returned. If you want --- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then --- consider using 'wait' or 'waitCheckTimeout' instead. +-- | Wait for an asynchronous operation to complete or timeout. +-- +-- See "Control.Distributed.Process.Platform.Async". waitTimeout :: (Serializable a) => TimeInterval -> AsyncChan a -> Process (Maybe (AsyncResult a)) waitTimeout t hAsync = @@ -248,7 +248,8 @@ waitCancelTimeout t hAsync = do -- because 'AsyncChan' does not hold on to its result after it has been read! -- -- This function is analagous to the @mergePortsBiased@ primitive. --- See 'Control.Distibuted.Process.mergePortsBiased' +-- +-- See "Control.Distibuted.Process.mergePortsBiased". waitAny :: (Serializable a) => [AsyncChan a] -> Process (AsyncResult a) @@ -267,51 +268,25 @@ waitAnyTimeout delay asyncs = in mergePortsBiased ports >>= receiveChanTimeout (asTimeout delay) -- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. --- To wait for cancellation to complete, use 'cancelWait' instead. The notes --- about the asynchronous nature of 'cancelWait' apply here also. -- --- See 'Control.Distributed.Process' +-- See "Control.Distributed.Process.Platform.Async". cancel :: AsyncChan a -> Process () cancel (AsyncChan _ g _) = send g CancelWait -- | Cancel an asynchronous operation and wait for the cancellation to complete. --- Because of the asynchronous nature of message passing, the instruction to --- cancel will race with the asynchronous worker, so it is /entirely possible/ --- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For --- example, the worker may complete its task after this function is called, but --- before the cancellation instruction is acted upon. --- --- If you wish to stop an asychronous operation /immediately/ (with caveats) then --- consider using 'cancelWith' or 'cancelKill' instead. -- +-- See "Control.Distributed.Process.Platform.Async". cancelWait :: (Serializable a) => AsyncChan a -> Process (AsyncResult a) cancelWait hAsync = cancel hAsync >> wait hAsync -- | Cancel an asynchronous operation immediately. --- This operation is performed by sending an /exit signal/ to the asynchronous --- worker, which leads to the following semantics: --- --- 1. if the worker already completed, this function has no effect --- 2. the worker might complete after this call, but before the signal arrives --- 3. the worker might ignore the exit signal using @catchExit@ --- --- In case of (3), this function has no effect. You should use 'cancel' --- if you need to guarantee that the asynchronous task is unable to ignore --- the cancellation instruction. --- --- You should also consider that when sending exit signals to a process, the --- definition of 'immediately' is somewhat vague and a scheduler might take --- time to handle the request, which can lead to situations similar to (1) as --- listed above, if the scheduler to which the calling process' thread is bound --- decides to GC whilst another scheduler on which the worker is running is able --- to continue. -- --- See 'Control.Distributed.Process.exit' +-- See "Control.Distributed.Process.Platform.Async". cancelWith :: (Serializable b) => b -> AsyncChan a -> Process () cancelWith reason = (flip exit) reason . worker -- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit. -- --- See 'Control.Distributed.Process.kill' +-- See "Control.Distributed.Process.Platform.Async". cancelKill :: String -> AsyncChan a -> Process () cancelKill reason = (flip kill) reason . worker From b4fb79d16d12ca553b1fc3ab611779d1623186db Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 23:02:12 +0000 Subject: [PATCH 0790/2357] Provide a common API for Async implementations We allow for importing either Async or one of the implementations, which can be done quite transparently except that using the 'top level' API means you've got to spawn a little differently. --- distributed-process-platform.cabal | 15 ++++++++++----- tests/TestGenServer.hs | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64573b96..6dcfac73 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -46,7 +46,8 @@ library Control.Distributed.Process.Platform.Timer other-modules: Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Async.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -79,7 +80,8 @@ test-suite TimerTests TestUtils, Control.Distributed.Process.Platform.Test Control.Distributed.Process.Platform.Internal.Types, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -112,7 +114,8 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Time, TestUtils, Control.Distributed.Process.Platform.Test, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestPrimitives.hs @@ -140,7 +143,8 @@ test-suite Tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestAsync.hs @@ -168,6 +172,7 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - MathsDemo + MathsDemo, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index c3a1d853..b33758e1 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -10,7 +10,6 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time From 8d0c3c66f001a13c6c7104ff3c3d74ec15d31a0a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 23:02:12 +0000 Subject: [PATCH 0791/2357] Provide a common API for Async implementations We allow for importing either Async or one of the implementations, which can be done quite transparently except that using the 'top level' API means you've got to spawn a little differently. --- distributed-process-platform.cabal | 15 ++++++++++----- .../Distributed/Process/Platform/GenProcess.hs | 6 +++--- tests/TestGenServer.hs | 1 - 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64573b96..6dcfac73 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -46,7 +46,8 @@ library Control.Distributed.Process.Platform.Timer other-modules: Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Async.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -79,7 +80,8 @@ test-suite TimerTests TestUtils, Control.Distributed.Process.Platform.Test Control.Distributed.Process.Platform.Internal.Types, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -112,7 +114,8 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Time, TestUtils, Control.Distributed.Process.Platform.Test, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestPrimitives.hs @@ -140,7 +143,8 @@ test-suite Tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestAsync.hs @@ -168,6 +172,7 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - MathsDemo + MathsDemo, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index f03b7a69..58c5773c 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -179,7 +179,7 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncSTM +-- import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.Internal.Types ( TerminateReason(..)) import Control.Distributed.Process.Platform.Time @@ -378,11 +378,11 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- see "Control.Distributed.Process.Platform.Async" -- callAsync :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (AsyncSTM b) + => ProcessId -> a -> Process (Async b) callAsync sid msg = do -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 - async $ asyncDo $ do -- note [call using async] + async $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index c3a1d853..b33758e1 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -10,7 +10,6 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time From bc1ecf826f0cd622219d268e96e66b3681e7f32d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 18 Jan 2013 23:02:12 +0000 Subject: [PATCH 0792/2357] Provide a common API for Async implementations We allow for importing either Async or one of the implementations, which can be done quite transparently except that using the 'top level' API means you've got to spawn a little differently. --- distributed-process-platform.cabal | 15 ++++++++++----- .../Distributed/Process/Platform/GenProcess.hs | 6 +++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 64573b96..6dcfac73 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -46,7 +46,8 @@ library Control.Distributed.Process.Platform.Timer other-modules: Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Async.Types test-suite TimerTests type: exitcode-stdio-1.0 @@ -79,7 +80,8 @@ test-suite TimerTests TestUtils, Control.Distributed.Process.Platform.Test Control.Distributed.Process.Platform.Internal.Types, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -112,7 +114,8 @@ test-suite PrimitivesTests Control.Distributed.Process.Platform.Time, TestUtils, Control.Distributed.Process.Platform.Test, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestPrimitives.hs @@ -140,7 +143,8 @@ test-suite Tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestAsync.hs @@ -168,6 +172,7 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - MathsDemo + MathsDemo, + Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index f03b7a69..58c5773c 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -179,7 +179,7 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.Async.AsyncSTM +-- import Control.Distributed.Process.Platform.Async.AsyncSTM import Control.Distributed.Process.Platform.Internal.Types ( TerminateReason(..)) import Control.Distributed.Process.Platform.Time @@ -378,11 +378,11 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- see "Control.Distributed.Process.Platform.Async" -- callAsync :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (AsyncSTM b) + => ProcessId -> a -> Process (Async b) callAsync sid msg = do -- TODO: use a unified async API here if possible -- https://github.com/haskell-distributed/distributed-process-platform/issues/55 - async $ asyncDo $ do -- note [call using async] + async $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) From 6b215c9009c94e8446ae3659b2a59edb26ae7ce5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 01:25:44 +0000 Subject: [PATCH 0793/2357] get rid of unused pragmas --- src/Control/Distributed/Process/Async.hs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 3f73d7c3..84f7946d 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -1,9 +1,3 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ExistentialQuantification #-} - ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.Platform.Async From 91bcb1af816cc6547dc9bef8f6344601952591c0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 01:26:30 +0000 Subject: [PATCH 0794/2357] tidy/refactor API, implement common functions directly --- src/Control/Distributed/Process/Async.hs | 98 +++++++++++++++----- src/Control/Distributed/Process/AsyncChan.hs | 21 ++++- 2 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 84f7946d..1af2de99 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -39,8 +39,12 @@ module Control.Distributed.Process.Platform.Async , async , asyncLinked , asyncSTM - , asyncDo - -- and stopping/killing + , asyncLinkedSTM + , asyncChan + , asyncLinkedChan + , task + , remoteTask + -- * Cancelling asynchronous operations , cancel , cancelWait , cancelWith @@ -51,12 +55,17 @@ module Control.Distributed.Process.Platform.Async , wait -- , waitAny -- , waitAnyTimeout + -- * Waiting with timeouts , waitTimeout + , waitCancelTimeout , waitCheckTimeout ) where import Control.Distributed.Process -import Control.Distributed.Process.Serializable (Serializable) +import Control.Distributed.Process.Serializable + ( Serializable + , SerializableDict + ) import Control.Distributed.Process.Platform.Async.Types ( Async(..) , AsyncRef @@ -64,8 +73,11 @@ import Control.Distributed.Process.Platform.Async.Types , AsyncResult(..) ) import qualified Control.Distributed.Process.Platform.Async.AsyncSTM as AsyncSTM --- import qualified Control.Distributed.Process.Platform.Async.AsyncChan as AsyncChan +import qualified Control.Distributed.Process.Platform.Async.AsyncChan as AsyncChan import Control.Distributed.Process.Platform.Time +import Data.Maybe + ( fromMaybe + ) -------------------------------------------------------------------------------- -- API -- @@ -90,24 +102,56 @@ asyncLinked p = AsyncSTM.newAsync AsyncSTM.asyncLinked (AsyncTask p) asyncSTM :: (Serializable a) => AsyncTask a -> Process (Async a) asyncSTM = AsyncSTM.newAsync AsyncSTM.async -asyncDo :: Process a -> AsyncTask a -asyncDo = AsyncTask +-- | Spawn an 'AsyncTask' (linked to the calling process) and return the +-- 'Async' handle to it. Uses the STM based implementation, whose handles +-- can be read by other processes, though they're not @Serializable@. +-- +-- See 'Control.Distributed.Process.Platform.Async.AsyncSTM'. +asyncLinkedSTM :: (Serializable a) => AsyncTask a -> Process (Async a) +asyncLinkedSTM = AsyncSTM.newAsync AsyncSTM.asyncLinked + +-- | Spawn an 'AsyncTask' and return the 'Async' handle to it. +-- Uses the channel based implementation, whose handles can be read by other +-- processes, though they're not @Serializable@. +-- +-- See 'Control.Distributed.Process.Platform.Async.AsyncSTM'. +asyncChan :: (Serializable a) => AsyncTask a -> Process (Async a) +asyncChan = AsyncChan.newAsync AsyncChan.async + +-- | Spawn an 'AsyncTask' (linked to the calling process) and return the +-- 'Async' handle to it. Uses the channel based implementation, whose handles +-- can be read by other processes, though they're not @Serializable@. +-- +-- See 'Control.Distributed.Process.Platform.Async.AsyncChan'. +asyncLinkedChan :: (Serializable a) => AsyncTask a -> Process (Async a) +asyncLinkedChan = AsyncChan.newAsync AsyncChan.asyncLinked + +-- | Wraps a regular @Process a@ as an 'AsyncTask'. +task :: Process a -> AsyncTask a +task = AsyncTask --- | Check whether an 'AsyncSTM' has completed yet. The status of the +-- | Wraps the components required and builds a remote 'AsyncTask'. +remoteTask :: Static (SerializableDict a) + -> NodeId + -> Closure (Process a) + -> AsyncTask a +remoteTask = AsyncRemoteTask + +-- | Check whether an 'Async' handle has completed yet. The status of the -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other -- constructors otherwise. This function does not block waiting for the result. -- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics. --- See 'Async'. {-# INLINE poll #-} poll :: (Serializable a) => Async a -> Process (AsyncResult a) -poll = h_poll +poll = hPoll -- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@. -- See 'poll'. -{-# INLINE check #-} check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a)) -check = h_check +check hAsync = poll hAsync >>= \r -> case r of + AsyncPending -> return Nothing + ar -> return (Just ar) -- | Wait for an asynchronous operation to complete or timeout. This variant -- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the @@ -115,17 +159,15 @@ check = h_check {-# INLINE waitCheckTimeout #-} waitCheckTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (AsyncResult a) -waitCheckTimeout = flip h_waitCheckTimeout +waitCheckTimeout t hAsync = + waitTimeout t hAsync >>= return . fromMaybe (AsyncPending) -- | Wait for an asynchronous action to complete, and return its -- value. The result (which can include failure and/or cancellation) is -- encoded by the 'AsyncResult' type. --- --- > wait = liftIO . atomically . waitSTM --- {-# INLINE wait #-} wait :: Async a -> Process (AsyncResult a) -wait = h_wait +wait = hWait -- | Wait for an asynchronous operation to complete or timeout. Returns -- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within @@ -135,7 +177,20 @@ wait = h_wait {-# INLINE waitTimeout #-} waitTimeout :: (Serializable a) => TimeInterval -> Async a -> Process (Maybe (AsyncResult a)) -waitTimeout = flip h_waitTimeout +waitTimeout = flip hWaitTimeout + +-- | Wait for an asynchronous operation to complete or timeout. If it times out, +-- then 'cancelWait' the async handle instead. +-- +waitCancelTimeout :: (Serializable a) + => TimeInterval + -> Async a + -> Process (AsyncResult a) +waitCancelTimeout t hAsync = do + r <- waitTimeout t hAsync + case r of + Nothing -> cancelWait hAsync + Just ar -> return ar -- | Cancel an asynchronous operation. Cancellation is asynchronous in nature. -- To wait for cancellation to complete, use 'cancelWait' instead. The notes @@ -144,7 +199,7 @@ waitTimeout = flip h_waitTimeout -- See 'Control.Distributed.Process' {-# INLINE cancel #-} cancel :: Async a -> Process () -cancel = h_cancel +cancel = hCancel -- | Cancel an asynchronous operation and wait for the cancellation to complete. -- Because of the asynchronous nature of message passing, the instruction to @@ -158,7 +213,7 @@ cancel = h_cancel -- {-# INLINE cancelWait #-} cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a) -cancelWait = h_cancelWait +cancelWait hAsync = cancel hAsync >> wait hAsync -- | Cancel an asynchronous operation immediately. -- This operation is performed by sending an /exit signal/ to the asynchronous @@ -184,11 +239,12 @@ cancelWait = h_cancelWait -- See 'Control.Distributed.Process.exit' {-# INLINE cancelWith #-} cancelWith :: (Serializable b) => b -> Async a -> Process () -cancelWith = flip h_cancelWith +cancelWith reason = (flip exit) reason . asyncWorker -- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit signal. -- -- See 'Control.Distributed.Process.kill' {-# INLINE cancelKill #-} cancelKill :: String -> Async a -> Process () -cancelKill = flip h_cancelKill +cancelKill reason = (flip kill) reason . asyncWorker + diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 17a3103f..79ecb2fe 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -43,10 +43,11 @@ module Control.Distributed.Process.Platform.Async.AsyncChan , AsyncChan(worker) , AsyncResult(..) , Async(asyncWorker) - -- functions for starting/spawning + -- * Spawning asynchronous operations , async , asyncLinked - -- and stopping/killing + , newAsync + -- * Cancelling asynchronous operations , cancel , cancelWait , cancelWith @@ -98,6 +99,22 @@ data AsyncChan a = AsyncChan { , channel :: (InternalChannel a) } +-- | Create a new 'AsyncChane' and wrap it in an 'Async' record. +-- +-- Used by "Control.Distributed.Process.Platform.Async". +newAsync :: (Serializable a) + => (AsyncTask a -> Process (AsyncChan a)) + -> AsyncTask a -> Process (Async a) +newAsync new t = do + hAsync <- new t + return Async { + hPoll = poll hAsync + , hWait = wait hAsync + , hWaitTimeout = (flip waitTimeout) hAsync + , hCancel = cancel hAsync + , asyncWorker = worker hAsync + } + -- | Spawns an asynchronous action in a new process. -- We ensure that if the caller's process exits, that the worker is killed. -- Because an @AsyncChan@ can only be used by the initial caller's process, if From 1388c02231b2f42c75abd9bc1fe528e4763fe948 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 01:33:48 +0000 Subject: [PATCH 0795/2357] typo --- src/Control/Distributed/Process/AsyncChan.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index 79ecb2fe..e808896e 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -89,7 +89,7 @@ type InternalChannel a = (SendPort (AsyncResult a), ReceivePort (AsyncResult a)) -- of this type /must not/ be passed to functions in this module by processes -- other than the caller of 'async' - that is, this module provides asynchronous -- actions whose results are accessible *only* by the initiating process. This --- limitation is imposed becuase of the use of type channels, for which the +-- limitation is imposed becuase of the use of typed channels, for which the -- @ReceivePort@ component is effectively /thread local/. -- -- See 'async' From 4ce1e4a6bc3fd3d716208caf79b03c45929a0b32 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 02:19:57 +0000 Subject: [PATCH 0796/2357] provide waitAnyCancel for AsyncChan --- src/Control/Distributed/Process/AsyncChan.hs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Control/Distributed/Process/AsyncChan.hs b/src/Control/Distributed/Process/AsyncChan.hs index e808896e..0d2ca981 100644 --- a/src/Control/Distributed/Process/AsyncChan.hs +++ b/src/Control/Distributed/Process/AsyncChan.hs @@ -57,6 +57,7 @@ module Control.Distributed.Process.Platform.Async.AsyncChan , check , wait , waitAny + , waitAnyCancel -- * Waiting with timeouts , waitAnyTimeout , waitTimeout @@ -275,6 +276,14 @@ waitAny asyncs = where recv :: (Serializable a) => [ReceivePort a] -> Process a recv ps = mergePortsBiased ps >>= receiveChan +-- | Like 'waitAny', but also cancels the other asynchronous +-- operations as soon as one has completed. +-- +waitAnyCancel :: (Serializable a) + => [AsyncChan a] -> Process (AsyncResult a) +waitAnyCancel asyncs = + waitAny asyncs `finally` mapM_ cancel asyncs + -- | Like 'waitAny' but times out after the specified delay. waitAnyTimeout :: (Serializable a) => TimeInterval From 2e258dec361f4a4f085addaad99cf9fff6c21506 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 19 Jan 2013 02:20:19 +0000 Subject: [PATCH 0797/2357] documentation and cosmetic changes --- src/Control/Distributed/Process/Async.hs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index 1af2de99..a34aef15 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -15,7 +15,11 @@ -- and @AsyncSTM@, whose async mechanism can be used by (i.e., shared across) -- multiple local processes. -- --- Both abstractions can run asynchronous operations on remote nodes. +-- Both abstractions can run asynchronous operations on remote nodes. The STM +-- based implementation provides a slightly richer API. The API defined in +-- /this/ module only supports a subset of operations on async handles, +-- and (specifically) does not support mixing handles initialised via +-- different implementations. -- -- There is an implicit contract for async workers; Workers must exit -- normally (i.e., should not call the 'exit', 'die' or 'terminate' @@ -23,7 +27,7 @@ -- @AsyncFailed DiedException@ instead of containing the result. -- -- See "Control.Distributed.Process.Platform.Async.AsyncSTM", --- "Control.Distributed.Process.Platform.Async.AsyncChan". +-- "Control.Distributed.Process.Platform.Async.AsyncChan". -- -- See "Control.Distributed.Platform.Task" for a high level layer built -- on these capabilities. @@ -53,8 +57,6 @@ module Control.Distributed.Process.Platform.Async , poll , check , wait --- , waitAny --- , waitAnyTimeout -- * Waiting with timeouts , waitTimeout , waitCancelTimeout From 502bdb1e9176e2825e46a14ad7acbc8ae7351f2a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 20 Jan 2013 01:21:06 +0000 Subject: [PATCH 0798/2357] re-introduce Counter.hs using GenProcess; rework the APIs a little --- distributed-process-platform.cabal | 3 +- tests/Counter.hs | 95 ++++++++++++++++++++++++++++++ tests/MathsDemo.hs | 4 +- tests/TestGenServer.hs | 15 +++-- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 tests/Counter.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 6dcfac73..a426c590 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -119,7 +119,7 @@ test-suite PrimitivesTests extensions: CPP main-is: TestPrimitives.hs -test-suite Tests +test-suite AsyncTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -173,6 +173,7 @@ test-suite GenServerTests other-modules: TestUtils, MathsDemo, + Counter, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/tests/Counter.hs b/tests/Counter.hs new file mode 100644 index 00000000..5b91902a --- /dev/null +++ b/tests/Counter.hs @@ -0,0 +1,95 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Counter + ( startCounter, + getCount, + getCountAsync, + incCount, + resetCount, + wait, + waitTimeout + ) where + +import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time +import Data.Binary +import Data.DeriveTH +import Data.Typeable (Typeable) + +-------------------------------------------------------------------------------- +-- Types -- +-------------------------------------------------------------------------------- + +-- Call and Cast request types. Response types are unnecessary as the GenProcess +-- API uses the Async API, which in turn guarantees that an async handle can +-- /only/ give back a reply for that *specific* request through the use of an +-- anonymous middle-man (as the sender and reciever in our case). + +data Increment = Increment + deriving (Show, Typeable) +$(derive makeBinary ''Increment) + +data Fetch = Fetch + deriving (Show, Typeable) +$(derive makeBinary ''Fetch) + +data Reset = Reset deriving (Show, Typeable) +$(derive makeBinary ''Reset) + +type State = Int + +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- + +-- | Increment count +incCount :: ProcessId -> Process Int +incCount sid = call sid Increment + +-- | Get the current count - this is replicating what 'call' actually does +getCount :: ProcessId -> Process Int +getCount sid = getCountAsync sid >>= wait >>= unpack + where unpack :: AsyncResult Int -> Process Int + unpack (AsyncDone i) = return i + unpack asyncOther = die asyncOther + +-- | Get the current count asynchronously +getCountAsync :: ProcessId -> Process (Async Int) +getCountAsync sid = callAsync sid Fetch + +-- | Reset the current count +resetCount :: ProcessId -> Process () +resetCount sid = cast sid Reset + +-------------------------------------------------------------------------------- +-- Implementation -- +-------------------------------------------------------------------------------- + +-- | Start a counter server +startCounter :: Int -> Process ProcessId +startCounter startCount = + let server = defaultProcess { + dispatchers = [ + handleCallIf (state (\count -> count > 10)) -- invariant + (\_ (_ :: Increment) -> + noReply_ (TerminateOther "Count > 10")) + + , handleCall handleIncrement + , handleCall (\count (_ :: Fetch) -> reply count count) + , handleCast (\_ Fetch -> continue 0) + ] + } :: ProcessDefinition State + in spawnLocal $ start startCount init' server >> return () + where init' :: InitHandler Int Int + init' count = return $ InitOk count Infinity + +handleIncrement :: State -> Increment -> Process (ProcessReply State Int) +handleIncrement count _ = + let newCount = count + 1 in do + next <- continue newCount + replyWith newCount next + diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index 7f564a8f..6ca7b555 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -45,7 +45,7 @@ launchMathServer = let server = statelessProcess { dispatchers = [ handleCall_ (\(Add x y) -> return (x + y)) - , handleCallIf_ (\(Divide _ y) -> y /= 0) handleDivide + , handleCallIf_ (input (\(Divide _ y) -> y /= 0)) handleDivide , handleCall_ (\(Divide _ _) -> divByZero) , action (\("stop") -> stop_ TerminateNormal) ] @@ -55,4 +55,4 @@ launchMathServer = handleDivide (Divide x y) = return $ Right $ x / y divByZero :: Process (Either DivByZero Double) - divByZero = return $ Left DivByZero \ No newline at end of file + divByZero = return $ Left DivByZero diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index b33758e1..f74d3722 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -6,6 +6,7 @@ module Main where import Control.Concurrent.MVar +import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() @@ -19,6 +20,8 @@ import Data.Binary() import Data.Typeable() import MathsDemo +import Prelude hiding (catch) + import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import TestUtils @@ -112,7 +115,7 @@ testKillMidCall result = do callAsync pid "hello-world" >>= cancelWait >>= unpack result pid where unpack :: TestResult Bool -> ProcessId -> AsyncResult () -> Process () unpack res sid AsyncCancelled = kill sid "stop" >> stash res True - unpack res sid _ = kill sid "stop" >> stash res False + unpack res sid _ = kill sid "stop" >> stash res False -- MathDemo test @@ -148,11 +151,11 @@ mkServer policy = , handleCast (\s' ("ping", pid :: ProcessId) -> send pid "pong" >> continue s') - , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") + , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) (\("timeout", Delay d) -> timeoutAfter_ d) - , action (\("stop") -> stop_ TerminateNormal) - , action (\("hibernate", d :: TimeInterval) -> hibernate_ d) + , handleCast_ (\("stop") -> stop_ TerminateNormal) + , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" @@ -160,7 +163,8 @@ mkServer policy = in do exitReason <- liftIO $ newEmptyMVar pid <- spawnLocal $ do - start () (statelessInit Infinity) s >>= stash exitReason + catch (start () (statelessInit Infinity) s >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) tests :: NT.Transport -> IO [Test] @@ -219,4 +223,3 @@ tests transport = do main :: IO () main = testMain $ tests - From a53026b75453d7d5fc0b8a88d57a313f35123c22 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 20 Jan 2013 01:21:06 +0000 Subject: [PATCH 0799/2357] re-introduce Counter.hs using GenProcess; rework the APIs a little --- distributed-process-platform.cabal | 3 +- .../Process/Platform/GenProcess.hs | 166 ++++++++++++------ tests/TestGenServer.hs | 15 +- 3 files changed, 126 insertions(+), 58 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 6dcfac73..a426c590 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -119,7 +119,7 @@ test-suite PrimitivesTests extensions: CPP main-is: TestPrimitives.hs -test-suite Tests +test-suite AsyncTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -173,6 +173,7 @@ test-suite GenServerTests other-modules: TestUtils, MathsDemo, + Counter, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 58c5773c..59c66132 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -122,7 +122,7 @@ -- handlers, and this is fine, as is using the 'catchExit' API from -- 'Control.Distributed.Process'. -- --- +-- ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess @@ -133,6 +133,8 @@ module Control.Distributed.Process.Platform.GenProcess , InitResult(..) , ProcessAction(..) , ProcessReply + , CallHandler + , CastHandler , InitHandler , TerminateHandler , TimeoutHandler @@ -140,6 +142,7 @@ module Control.Distributed.Process.Platform.GenProcess , ProcessDefinition(..) -- * Client interaction with the process , start + , defaultProcess , statelessProcess , statelessInit , call @@ -149,8 +152,13 @@ module Control.Distributed.Process.Platform.GenProcess , callTimeout , cast -- * Handler interaction inside the process + , condition + , state + , input , reply , replyWith + , noReply + , noReply_ , continue , continue_ , timeoutAfter @@ -178,15 +186,15 @@ module Control.Distributed.Process.Platform.GenProcess import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Async --- import Control.Distributed.Process.Platform.Async.AsyncSTM +import Control.Distributed.Process.Platform.Async hiding (check) import Control.Distributed.Process.Platform.Internal.Types ( TerminateReason(..)) +import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time -import Data.Binary +import Data.Binary hiding (decode) import Data.DeriveTH -import Data.Typeable (Typeable, typeOf) +import Data.Typeable (Typeable) import Prelude hiding (init) -------------------------------------------------------------------------------- @@ -238,6 +246,17 @@ data ProcessReply s a = ProcessReply a (ProcessAction s) | NoReply (ProcessAction s) +type CallHandler a s = s -> a -> Process (ProcessReply s a) + +type CastHandler s = s -> Process () + +-- type InfoHandler a = forall a b. (Serializable a, Serializable b) => a -> Process b + +data Condition s m = + Condition (s -> m -> Bool) + | State (s -> Bool) + | Input (m -> Bool) + -- | An expression used to initialise a process with its state. type InitHandler a s = a -> Process (InitResult s) @@ -310,6 +329,15 @@ start args init behave = do InitOk s d -> initLoop behave s d >>= return . Right f@(InitFail _) -> return $ Left f +defaultProcess :: ProcessDefinition s +defaultProcess = ProcessDefinition { + dispatchers = [] + , infoHandlers = [] + , timeoutHandler = \s _ -> continue s + , terminateHandler = \_ _ -> return () + , unhandledMessagePolicy = Terminate + } :: ProcessDefinition s + -- | A basic, stateless process definition, where the unhandled message policy -- is set to 'Terminate', the default timeout handlers does nothing (i.e., the -- same as calling @continue ()@ and the terminate handler is a no-op. @@ -335,7 +363,7 @@ call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r unpack (AsyncFailed r) = die $ TerminateOther $ "CALL_FAILED;" ++ show r - unpack ar = die $ TerminateOther $ show (typeOf ar) + unpack ar = die $ TerminateOther $ showTypeRep ar -- | Safe version of 'call' that returns information about the error -- if the operation fails. If an error occurs then the explanation will be @@ -345,7 +373,7 @@ safeCall :: forall a b . (Serializable a, Serializable b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Right r unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r - unpack ar = return $ Left $ TerminateOther $ show (typeOf ar) + unpack ar = return $ Left $ TerminateOther $ showTypeRep ar -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -426,8 +454,18 @@ cast sid msg = send sid (CastMessage msg) -- Producing ProcessAction and ProcessReply from inside handler expressions -- -------------------------------------------------------------------------------- +condition :: forall a b. (Serializable a, Serializable b) + => (a -> b -> Bool) + -> Condition a b +condition = Condition + +state :: forall s m. (Serializable m) => (s -> Bool) -> Condition s m +state = State + +input :: forall s m. (Serializable m) => (m -> Bool) -> Condition s m +input = Input + -- | Instructs the process to send a reply and continue running. --- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r @@ -436,7 +474,15 @@ replyWith :: (Serializable m) => m -> ProcessAction s -> Process (ProcessReply s m) -replyWith msg state = return $ ProcessReply msg state +replyWith msg st = return $ ProcessReply msg st + +-- | Instructs the process to skip sending a reply /and/ evaluate a 'ProcessAction' +noReply :: (Serializable r) => ProcessAction s -> Process (ProcessReply s r) +noReply = return . NoReply + +-- | Halt a call handler without regard for the expected return type. +noReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) +noReply_ r = stop r >>= noReply -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) @@ -499,7 +545,7 @@ stop_ r _ = stop r handleCall_ :: (Serializable a, Serializable b) => (a -> Process b) -> Dispatcher s -handleCall_ = handleCallIf_ (const True) +handleCall_ = handleCallIf_ $ input (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. This variant ignores the state argument present in 'handleCall' and @@ -507,14 +553,14 @@ handleCall_ = handleCallIf_ (const True) -- only dispatched to the handler if the supplied condition evaluates to @True@ -- -- See 'handleCall' -handleCallIf_ :: (Serializable a, Serializable b) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCallIf_ :: forall s a b . (Serializable a, Serializable b) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (a -> Process b) -- ^ a function from an input message to a reply -> Dispatcher s handleCallIf_ cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheckCall cond + , dispatchIf = checkCall cond } where doHandle :: (Serializable a, Serializable b) => (a -> Process b) @@ -522,8 +568,7 @@ handleCallIf_ cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ _ = error "illegal input" - -- TODO: standard 'this cannot happen' error message + doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -537,7 +582,7 @@ handleCallIf_ cond handler handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall = handleCallIf (const True) +handleCall = handleCallIf $ state (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, @@ -545,15 +590,15 @@ handleCall = handleCallIf (const True) -- in a 'Behaviour' specification for the /GenProcess/. Messages are only -- dispatched to the handler if the supplied condition evaluates to @True@ -- -handleCallIf :: (Serializable a, Serializable b) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCallIf :: forall s a b . (Serializable a, Serializable b) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (s -> a -> Process (ProcessReply s b)) -- ^ a reply yielding function over the process state and input message -> Dispatcher s handleCallIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheckCall cond + , dispatchIf = checkCall cond } where doHandle :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) @@ -561,8 +606,7 @@ handleCallIf cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = error "illegal input" - -- TODO: standard 'this cannot happen' error message + doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -576,42 +620,43 @@ handleCallIf cond handler -- > handleCast = handleCastIf (const True) -- handleCast :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCast = handleCastIf (const True) + => (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleCast = handleCastIf $ input (const True) -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion -- in a 'Behaviour' specification for the /GenProcess/. -- -handleCastIf :: (Serializable a) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCastIf :: forall s a . (Serializable a) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (s -> a -> Process (ProcessAction s)) -- ^ an action yielding function over the process state and input message -> Dispatcher s handleCastIf cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) - , dispatchIf = \_ (CastMessage msg) -> cond msg + , dispatchIf = checkCast cond } -- | Version of 'handleCast' that ignores the server state. -- handleCast_ :: (Serializable a) - => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -handleCast_ = handleCastIf_ (const True) + => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s +handleCast_ = handleCastIf_ $ input (const True) -- | Version of 'handleCastIf' that ignores the server state. -- -handleCastIf_ :: (Serializable a) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCastIf_ :: forall s a . (Serializable a) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (a -> (s -> Process (ProcessAction s))) -- ^ a function from the input message to a /stateless action/, cf 'continue_' -> Dispatcher s handleCastIf_ cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h p $ s) - , dispatchIf = \_ (CastMessage msg) -> cond msg + , dispatchIf = checkCast cond } -- | Constructs an /action/ handler. Like 'handleDispatch' this can handle both @@ -637,18 +682,18 @@ action h = handleDispatch perform handleDispatch :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleDispatch = handleDispatchIf (const True) +handleDispatch = handleDispatchIf $ input (const True) -- | Constructs a handler for both /call/ and /cast/ messages. Messages are only -- dispatched to the handler if the supplied condition evaluates to @True@. -- -handleDispatchIf :: (Serializable a) - => (a -> Bool) +handleDispatchIf :: forall s a . (Serializable a) + => Condition s a -> (s -> a -> Process (ProcessAction s)) -> Dispatcher s handleDispatchIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheck cond + , dispatchIf = check cond } where doHandle :: (Serializable a) => (s -> a -> Process (ProcessAction s)) @@ -660,13 +705,6 @@ handleDispatchIf cond handler = DispatchIf { (CallMessage p _) -> (h s p) (CastMessage p) -> (h s p) - doCheck :: forall s a. (Serializable a) - => (a -> Bool) -> s -> Message a -> Bool - doCheck c _ (CallMessage m _) = c m - doCheck c _ (CastMessage m) = c m - --- wrapping /normal/ functions with InfoDispatcher - -- | Creates a generic input handler (i.e., for recieved messages that are /not/ -- sent using the 'cast' or 'call' APIs) from an ordinary function in the -- 'Process' monad. @@ -682,10 +720,36 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) -doCheckCall :: forall s a. (Serializable a) - => (a -> Bool) -> s -> Message a -> Bool -doCheckCall c _ (CallMessage m _) = c m -doCheckCall _ _ _ = False +check :: forall s m . (Serializable m) + => Condition s m + -> s + -> Message m + -> Bool +check (Condition c) st msg = c st $ decode msg +check (State c) st _ = c st +check (Input c) _ msg = c $ decode msg + +checkCall :: forall s m . (Serializable m) + => Condition s m + -> s + -> Message m + -> Bool +checkCall cond st msg@(CallMessage _ _) = check cond st msg +checkCall _ _ _ = False + +checkCast :: forall s m . (Serializable m) + => Condition s m + -> s + -> Message m + -> Bool +checkCast cond st msg@(CastMessage _) = check cond st msg +checkCast _ _ _ = False + +decode :: Message a -> a +decode (CallMessage a _) = a +decode (CastMessage a) = a + +-- wrapping /normal/ functions with InfoDispatcher -------------------------------------------------------------------------------- -- Process Implementation -- @@ -740,26 +804,26 @@ loop :: [Match (ProcessAction s)] -> s -> Delay -> Process TerminateReason -loop ms def state t = +loop ms def st t = let handleTimeout = timeoutHandler def handleStop = terminateHandler def in do - ac <- processReceive ms handleTimeout state t + ac <- processReceive ms handleTimeout st t case ac of (ProcessContinue s') -> loop ms def s' t (ProcessTimeout t' s') -> loop ms def s' (Delay t') (ProcessHibernate d' s') -> block d' >> loop ms def s' t - (ProcessStop r) -> handleStop state r >> return (r :: TerminateReason) + (ProcessStop r) -> handleStop st r >> return (r :: TerminateReason) where block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) processReceive :: [Match (ProcessAction s)] -> TimeoutHandler s -> s -> Delay -> Process (ProcessAction s) -processReceive ms handleTimeout state d = do +processReceive ms handleTimeout st d = do next <- recv ms d case next of - Nothing -> handleTimeout state d + Nothing -> handleTimeout st d Just pa -> return pa where recv :: [Match (ProcessAction s)] diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index b33758e1..f74d3722 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -6,6 +6,7 @@ module Main where import Control.Concurrent.MVar +import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() @@ -19,6 +20,8 @@ import Data.Binary() import Data.Typeable() import MathsDemo +import Prelude hiding (catch) + import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import TestUtils @@ -112,7 +115,7 @@ testKillMidCall result = do callAsync pid "hello-world" >>= cancelWait >>= unpack result pid where unpack :: TestResult Bool -> ProcessId -> AsyncResult () -> Process () unpack res sid AsyncCancelled = kill sid "stop" >> stash res True - unpack res sid _ = kill sid "stop" >> stash res False + unpack res sid _ = kill sid "stop" >> stash res False -- MathDemo test @@ -148,11 +151,11 @@ mkServer policy = , handleCast (\s' ("ping", pid :: ProcessId) -> send pid "pong" >> continue s') - , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") + , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) (\("timeout", Delay d) -> timeoutAfter_ d) - , action (\("stop") -> stop_ TerminateNormal) - , action (\("hibernate", d :: TimeInterval) -> hibernate_ d) + , handleCast_ (\("stop") -> stop_ TerminateNormal) + , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" @@ -160,7 +163,8 @@ mkServer policy = in do exitReason <- liftIO $ newEmptyMVar pid <- spawnLocal $ do - start () (statelessInit Infinity) s >>= stash exitReason + catch (start () (statelessInit Infinity) s >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) tests :: NT.Transport -> IO [Test] @@ -219,4 +223,3 @@ tests transport = do main :: IO () main = testMain $ tests - From 6e32e7db7d79ab55e280d4b22fb66dac8bc68ae6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 20 Jan 2013 01:21:06 +0000 Subject: [PATCH 0800/2357] re-introduce Counter.hs using GenProcess; rework the APIs a little --- distributed-process-platform.cabal | 3 +- src/Control/Distributed/Process/Platform.hs | 2 +- .../Process/Platform/GenProcess.hs | 166 ++++++++++++------ .../Process/Platform/Internal/Common.hs | 18 ++ 4 files changed, 136 insertions(+), 53 deletions(-) create mode 100644 src/Control/Distributed/Process/Platform/Internal/Common.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 6dcfac73..a426c590 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -119,7 +119,7 @@ test-suite PrimitivesTests extensions: CPP main-is: TestPrimitives.hs -test-suite Tests +test-suite AsyncTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -173,6 +173,7 @@ test-suite GenServerTests other-modules: TestUtils, MathsDemo, + Counter, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index db264b64..8cd893a6 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -21,7 +21,7 @@ module Control.Distributed.Process.Platform , getTag -- common type - , TerminateReason + , TerminateReason(..) -- remote call table , __remoteTable diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 58c5773c..59c66132 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -122,7 +122,7 @@ -- handlers, and this is fine, as is using the 'catchExit' API from -- 'Control.Distributed.Process'. -- --- +-- ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess @@ -133,6 +133,8 @@ module Control.Distributed.Process.Platform.GenProcess , InitResult(..) , ProcessAction(..) , ProcessReply + , CallHandler + , CastHandler , InitHandler , TerminateHandler , TimeoutHandler @@ -140,6 +142,7 @@ module Control.Distributed.Process.Platform.GenProcess , ProcessDefinition(..) -- * Client interaction with the process , start + , defaultProcess , statelessProcess , statelessInit , call @@ -149,8 +152,13 @@ module Control.Distributed.Process.Platform.GenProcess , callTimeout , cast -- * Handler interaction inside the process + , condition + , state + , input , reply , replyWith + , noReply + , noReply_ , continue , continue_ , timeoutAfter @@ -178,15 +186,15 @@ module Control.Distributed.Process.Platform.GenProcess import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Async --- import Control.Distributed.Process.Platform.Async.AsyncSTM +import Control.Distributed.Process.Platform.Async hiding (check) import Control.Distributed.Process.Platform.Internal.Types ( TerminateReason(..)) +import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time -import Data.Binary +import Data.Binary hiding (decode) import Data.DeriveTH -import Data.Typeable (Typeable, typeOf) +import Data.Typeable (Typeable) import Prelude hiding (init) -------------------------------------------------------------------------------- @@ -238,6 +246,17 @@ data ProcessReply s a = ProcessReply a (ProcessAction s) | NoReply (ProcessAction s) +type CallHandler a s = s -> a -> Process (ProcessReply s a) + +type CastHandler s = s -> Process () + +-- type InfoHandler a = forall a b. (Serializable a, Serializable b) => a -> Process b + +data Condition s m = + Condition (s -> m -> Bool) + | State (s -> Bool) + | Input (m -> Bool) + -- | An expression used to initialise a process with its state. type InitHandler a s = a -> Process (InitResult s) @@ -310,6 +329,15 @@ start args init behave = do InitOk s d -> initLoop behave s d >>= return . Right f@(InitFail _) -> return $ Left f +defaultProcess :: ProcessDefinition s +defaultProcess = ProcessDefinition { + dispatchers = [] + , infoHandlers = [] + , timeoutHandler = \s _ -> continue s + , terminateHandler = \_ _ -> return () + , unhandledMessagePolicy = Terminate + } :: ProcessDefinition s + -- | A basic, stateless process definition, where the unhandled message policy -- is set to 'Terminate', the default timeout handlers does nothing (i.e., the -- same as calling @continue ()@ and the terminate handler is a no-op. @@ -335,7 +363,7 @@ call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r unpack (AsyncFailed r) = die $ TerminateOther $ "CALL_FAILED;" ++ show r - unpack ar = die $ TerminateOther $ show (typeOf ar) + unpack ar = die $ TerminateOther $ showTypeRep ar -- | Safe version of 'call' that returns information about the error -- if the operation fails. If an error occurs then the explanation will be @@ -345,7 +373,7 @@ safeCall :: forall a b . (Serializable a, Serializable b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Right r unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r - unpack ar = return $ Left $ TerminateOther $ show (typeOf ar) + unpack ar = return $ Left $ TerminateOther $ showTypeRep ar -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -426,8 +454,18 @@ cast sid msg = send sid (CastMessage msg) -- Producing ProcessAction and ProcessReply from inside handler expressions -- -------------------------------------------------------------------------------- +condition :: forall a b. (Serializable a, Serializable b) + => (a -> b -> Bool) + -> Condition a b +condition = Condition + +state :: forall s m. (Serializable m) => (s -> Bool) -> Condition s m +state = State + +input :: forall s m. (Serializable m) => (m -> Bool) -> Condition s m +input = Input + -- | Instructs the process to send a reply and continue running. --- > reply reply' state = replyWith reply' (continue state) reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) reply r s = continue s >>= replyWith r @@ -436,7 +474,15 @@ replyWith :: (Serializable m) => m -> ProcessAction s -> Process (ProcessReply s m) -replyWith msg state = return $ ProcessReply msg state +replyWith msg st = return $ ProcessReply msg st + +-- | Instructs the process to skip sending a reply /and/ evaluate a 'ProcessAction' +noReply :: (Serializable r) => ProcessAction s -> Process (ProcessReply s r) +noReply = return . NoReply + +-- | Halt a call handler without regard for the expected return type. +noReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) +noReply_ r = stop r >>= noReply -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) @@ -499,7 +545,7 @@ stop_ r _ = stop r handleCall_ :: (Serializable a, Serializable b) => (a -> Process b) -> Dispatcher s -handleCall_ = handleCallIf_ (const True) +handleCall_ = handleCallIf_ $ input (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. This variant ignores the state argument present in 'handleCall' and @@ -507,14 +553,14 @@ handleCall_ = handleCallIf_ (const True) -- only dispatched to the handler if the supplied condition evaluates to @True@ -- -- See 'handleCall' -handleCallIf_ :: (Serializable a, Serializable b) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCallIf_ :: forall s a b . (Serializable a, Serializable b) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (a -> Process b) -- ^ a function from an input message to a reply -> Dispatcher s handleCallIf_ cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheckCall cond + , dispatchIf = checkCall cond } where doHandle :: (Serializable a, Serializable b) => (a -> Process b) @@ -522,8 +568,7 @@ handleCallIf_ cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ _ = error "illegal input" - -- TODO: standard 'this cannot happen' error message + doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -537,7 +582,7 @@ handleCallIf_ cond handler handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s -handleCall = handleCallIf (const True) +handleCall = handleCallIf $ state (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, @@ -545,15 +590,15 @@ handleCall = handleCallIf (const True) -- in a 'Behaviour' specification for the /GenProcess/. Messages are only -- dispatched to the handler if the supplied condition evaluates to @True@ -- -handleCallIf :: (Serializable a, Serializable b) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCallIf :: forall s a b . (Serializable a, Serializable b) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (s -> a -> Process (ProcessReply s b)) -- ^ a reply yielding function over the process state and input message -> Dispatcher s handleCallIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheckCall cond + , dispatchIf = checkCall cond } where doHandle :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) @@ -561,8 +606,7 @@ handleCallIf cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = error "illegal input" - -- TODO: standard 'this cannot happen' error message + doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -576,42 +620,43 @@ handleCallIf cond handler -- > handleCast = handleCastIf (const True) -- handleCast :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleCast = handleCastIf (const True) + => (s -> a -> Process (ProcessAction s)) + -> Dispatcher s +handleCast = handleCastIf $ input (const True) -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, -- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion -- in a 'Behaviour' specification for the /GenProcess/. -- -handleCastIf :: (Serializable a) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCastIf :: forall s a . (Serializable a) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (s -> a -> Process (ProcessAction s)) -- ^ an action yielding function over the process state and input message -> Dispatcher s handleCastIf cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h s p) - , dispatchIf = \_ (CastMessage msg) -> cond msg + , dispatchIf = checkCast cond } -- | Version of 'handleCast' that ignores the server state. -- handleCast_ :: (Serializable a) - => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -handleCast_ = handleCastIf_ (const True) + => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s +handleCast_ = handleCastIf_ $ input (const True) -- | Version of 'handleCastIf' that ignores the server state. -- -handleCastIf_ :: (Serializable a) - => (a -> Bool) -- ^ predicate that must be satisfied for the handler to run +handleCastIf_ :: forall s a . (Serializable a) + => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (a -> (s -> Process (ProcessAction s))) -- ^ a function from the input message to a /stateless action/, cf 'continue_' -> Dispatcher s handleCastIf_ cond h = DispatchIf { dispatch = (\s (CastMessage p) -> h p $ s) - , dispatchIf = \_ (CastMessage msg) -> cond msg + , dispatchIf = checkCast cond } -- | Constructs an /action/ handler. Like 'handleDispatch' this can handle both @@ -637,18 +682,18 @@ action h = handleDispatch perform handleDispatch :: (Serializable a) => (s -> a -> Process (ProcessAction s)) -> Dispatcher s -handleDispatch = handleDispatchIf (const True) +handleDispatch = handleDispatchIf $ input (const True) -- | Constructs a handler for both /call/ and /cast/ messages. Messages are only -- dispatched to the handler if the supplied condition evaluates to @True@. -- -handleDispatchIf :: (Serializable a) - => (a -> Bool) +handleDispatchIf :: forall s a . (Serializable a) + => Condition s a -> (s -> a -> Process (ProcessAction s)) -> Dispatcher s handleDispatchIf cond handler = DispatchIf { dispatch = doHandle handler - , dispatchIf = doCheck cond + , dispatchIf = check cond } where doHandle :: (Serializable a) => (s -> a -> Process (ProcessAction s)) @@ -660,13 +705,6 @@ handleDispatchIf cond handler = DispatchIf { (CallMessage p _) -> (h s p) (CastMessage p) -> (h s p) - doCheck :: forall s a. (Serializable a) - => (a -> Bool) -> s -> Message a -> Bool - doCheck c _ (CallMessage m _) = c m - doCheck c _ (CastMessage m) = c m - --- wrapping /normal/ functions with InfoDispatcher - -- | Creates a generic input handler (i.e., for recieved messages that are /not/ -- sent using the 'cast' or 'call' APIs) from an ordinary function in the -- 'Process' monad. @@ -682,10 +720,36 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) -doCheckCall :: forall s a. (Serializable a) - => (a -> Bool) -> s -> Message a -> Bool -doCheckCall c _ (CallMessage m _) = c m -doCheckCall _ _ _ = False +check :: forall s m . (Serializable m) + => Condition s m + -> s + -> Message m + -> Bool +check (Condition c) st msg = c st $ decode msg +check (State c) st _ = c st +check (Input c) _ msg = c $ decode msg + +checkCall :: forall s m . (Serializable m) + => Condition s m + -> s + -> Message m + -> Bool +checkCall cond st msg@(CallMessage _ _) = check cond st msg +checkCall _ _ _ = False + +checkCast :: forall s m . (Serializable m) + => Condition s m + -> s + -> Message m + -> Bool +checkCast cond st msg@(CastMessage _) = check cond st msg +checkCast _ _ _ = False + +decode :: Message a -> a +decode (CallMessage a _) = a +decode (CastMessage a) = a + +-- wrapping /normal/ functions with InfoDispatcher -------------------------------------------------------------------------------- -- Process Implementation -- @@ -740,26 +804,26 @@ loop :: [Match (ProcessAction s)] -> s -> Delay -> Process TerminateReason -loop ms def state t = +loop ms def st t = let handleTimeout = timeoutHandler def handleStop = terminateHandler def in do - ac <- processReceive ms handleTimeout state t + ac <- processReceive ms handleTimeout st t case ac of (ProcessContinue s') -> loop ms def s' t (ProcessTimeout t' s') -> loop ms def s' (Delay t') (ProcessHibernate d' s') -> block d' >> loop ms def s' t - (ProcessStop r) -> handleStop state r >> return (r :: TerminateReason) + (ProcessStop r) -> handleStop st r >> return (r :: TerminateReason) where block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) processReceive :: [Match (ProcessAction s)] -> TimeoutHandler s -> s -> Delay -> Process (ProcessAction s) -processReceive ms handleTimeout state d = do +processReceive ms handleTimeout st d = do next <- recv ms d case next of - Nothing -> handleTimeout state d + Nothing -> handleTimeout st d Just pa -> return pa where recv :: [Match (ProcessAction s)] diff --git a/src/Control/Distributed/Process/Platform/Internal/Common.hs b/src/Control/Distributed/Process/Platform/Internal/Common.hs new file mode 100644 index 00000000..6b5ec05b --- /dev/null +++ b/src/Control/Distributed/Process/Platform/Internal/Common.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE ExistentialQuantification #-} + +module Control.Distributed.Process.Platform.Internal.Common where + +import Control.Distributed.Process (Process, die) +import Control.Distributed.Process.Platform.Internal.Types +import Control.Distributed.Process.Serializable +import Data.Typeable (Typeable, typeOf) --, splitTyConApp) + +failTypeCheck :: forall a b . (Serializable a) => a -> Process b +failTypeCheck m = failUnexpectedType "FAILED_TYPE_CHECK :-" m + +failUnexpectedType :: forall a b . (Serializable a) => String -> a -> Process b +failUnexpectedType s m = die $ TerminateOther $ s ++ showTypeRep m + +showTypeRep :: forall a. (Typeable a) => a -> String +showTypeRep m = show $ typeOf m + From 93897eff9dd2da053814c5bd0668b46d5d7f1c12 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 20 Jan 2013 01:21:06 +0000 Subject: [PATCH 0801/2357] re-introduce Counter.hs using GenProcess; rework the APIs a little --- distributed-process-platform.cabal | 3 ++- tests/TestGenServer.hs | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 6dcfac73..a426c590 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -119,7 +119,7 @@ test-suite PrimitivesTests extensions: CPP main-is: TestPrimitives.hs -test-suite Tests +test-suite AsyncTests type: exitcode-stdio-1.0 x-uses-tf: true build-depends: @@ -173,6 +173,7 @@ test-suite GenServerTests other-modules: TestUtils, MathsDemo, + Counter, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index b33758e1..f74d3722 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -6,6 +6,7 @@ module Main where import Control.Concurrent.MVar +import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node import Control.Distributed.Process.Serializable() @@ -19,6 +20,8 @@ import Data.Binary() import Data.Typeable() import MathsDemo +import Prelude hiding (catch) + import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import TestUtils @@ -112,7 +115,7 @@ testKillMidCall result = do callAsync pid "hello-world" >>= cancelWait >>= unpack result pid where unpack :: TestResult Bool -> ProcessId -> AsyncResult () -> Process () unpack res sid AsyncCancelled = kill sid "stop" >> stash res True - unpack res sid _ = kill sid "stop" >> stash res False + unpack res sid _ = kill sid "stop" >> stash res False -- MathDemo test @@ -148,11 +151,11 @@ mkServer policy = , handleCast (\s' ("ping", pid :: ProcessId) -> send pid "pong" >> continue s') - , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") + , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) (\("timeout", Delay d) -> timeoutAfter_ d) - , action (\("stop") -> stop_ TerminateNormal) - , action (\("hibernate", d :: TimeInterval) -> hibernate_ d) + , handleCast_ (\("stop") -> stop_ TerminateNormal) + , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" @@ -160,7 +163,8 @@ mkServer policy = in do exitReason <- liftIO $ newEmptyMVar pid <- spawnLocal $ do - start () (statelessInit Infinity) s >>= stash exitReason + catch (start () (statelessInit Infinity) s >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) tests :: NT.Transport -> IO [Test] @@ -219,4 +223,3 @@ tests transport = do main :: IO () main = testMain $ tests - From e61bd219aaffadb7247fd051f2e0af1e594b05ac Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 20 Jan 2013 01:46:54 +0000 Subject: [PATCH 0802/2357] oops - better state that invariant properly! --- tests/Counter.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Counter.hs b/tests/Counter.hs index 5b91902a..13122cf7 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -74,7 +74,7 @@ startCounter :: Int -> Process ProcessId startCounter startCount = let server = defaultProcess { dispatchers = [ - handleCallIf (state (\count -> count > 10)) -- invariant + handleCallIf (state (\count -> count <= 10)) -- invariant (\_ (_ :: Increment) -> noReply_ (TerminateOther "Count > 10")) From f99f299b52cce3c67a64887684a08f3b8a5dac56 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:39:21 +0000 Subject: [PATCH 0803/2357] fix nasty bug in GenProcess callAsync We previously attempted to force the content within the async result into 'die/exit' which crashed on attempting to serialise some data structures. --- .../Process/Platform/GenProcess.hs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 59c66132..1ae3c309 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -361,9 +361,11 @@ call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b - unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ TerminateOther $ "CALL_FAILED;" ++ show r - unpack ar = die $ TerminateOther $ showTypeRep ar + unpack (AsyncDone r) = return r + unpack (AsyncFailed r) = die $ TerminateOther $ "CallFailed; " ++ show r + unpack (AsyncLinkFailed r) = die $ TerminateOther $ "LinkFailed; " ++ show r + unpack AsyncCancelled = die $ TerminateOther $ "Cancelled" + unpack AsyncPending = terminate -- as this *cannot* happen -- | Safe version of 'call' that returns information about the error -- if the operation fails. If an error occurs then the explanation will be @@ -371,9 +373,12 @@ call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Either TerminateReason b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] - where unpack (AsyncDone r) = return $ Right r - unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r - unpack ar = return $ Left $ TerminateOther $ showTypeRep ar + where unpack (AsyncDone r) = return $ Right r + unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r + unpack (AsyncLinkFailed r) = return $ Left $ TerminateOther $ show r + unpack AsyncCancelled = return $ Left $ TerminateOther $ "Cancelled" + unpack AsyncPending = return $ Left $ TerminateOther $ "Pending" +-- unpack ar = return $ Left $ TerminateOther $ showTypeRep ar -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -420,9 +425,10 @@ callAsync sid msg = do (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) ] -- TODO: better failure API + unmonitor mRef case r of Right m -> return m - Left err -> fail $ "call: remote process died: " ++ show err + Left err -> (say $ "call: remote process died: " ++ show err) >> terminate -- note [call using async] -- One problem with using plain expect/receive primitives to perform a From 01e30a56132ccff5d2b19708a26f214c8a0bfa24 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:39:21 +0000 Subject: [PATCH 0804/2357] fix nasty bug in GenProcess callAsync We previously attempted to force the content within the async result into 'die/exit' which crashed on attempting to serialise some data structures. --- .../Process/Platform/GenProcess.hs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 59c66132..1ae3c309 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -361,9 +361,11 @@ call :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process b call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b - unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ TerminateOther $ "CALL_FAILED;" ++ show r - unpack ar = die $ TerminateOther $ showTypeRep ar + unpack (AsyncDone r) = return r + unpack (AsyncFailed r) = die $ TerminateOther $ "CallFailed; " ++ show r + unpack (AsyncLinkFailed r) = die $ TerminateOther $ "LinkFailed; " ++ show r + unpack AsyncCancelled = die $ TerminateOther $ "Cancelled" + unpack AsyncPending = terminate -- as this *cannot* happen -- | Safe version of 'call' that returns information about the error -- if the operation fails. If an error occurs then the explanation will be @@ -371,9 +373,12 @@ call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Either TerminateReason b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] - where unpack (AsyncDone r) = return $ Right r - unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r - unpack ar = return $ Left $ TerminateOther $ showTypeRep ar + where unpack (AsyncDone r) = return $ Right r + unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r + unpack (AsyncLinkFailed r) = return $ Left $ TerminateOther $ show r + unpack AsyncCancelled = return $ Left $ TerminateOther $ "Cancelled" + unpack AsyncPending = return $ Left $ TerminateOther $ "Pending" +-- unpack ar = return $ Left $ TerminateOther $ showTypeRep ar -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -420,9 +425,10 @@ callAsync sid msg = do (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) ] -- TODO: better failure API + unmonitor mRef case r of Right m -> return m - Left err -> fail $ "call: remote process died: " ++ show err + Left err -> (say $ "call: remote process died: " ++ show err) >> terminate -- note [call using async] -- One problem with using plain expect/receive primitives to perform a From 3accc7585a5d1726e51b67a80f89b6f02e4b169d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:40:23 +0000 Subject: [PATCH 0805/2357] don't try to use the TypeRep in 'die' as this can fail horribly --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 1ae3c309..2fa1d30a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -574,7 +574,7 @@ handleCallIf_ cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -612,7 +612,7 @@ handleCallIf cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop From c09a281820cdd792a0db3ccb842491b00dae3608 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:40:23 +0000 Subject: [PATCH 0806/2357] don't try to use the TypeRep in 'die' as this can fail horribly --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 1ae3c309..2fa1d30a 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -574,7 +574,7 @@ handleCallIf_ cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -612,7 +612,7 @@ handleCallIf cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ m = failUnexpectedType "CALL_HANDLER_TYPE_MISMATCH :- " m + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop From 02e667aeea1bf40bee3bd90884d1a2294021928d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:42:24 +0000 Subject: [PATCH 0807/2357] fix state propagation bug in GenProcess main loop Forgetting to recalculate the match specification in each recursive call led to stale state being returned from executing callbacks. --- .../Process/Platform/GenProcess.hs | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 2fa1d30a..00f05d78 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -326,7 +326,7 @@ start :: a start args init behave = do ir <- init args case ir of - InitOk s d -> initLoop behave s d >>= return . Right + InitOk s d -> loop behave s d >>= return . Right f@(InitFail _) -> return $ Left f defaultProcess :: ProcessDefinition s @@ -771,13 +771,24 @@ applyPolicy s p m = DeadLetter pid -> forward m pid >> continue s Drop -> continue s -initLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason -initLoop b s w = - let p = unhandledMessagePolicy b - ms = map (matchMessage p s) (dispatchers b) - ms' = ms ++ addInfoAux p s (infoHandlers b) - in loop ms' b s w +loop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason +loop pDef pState recvDelay = + let p = unhandledMessagePolicy pDef + handleTimeout = timeoutHandler pDef + handleStop = terminateHandler pDef + ms = map (matchMessage p pState) (dispatchers pDef) + ms' = ms ++ addInfoAux p pState (infoHandlers pDef) + in do + ac <- processReceive ms' handleTimeout pState recvDelay + case ac of + (ProcessContinue s') -> loop pDef s' recvDelay + (ProcessTimeout t' s') -> loop pDef s' (Delay t') + (ProcessHibernate d' s') -> block d' >> loop pDef s' recvDelay + (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) where + block :: TimeInterval -> Process () + block i = liftIO $ threadDelay (asTimeout i) + addInfoAux :: UnhandledMessagePolicy -> s -> [InfoDispatcher s] @@ -805,24 +816,6 @@ initLoop b s w = Nothing -> applyPolicy st pol msg Just act -> return act -loop :: [Match (ProcessAction s)] - -> ProcessDefinition s - -> s - -> Delay - -> Process TerminateReason -loop ms def st t = - let handleTimeout = timeoutHandler def - handleStop = terminateHandler def - in do - ac <- processReceive ms handleTimeout st t - case ac of - (ProcessContinue s') -> loop ms def s' t - (ProcessTimeout t' s') -> loop ms def s' (Delay t') - (ProcessHibernate d' s') -> block d' >> loop ms def s' t - (ProcessStop r) -> handleStop st r >> return (r :: TerminateReason) - where block :: TimeInterval -> Process () - block i = liftIO $ threadDelay (asTimeout i) - processReceive :: [Match (ProcessAction s)] -> TimeoutHandler s -> s -> Delay -> Process (ProcessAction s) From 3e774c10117b58bcd3032d5e036611f7a91f1ff3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:42:24 +0000 Subject: [PATCH 0808/2357] fix state propagation bug in GenProcess main loop Forgetting to recalculate the match specification in each recursive call led to stale state being returned from executing callbacks. --- .../Process/Platform/GenProcess.hs | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 2fa1d30a..00f05d78 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -326,7 +326,7 @@ start :: a start args init behave = do ir <- init args case ir of - InitOk s d -> initLoop behave s d >>= return . Right + InitOk s d -> loop behave s d >>= return . Right f@(InitFail _) -> return $ Left f defaultProcess :: ProcessDefinition s @@ -771,13 +771,24 @@ applyPolicy s p m = DeadLetter pid -> forward m pid >> continue s Drop -> continue s -initLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason -initLoop b s w = - let p = unhandledMessagePolicy b - ms = map (matchMessage p s) (dispatchers b) - ms' = ms ++ addInfoAux p s (infoHandlers b) - in loop ms' b s w +loop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason +loop pDef pState recvDelay = + let p = unhandledMessagePolicy pDef + handleTimeout = timeoutHandler pDef + handleStop = terminateHandler pDef + ms = map (matchMessage p pState) (dispatchers pDef) + ms' = ms ++ addInfoAux p pState (infoHandlers pDef) + in do + ac <- processReceive ms' handleTimeout pState recvDelay + case ac of + (ProcessContinue s') -> loop pDef s' recvDelay + (ProcessTimeout t' s') -> loop pDef s' (Delay t') + (ProcessHibernate d' s') -> block d' >> loop pDef s' recvDelay + (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) where + block :: TimeInterval -> Process () + block i = liftIO $ threadDelay (asTimeout i) + addInfoAux :: UnhandledMessagePolicy -> s -> [InfoDispatcher s] @@ -805,24 +816,6 @@ initLoop b s w = Nothing -> applyPolicy st pol msg Just act -> return act -loop :: [Match (ProcessAction s)] - -> ProcessDefinition s - -> s - -> Delay - -> Process TerminateReason -loop ms def st t = - let handleTimeout = timeoutHandler def - handleStop = terminateHandler def - in do - ac <- processReceive ms handleTimeout st t - case ac of - (ProcessContinue s') -> loop ms def s' t - (ProcessTimeout t' s') -> loop ms def s' (Delay t') - (ProcessHibernate d' s') -> block d' >> loop ms def s' t - (ProcessStop r) -> handleStop st r >> return (r :: TerminateReason) - where block :: TimeInterval -> Process () - block i = liftIO $ threadDelay (asTimeout i) - processReceive :: [Match (ProcessAction s)] -> TimeoutHandler s -> s -> Delay -> Process (ProcessAction s) From a9c572a7236723c15b45974177fede9860d46104 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:42:49 +0000 Subject: [PATCH 0809/2357] quieten the compiler --- tests/TestUtils.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 78837c87..4e440421 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -90,7 +90,7 @@ newLogger :: IO Logger newLogger = do tid <- liftIO $ myThreadId q <- liftIO $ newTQueueIO - forkIO $ logger q + _ <- forkIO $ logger q return $ Logger tid q where logger q' = forever $ do msg <- atomically $ readTQueue q' From f5fe0770a98d48e929c7706e17907758a0ea6a39 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:42:49 +0000 Subject: [PATCH 0810/2357] quieten the compiler --- tests/TestUtils.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestUtils.hs b/tests/TestUtils.hs index 78837c87..4e440421 100644 --- a/tests/TestUtils.hs +++ b/tests/TestUtils.hs @@ -90,7 +90,7 @@ newLogger :: IO Logger newLogger = do tid <- liftIO $ myThreadId q <- liftIO $ newTQueueIO - forkIO $ logger q + _ <- forkIO $ logger q return $ Logger tid q where logger q' = forever $ do msg <- atomically $ readTQueue q' From 8d2884af6dab3ef39c3c9e57990367dbe39d86f6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:44:01 +0000 Subject: [PATCH 0811/2357] As per ef0b63 - apparently we cannot work with *any* TypeRep here --- .../Distributed/Process/Platform/Internal/Common.hs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/Internal/Common.hs b/src/Control/Distributed/Process/Platform/Internal/Common.hs index 6b5ec05b..22450709 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Common.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Common.hs @@ -4,14 +4,13 @@ module Control.Distributed.Process.Platform.Internal.Common where import Control.Distributed.Process (Process, die) import Control.Distributed.Process.Platform.Internal.Types -import Control.Distributed.Process.Serializable -import Data.Typeable (Typeable, typeOf) --, splitTyConApp) +import Data.Typeable (Typeable, typeOf) -failTypeCheck :: forall a b . (Serializable a) => a -> Process b -failTypeCheck m = failUnexpectedType "FAILED_TYPE_CHECK :-" m +failTypeCheck :: Process b +failTypeCheck = failUnexpectedType "FAILED_TYPE_CHECK :-" -failUnexpectedType :: forall a b . (Serializable a) => String -> a -> Process b -failUnexpectedType s m = die $ TerminateOther $ s ++ showTypeRep m +failUnexpectedType :: String -> Process b +failUnexpectedType s = die $ TerminateOther s showTypeRep :: forall a. (Typeable a) => a -> String showTypeRep m = show $ typeOf m From 64bcca40400617028e4409d2c1d8d8657c93f212 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:45:20 +0000 Subject: [PATCH 0812/2357] quiet warnings, quiet --- src/Control/Distributed/Process/Platform.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 8cd893a6..ad861276 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -29,7 +29,7 @@ module Control.Distributed.Process.Platform import Control.Distributed.Process import Control.Distributed.Process.Platform.Internal.Types - ( TerminateReason + ( TerminateReason(..) , Tag , TagPool , newTagPool From c9c778f781acbcbca30c39fc33910d93ecb9bfad Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:45:40 +0000 Subject: [PATCH 0813/2357] re-organise Platform exports --- src/Control/Distributed/Process/Platform.hs | 27 ++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index ad861276..854ef364 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -2,28 +2,27 @@ -- module Control.Distributed.Process.Platform ( - -- extra primitives - spawnLinkLocal + -- * Exported Types + TerminateReason(..) + , Tag + , TagPool + + -- * Utilities and Extended Primitives + , spawnLinkLocal , spawnMonitorLocal , linkOnFailure - - -- registration/start - , whereisOrStart - , whereisOrStartRemote - - -- matching + , times , matchCond - -- tags - , Tag - , TagPool + -- * Call/Tagging support , newTagPool , getTag - -- common type - , TerminateReason(..) + -- * Registration and Process Lookup + , whereisOrStart + , whereisOrStartRemote - -- remote call table + -- remote call table , __remoteTable ) where From c2f6ba35d8472e905cb598af1b562e802c43bc2b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:49:15 +0000 Subject: [PATCH 0814/2357] Fiddle with the Counter server and tests until we have something working However, there is clearly a nasty bug in GenProcess 'call', so we have some disabled test cases for the time being. --- tests/TestGenServer.hs | 102 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index f74d3722..961ace27 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,5 +1,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE TemplateHaskell #-} -- NB: this module contains tests for the GenProcess /and/ GenServer API. @@ -9,16 +12,19 @@ import Control.Concurrent.MVar import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer +import Control.Distributed.Process.Serializable() -import Data.Binary() -import Data.Typeable() +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH import MathsDemo +import Counter import Prelude hiding (catch) @@ -28,6 +34,10 @@ import TestUtils import qualified Network.Transport as NT +data GetState = GetState + deriving (Typeable, Show, Eq) +$(derive makeBinary ''GetState) + testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -63,7 +73,7 @@ testDropPolicy result = do send pid ("UNSOLICITED_MAIL", 500 :: Int) - sleep $ seconds 1 + sleep $ milliSeconds 250 mref <- monitor pid cast pid "stop" @@ -117,11 +127,52 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False --- MathDemo test +testStateHandling :: TestResult Bool -> Process () +testStateHandling result = do + pid <- statefulServer "ok" + cast pid ("ko" :: String) -- updateState + liftIO $ putStrLn "cast sent!" + s2 <- call pid GetState -- getState + say $ "s2 = " ++ s2 + sleep $ seconds 2 + stash result (s2 == "ko") + +-- MathDemo tests + +testAdd :: ProcessId -> TestResult Double -> Process () +testAdd pid result = add pid 10 10 >>= stash result testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () testDivByZero pid result = divide pid 125 0 >>= stash result +-- Counter tests + +testCounterCurrentState :: ProcessId -> TestResult Int -> Process () +testCounterCurrentState pid result = getCount pid >>= stash result + +testCounterIncrement :: ProcessId -> TestResult Int -> Process () +testCounterIncrement pid result = do + 6 <- incCount pid + 7 <- incCount pid + getCount pid >>= stash result + +testCounterExceedsLimit :: ProcessId -> TestResult Bool -> Process () +testCounterExceedsLimit pid result = do + _ <- monitor pid + 8 <- incCount pid + 9 <- incCount pid + 10 <- incCount pid + sleep $ seconds 1 + _ <- incCount pid + sleep $ seconds 1 + r <- receiveWait [ + match (\(ProcessMonitorNotification _ _ r') -> return r') + ] + liftIO $ putStrLn $ "counter died with " ++ (show r) + pInfo <- getProcessInfo pid + liftIO $ putStrLn $ "pinfo = " ++ (show pInfo) + stash result True + -- utilities waitForExit :: MVar (Either (InitResult ()) TerminateReason) @@ -146,7 +197,11 @@ mkServer policy = dispatchers = [ -- note: state is passed here, as a 'stateless' process is -- in fact process definition whose state is () - handleCall (\s' (m :: String) -> reply m s') + + handleCastIf (input (\msg -> msg == "stop")) + (\_ _ -> stop TerminateNormal) + + , handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" , handleCast (\s' ("ping", pid :: ProcessId) -> @@ -154,7 +209,6 @@ mkServer policy = , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) (\("timeout", Delay d) -> timeoutAfter_ d) - , handleCast_ (\("stop") -> stop_ TerminateNormal) , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy @@ -167,12 +221,27 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) +statefulServer :: String -> Process ProcessId +statefulServer st = + let b = defaultProcess { + dispatchers = [ + handleCast (\_ new -> continue new) + , handleCall (\s GetState -> reply s s) + ] + } :: ProcessDefinition String + in spawnLocal $ start st init' b >> return () + where init' :: String -> Process (InitResult String) + init' initS = return $ InitOk initS Infinity + tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable mpid <- newEmptyMVar _ <- forkProcess localNode $ launchMathServer >>= stash mpid pid <- takeMVar mpid + cpid <- newEmptyMVar + _ <- forkProcess localNode $ startCounter 5 >>= stash cpid + counter <- takeMVar cpid return [ testGroup "basic server functionality" [ testCase "basic call with explicit server reply" @@ -218,8 +287,27 @@ tests transport = do (delayedAssertion "expected the server to return DivByZero" localNode (Left DivByZero) (testDivByZero pid)) + , testCase "10 + 10 = 20" + (delayedAssertion + "expected the server to return DivByZero" + localNode 20 (testAdd pid)) + ] + , testGroup "counter server examples" [ + testCase "initial counter state = 5" + (delayedAssertion + "expected the server to return the initial state of 5" + localNode 5 (testCounterCurrentState counter)) + , testCase "increment counter twice" + (delayedAssertion + "expected the server to return the incremented state as 7" + localNode 7 (testCounterIncrement counter)) + -- , testCase "exceed counter limits" + -- (delayedAssertion + -- "expected the server to terminate once the limit was exceeded" + -- localNode True (testCounterExceedsLimit counter)) ] ] main :: IO () main = testMain $ tests + From 5f5c4f693ffb35d5ad01df2d12607c1c87a6d4bd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:49:15 +0000 Subject: [PATCH 0815/2357] Fiddle with the Counter server and tests until we have something working However, there is clearly a nasty bug in GenProcess 'call', so we have some disabled test cases for the time being. --- tests/TestGenServer.hs | 102 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index f74d3722..961ace27 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,5 +1,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE TemplateHaskell #-} -- NB: this module contains tests for the GenProcess /and/ GenServer API. @@ -9,16 +12,19 @@ import Control.Concurrent.MVar import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer +import Control.Distributed.Process.Serializable() -import Data.Binary() -import Data.Typeable() +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH import MathsDemo +import Counter import Prelude hiding (catch) @@ -28,6 +34,10 @@ import TestUtils import qualified Network.Transport as NT +data GetState = GetState + deriving (Typeable, Show, Eq) +$(derive makeBinary ''GetState) + testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -63,7 +73,7 @@ testDropPolicy result = do send pid ("UNSOLICITED_MAIL", 500 :: Int) - sleep $ seconds 1 + sleep $ milliSeconds 250 mref <- monitor pid cast pid "stop" @@ -117,11 +127,52 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False --- MathDemo test +testStateHandling :: TestResult Bool -> Process () +testStateHandling result = do + pid <- statefulServer "ok" + cast pid ("ko" :: String) -- updateState + liftIO $ putStrLn "cast sent!" + s2 <- call pid GetState -- getState + say $ "s2 = " ++ s2 + sleep $ seconds 2 + stash result (s2 == "ko") + +-- MathDemo tests + +testAdd :: ProcessId -> TestResult Double -> Process () +testAdd pid result = add pid 10 10 >>= stash result testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () testDivByZero pid result = divide pid 125 0 >>= stash result +-- Counter tests + +testCounterCurrentState :: ProcessId -> TestResult Int -> Process () +testCounterCurrentState pid result = getCount pid >>= stash result + +testCounterIncrement :: ProcessId -> TestResult Int -> Process () +testCounterIncrement pid result = do + 6 <- incCount pid + 7 <- incCount pid + getCount pid >>= stash result + +testCounterExceedsLimit :: ProcessId -> TestResult Bool -> Process () +testCounterExceedsLimit pid result = do + _ <- monitor pid + 8 <- incCount pid + 9 <- incCount pid + 10 <- incCount pid + sleep $ seconds 1 + _ <- incCount pid + sleep $ seconds 1 + r <- receiveWait [ + match (\(ProcessMonitorNotification _ _ r') -> return r') + ] + liftIO $ putStrLn $ "counter died with " ++ (show r) + pInfo <- getProcessInfo pid + liftIO $ putStrLn $ "pinfo = " ++ (show pInfo) + stash result True + -- utilities waitForExit :: MVar (Either (InitResult ()) TerminateReason) @@ -146,7 +197,11 @@ mkServer policy = dispatchers = [ -- note: state is passed here, as a 'stateless' process is -- in fact process definition whose state is () - handleCall (\s' (m :: String) -> reply m s') + + handleCastIf (input (\msg -> msg == "stop")) + (\_ _ -> stop TerminateNormal) + + , handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" , handleCast (\s' ("ping", pid :: ProcessId) -> @@ -154,7 +209,6 @@ mkServer policy = , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) (\("timeout", Delay d) -> timeoutAfter_ d) - , handleCast_ (\("stop") -> stop_ TerminateNormal) , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy @@ -167,12 +221,27 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) +statefulServer :: String -> Process ProcessId +statefulServer st = + let b = defaultProcess { + dispatchers = [ + handleCast (\_ new -> continue new) + , handleCall (\s GetState -> reply s s) + ] + } :: ProcessDefinition String + in spawnLocal $ start st init' b >> return () + where init' :: String -> Process (InitResult String) + init' initS = return $ InitOk initS Infinity + tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable mpid <- newEmptyMVar _ <- forkProcess localNode $ launchMathServer >>= stash mpid pid <- takeMVar mpid + cpid <- newEmptyMVar + _ <- forkProcess localNode $ startCounter 5 >>= stash cpid + counter <- takeMVar cpid return [ testGroup "basic server functionality" [ testCase "basic call with explicit server reply" @@ -218,8 +287,27 @@ tests transport = do (delayedAssertion "expected the server to return DivByZero" localNode (Left DivByZero) (testDivByZero pid)) + , testCase "10 + 10 = 20" + (delayedAssertion + "expected the server to return DivByZero" + localNode 20 (testAdd pid)) + ] + , testGroup "counter server examples" [ + testCase "initial counter state = 5" + (delayedAssertion + "expected the server to return the initial state of 5" + localNode 5 (testCounterCurrentState counter)) + , testCase "increment counter twice" + (delayedAssertion + "expected the server to return the incremented state as 7" + localNode 7 (testCounterIncrement counter)) + -- , testCase "exceed counter limits" + -- (delayedAssertion + -- "expected the server to terminate once the limit was exceeded" + -- localNode True (testCounterExceedsLimit counter)) ] ] main :: IO () main = testMain $ tests + From d0c6f9770a7a4e30710e1842cca2449bea28c1ae Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 21 Jan 2013 17:49:15 +0000 Subject: [PATCH 0816/2357] Fiddle with the Counter server and tests until we have something working However, there is clearly a nasty bug in GenProcess 'call', so we have some disabled test cases for the time being. --- tests/Counter.hs | 45 ++++++++++-------- tests/TestGenServer.hs | 102 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 27 deletions(-) diff --git a/tests/Counter.hs b/tests/Counter.hs index 13122cf7..fa511380 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE BangPatterns #-} module Counter ( startCounter, @@ -52,10 +53,7 @@ incCount sid = call sid Increment -- | Get the current count - this is replicating what 'call' actually does getCount :: ProcessId -> Process Int -getCount sid = getCountAsync sid >>= wait >>= unpack - where unpack :: AsyncResult Int -> Process Int - unpack (AsyncDone i) = return i - unpack asyncOther = die asyncOther +getCount sid = call sid Fetch -- | Get the current count asynchronously getCountAsync :: ProcessId -> Process (Async Int) @@ -65,31 +63,38 @@ getCountAsync sid = callAsync sid Fetch resetCount :: ProcessId -> Process () resetCount sid = cast sid Reset +-- | Start a counter server +startCounter :: Int -> Process ProcessId +startCounter startCount = + let server = serverDefinition + in spawnLocal $ start startCount init' server >> return () + where init' :: InitHandler Int Int + init' count = return $ InitOk count Infinity + -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- --- | Start a counter server -startCounter :: Int -> Process ProcessId -startCounter startCount = - let server = defaultProcess { +serverDefinition :: ProcessDefinition State +serverDefinition = defaultProcess { dispatchers = [ - handleCallIf (state (\count -> count <= 10)) -- invariant - (\_ (_ :: Increment) -> - noReply_ (TerminateOther "Count > 10")) + handleCallIf (condition (\count Increment -> count >= 10))-- invariant + (\_ (_ :: Increment) -> do + say "terminating...." + noReply_ (TerminateOther "Count > 10")) , handleCall handleIncrement - , handleCall (\count (_ :: Fetch) -> reply count count) - , handleCast (\_ Fetch -> continue 0) + , handleCall (\count Fetch -> reply count count) + , handleCast (\_ Reset -> continue 0) ] + , terminateHandler = (\s r -> do + say $ "terminating counter when state = " ++ (show s) ++ " because " ++ show r) } :: ProcessDefinition State - in spawnLocal $ start startCount init' server >> return () - where init' :: InitHandler Int Int - init' count = return $ InitOk count Infinity handleIncrement :: State -> Increment -> Process (ProcessReply State Int) -handleIncrement count _ = - let newCount = count + 1 in do - next <- continue newCount - replyWith newCount next +handleIncrement count Increment = do + next <- increment + replyWith (count + 1) $ next + where increment :: Process (ProcessAction State) + !increment = return (ProcessContinue (count + 1)) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index f74d3722..961ace27 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -1,5 +1,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE TemplateHaskell #-} -- NB: this module contains tests for the GenProcess /and/ GenServer API. @@ -9,16 +12,19 @@ import Control.Concurrent.MVar import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Node -import Control.Distributed.Process.Serializable() +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer +import Control.Distributed.Process.Serializable() -import Data.Binary() -import Data.Typeable() +import Data.Binary +import Data.Typeable (Typeable) +import Data.DeriveTH import MathsDemo +import Counter import Prelude hiding (catch) @@ -28,6 +34,10 @@ import TestUtils import qualified Network.Transport as NT +data GetState = GetState + deriving (Typeable, Show, Eq) +$(derive makeBinary ''GetState) + testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -63,7 +73,7 @@ testDropPolicy result = do send pid ("UNSOLICITED_MAIL", 500 :: Int) - sleep $ seconds 1 + sleep $ milliSeconds 250 mref <- monitor pid cast pid "stop" @@ -117,11 +127,52 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False --- MathDemo test +testStateHandling :: TestResult Bool -> Process () +testStateHandling result = do + pid <- statefulServer "ok" + cast pid ("ko" :: String) -- updateState + liftIO $ putStrLn "cast sent!" + s2 <- call pid GetState -- getState + say $ "s2 = " ++ s2 + sleep $ seconds 2 + stash result (s2 == "ko") + +-- MathDemo tests + +testAdd :: ProcessId -> TestResult Double -> Process () +testAdd pid result = add pid 10 10 >>= stash result testDivByZero :: ProcessId -> TestResult (Either DivByZero Double) -> Process () testDivByZero pid result = divide pid 125 0 >>= stash result +-- Counter tests + +testCounterCurrentState :: ProcessId -> TestResult Int -> Process () +testCounterCurrentState pid result = getCount pid >>= stash result + +testCounterIncrement :: ProcessId -> TestResult Int -> Process () +testCounterIncrement pid result = do + 6 <- incCount pid + 7 <- incCount pid + getCount pid >>= stash result + +testCounterExceedsLimit :: ProcessId -> TestResult Bool -> Process () +testCounterExceedsLimit pid result = do + _ <- monitor pid + 8 <- incCount pid + 9 <- incCount pid + 10 <- incCount pid + sleep $ seconds 1 + _ <- incCount pid + sleep $ seconds 1 + r <- receiveWait [ + match (\(ProcessMonitorNotification _ _ r') -> return r') + ] + liftIO $ putStrLn $ "counter died with " ++ (show r) + pInfo <- getProcessInfo pid + liftIO $ putStrLn $ "pinfo = " ++ (show pInfo) + stash result True + -- utilities waitForExit :: MVar (Either (InitResult ()) TerminateReason) @@ -146,7 +197,11 @@ mkServer policy = dispatchers = [ -- note: state is passed here, as a 'stateless' process is -- in fact process definition whose state is () - handleCall (\s' (m :: String) -> reply m s') + + handleCastIf (input (\msg -> msg == "stop")) + (\_ _ -> stop TerminateNormal) + + , handleCall (\s' (m :: String) -> reply m s') , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" , handleCast (\s' ("ping", pid :: ProcessId) -> @@ -154,7 +209,6 @@ mkServer policy = , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) (\("timeout", Delay d) -> timeoutAfter_ d) - , handleCast_ (\("stop") -> stop_ TerminateNormal) , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) ] , unhandledMessagePolicy = policy @@ -167,12 +221,27 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) +statefulServer :: String -> Process ProcessId +statefulServer st = + let b = defaultProcess { + dispatchers = [ + handleCast (\_ new -> continue new) + , handleCall (\s GetState -> reply s s) + ] + } :: ProcessDefinition String + in spawnLocal $ start st init' b >> return () + where init' :: String -> Process (InitResult String) + init' initS = return $ InitOk initS Infinity + tests :: NT.Transport -> IO [Test] tests transport = do localNode <- newLocalNode transport initRemoteTable mpid <- newEmptyMVar _ <- forkProcess localNode $ launchMathServer >>= stash mpid pid <- takeMVar mpid + cpid <- newEmptyMVar + _ <- forkProcess localNode $ startCounter 5 >>= stash cpid + counter <- takeMVar cpid return [ testGroup "basic server functionality" [ testCase "basic call with explicit server reply" @@ -218,8 +287,27 @@ tests transport = do (delayedAssertion "expected the server to return DivByZero" localNode (Left DivByZero) (testDivByZero pid)) + , testCase "10 + 10 = 20" + (delayedAssertion + "expected the server to return DivByZero" + localNode 20 (testAdd pid)) + ] + , testGroup "counter server examples" [ + testCase "initial counter state = 5" + (delayedAssertion + "expected the server to return the initial state of 5" + localNode 5 (testCounterCurrentState counter)) + , testCase "increment counter twice" + (delayedAssertion + "expected the server to return the incremented state as 7" + localNode 7 (testCounterIncrement counter)) + -- , testCase "exceed counter limits" + -- (delayedAssertion + -- "expected the server to terminate once the limit was exceeded" + -- localNode True (testCounterExceedsLimit counter)) ] ] main :: IO () main = testMain $ tests + From e967ee862702ead73049eb7f79f29bd4c486553f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 01:54:50 +0000 Subject: [PATCH 0817/2357] Ensure that GenProcess handles crashes whilst sync calls are underway A cleaner API for terminating (or informing) the client has been added. Additionally the Counter server example and test cases have been updated to reflect the changes. --- distributed-process-platform.cabal | 11 +------ .../Process/Platform/GenProcess.hs | 16 ++++----- tests/TestGenServer.hs | 33 ++++++++++--------- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index a426c590..3f6f7a7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -72,16 +72,6 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - TestUtils, - Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -177,3 +167,4 @@ test-suite GenServerTests Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs + diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 00f05d78..01731389 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -362,8 +362,8 @@ call :: forall a b . (Serializable a, Serializable b) call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ TerminateOther $ "CallFailed; " ++ show r - unpack (AsyncLinkFailed r) = die $ TerminateOther $ "LinkFailed; " ++ show r + unpack (AsyncFailed r) = die $ explain "CallFailed" r + unpack (AsyncLinkFailed r) = die $ explain "LinkFailed" r unpack AsyncCancelled = die $ TerminateOther $ "Cancelled" unpack AsyncPending = terminate -- as this *cannot* happen @@ -374,11 +374,10 @@ safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Either TerminateReason b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Right r - unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r - unpack (AsyncLinkFailed r) = return $ Left $ TerminateOther $ show r + unpack (AsyncFailed r) = return $ Left $ explain "CallFailed" r + unpack (AsyncLinkFailed r) = return $ Left $ explain "LinkFailed" r unpack AsyncCancelled = return $ Left $ TerminateOther $ "Cancelled" unpack AsyncPending = return $ Left $ TerminateOther $ "Pending" --- unpack ar = return $ Left $ TerminateOther $ showTypeRep ar -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -413,8 +412,6 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Async b) callAsync sid msg = do --- TODO: use a unified async API here if possible --- https://github.com/haskell-distributed/distributed-process-platform/issues/55 async $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid @@ -427,8 +424,8 @@ callAsync sid msg = do -- TODO: better failure API unmonitor mRef case r of - Right m -> return m - Left err -> (say $ "call: remote process died: " ++ show err) >> terminate + Right m -> return m + Left err -> die $ TerminateOther ("ServerExit (" ++ (show err) ++ ")") -- note [call using async] -- One problem with using plain expect/receive primitives to perform a @@ -839,3 +836,4 @@ sendTo :: (Serializable m) => Recipient -> m -> Process () sendTo (SendToPid p) m = send p m sendTo (SendToService s) m = nsend s m sendTo (SendToRemoteService s n) m = nsendRemote n s m + diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 961ace27..6fa4334e 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -158,20 +158,21 @@ testCounterIncrement pid result = do testCounterExceedsLimit :: ProcessId -> TestResult Bool -> Process () testCounterExceedsLimit pid result = do - _ <- monitor pid - 8 <- incCount pid - 9 <- incCount pid - 10 <- incCount pid - sleep $ seconds 1 - _ <- incCount pid - sleep $ seconds 1 + mref <- monitor pid + 7 <- getCount pid + + -- exceed the limit + 3 `times` (incCount pid >> return ()) + + -- this time we should fail + _ <- (incCount pid) + `catchExit` \_ (TerminateOther _) -> return 1 + r <- receiveWait [ - match (\(ProcessMonitorNotification _ _ r') -> return r') + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\(ProcessMonitorNotification _ _ r') -> return r') ] - liftIO $ putStrLn $ "counter died with " ++ (show r) - pInfo <- getProcessInfo pid - liftIO $ putStrLn $ "pinfo = " ++ (show pInfo) - stash result True + stash result (r == DiedNormal) -- utilities @@ -301,10 +302,10 @@ tests transport = do (delayedAssertion "expected the server to return the incremented state as 7" localNode 7 (testCounterIncrement counter)) - -- , testCase "exceed counter limits" - -- (delayedAssertion - -- "expected the server to terminate once the limit was exceeded" - -- localNode True (testCounterExceedsLimit counter)) + , testCase "exceed counter limits" + (delayedAssertion + "expected the server to terminate once the limit was exceeded" + localNode True (testCounterExceedsLimit counter)) ] ] From ae5deb4b5bf5627eca4ecf62332174efa1dbe2cb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 01:54:50 +0000 Subject: [PATCH 0818/2357] Ensure that GenProcess handles crashes whilst sync calls are underway A cleaner API for terminating (or informing) the client has been added. Additionally the Counter server example and test cases have been updated to reflect the changes. --- distributed-process-platform.cabal | 11 +---------- .../Process/Platform/GenProcess.hs | 16 +++++++--------- .../Process/Platform/Internal/Common.hs | 13 +++---------- .../Process/Platform/Internal/Primitives.hs | 11 +++++++++++ .../Process/Platform/Internal/Types.hs | 19 ++++--------------- 5 files changed, 26 insertions(+), 44 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index a426c590..3f6f7a7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -72,16 +72,6 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - TestUtils, - Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -177,3 +167,4 @@ test-suite GenServerTests Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs + diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 00f05d78..01731389 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -362,8 +362,8 @@ call :: forall a b . (Serializable a, Serializable b) call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] where unpack :: AsyncResult b -> Process b unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ TerminateOther $ "CallFailed; " ++ show r - unpack (AsyncLinkFailed r) = die $ TerminateOther $ "LinkFailed; " ++ show r + unpack (AsyncFailed r) = die $ explain "CallFailed" r + unpack (AsyncLinkFailed r) = die $ explain "LinkFailed" r unpack AsyncCancelled = die $ TerminateOther $ "Cancelled" unpack AsyncPending = terminate -- as this *cannot* happen @@ -374,11 +374,10 @@ safeCall :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Either TerminateReason b) safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Right r - unpack (AsyncFailed r) = return $ Left $ TerminateOther $ show r - unpack (AsyncLinkFailed r) = return $ Left $ TerminateOther $ show r + unpack (AsyncFailed r) = return $ Left $ explain "CallFailed" r + unpack (AsyncLinkFailed r) = return $ Left $ explain "LinkFailed" r unpack AsyncCancelled = return $ Left $ TerminateOther $ "Cancelled" unpack AsyncPending = return $ Left $ TerminateOther $ "Pending" --- unpack ar = return $ Left $ TerminateOther $ showTypeRep ar -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use @@ -413,8 +412,6 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Async b) callAsync sid msg = do --- TODO: use a unified async API here if possible --- https://github.com/haskell-distributed/distributed-process-platform/issues/55 async $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid @@ -427,8 +424,8 @@ callAsync sid msg = do -- TODO: better failure API unmonitor mRef case r of - Right m -> return m - Left err -> (say $ "call: remote process died: " ++ show err) >> terminate + Right m -> return m + Left err -> die $ TerminateOther ("ServerExit (" ++ (show err) ++ ")") -- note [call using async] -- One problem with using plain expect/receive primitives to perform a @@ -839,3 +836,4 @@ sendTo :: (Serializable m) => Recipient -> m -> Process () sendTo (SendToPid p) m = send p m sendTo (SendToService s) m = nsend s m sendTo (SendToRemoteService s n) m = nsendRemote n s m + diff --git a/src/Control/Distributed/Process/Platform/Internal/Common.hs b/src/Control/Distributed/Process/Platform/Internal/Common.hs index 22450709..e093be41 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Common.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Common.hs @@ -2,16 +2,9 @@ module Control.Distributed.Process.Platform.Internal.Common where -import Control.Distributed.Process (Process, die) +import Control.Distributed.Process import Control.Distributed.Process.Platform.Internal.Types -import Data.Typeable (Typeable, typeOf) -failTypeCheck :: Process b -failTypeCheck = failUnexpectedType "FAILED_TYPE_CHECK :-" - -failUnexpectedType :: String -> Process b -failUnexpectedType s = die $ TerminateOther s - -showTypeRep :: forall a. (Typeable a) => a -> String -showTypeRep m = show $ typeOf m +explain :: String -> DiedReason -> TerminateReason +explain m r = TerminateOther (m ++ " (" ++ (show r) ++ ")") diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index bd2e3395..3b17a50a 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -28,6 +28,9 @@ module Control.Distributed.Process.Platform.Internal.Primitives -- matching , matchCond + -- utility + , times + -- remote table , __remoteTable ) @@ -42,6 +45,14 @@ import Control.Distributed.Process.Platform.Internal.Types import Control.Monad (void) import Data.Maybe (isJust, fromJust) +-- utility + +times :: Int -> Process () -> Process () +n `times` proc = runP proc n + where runP :: Process () -> Int -> Process () + runP _ 0 = return () + runP p n' = p >> runP p (n' - 1) + -- spawning, linking and generic server startup -- | Node local version of 'Control.Distributed.Process.spawnLink'. diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index f33446e8..6ca7a626 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE TemplateHaskell #-} -- | Types used throughout the Cloud Haskell framework -- @@ -13,7 +14,6 @@ module Control.Distributed.Process.Platform.Internal.Types , TerminateReason(..) ) where -import Control.Applicative ((<$>)) import Control.Concurrent.MVar (MVar, newMVar, modifyMVar) import Control.Distributed.Process import Control.Distributed.Process.Serializable () @@ -21,6 +21,7 @@ import Data.Binary ( Binary(put, get) , putWord8 , getWord8) +import Data.DeriveTH import Data.Typeable (Typeable) -- | Simple representation of a channel. @@ -74,18 +75,6 @@ instance Binary Shutdown where data TerminateReason = TerminateNormal -- ^ indicates normal exit | TerminateShutdown -- ^ normal response to a 'Shutdown' - | TerminateOther String -- ^ abnormal (error) shutdown + | TerminateOther !String -- ^ abnormal (error) shutdown deriving (Typeable, Eq, Show) - -instance Binary TerminateReason where - put TerminateNormal = putWord8 1 - put TerminateShutdown = putWord8 2 - put (TerminateOther s) = putWord8 3 >> put s - - get = do - header <- getWord8 - case header of - 1 -> return TerminateNormal - 2 -> return TerminateShutdown - 3 -> TerminateOther <$> get - _ -> fail "TerminateReason.get: invalid" +$(derive makeBinary ''TerminateReason) From 69004ed307950a59a953e127912b525019f6fda0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 01:54:50 +0000 Subject: [PATCH 0819/2357] Ensure that GenProcess handles crashes whilst sync calls are underway A cleaner API for terminating (or informing) the client has been added. Additionally the Counter server example and test cases have been updated to reflect the changes. --- distributed-process-platform.cabal | 11 +--------- tests/TestGenServer.hs | 33 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index a426c590..3f6f7a7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -72,16 +72,6 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - TestUtils, - Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -177,3 +167,4 @@ test-suite GenServerTests Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs + diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 961ace27..6fa4334e 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -158,20 +158,21 @@ testCounterIncrement pid result = do testCounterExceedsLimit :: ProcessId -> TestResult Bool -> Process () testCounterExceedsLimit pid result = do - _ <- monitor pid - 8 <- incCount pid - 9 <- incCount pid - 10 <- incCount pid - sleep $ seconds 1 - _ <- incCount pid - sleep $ seconds 1 + mref <- monitor pid + 7 <- getCount pid + + -- exceed the limit + 3 `times` (incCount pid >> return ()) + + -- this time we should fail + _ <- (incCount pid) + `catchExit` \_ (TerminateOther _) -> return 1 + r <- receiveWait [ - match (\(ProcessMonitorNotification _ _ r') -> return r') + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\(ProcessMonitorNotification _ _ r') -> return r') ] - liftIO $ putStrLn $ "counter died with " ++ (show r) - pInfo <- getProcessInfo pid - liftIO $ putStrLn $ "pinfo = " ++ (show pInfo) - stash result True + stash result (r == DiedNormal) -- utilities @@ -301,10 +302,10 @@ tests transport = do (delayedAssertion "expected the server to return the incremented state as 7" localNode 7 (testCounterIncrement counter)) - -- , testCase "exceed counter limits" - -- (delayedAssertion - -- "expected the server to terminate once the limit was exceeded" - -- localNode True (testCounterExceedsLimit counter)) + , testCase "exceed counter limits" + (delayedAssertion + "expected the server to terminate once the limit was exceeded" + localNode True (testCounterExceedsLimit counter)) ] ] From e6bc4990af448dd26f89dadd05eb70d11e6c4261 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 01:54:50 +0000 Subject: [PATCH 0820/2357] Ensure that GenProcess handles crashes whilst sync calls are underway A cleaner API for terminating (or informing) the client has been added. Additionally the Counter server example and test cases have been updated to reflect the changes. --- distributed-process-platform.cabal | 11 +--------- tests/Counter.hs | 10 ++------- tests/TestGenServer.hs | 33 +++++++++++++++--------------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index a426c590..3f6f7a7e 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -72,16 +72,6 @@ test-suite TimerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - TestUtils, - Control.Distributed.Process.Platform.Test - Control.Distributed.Process.Platform.Internal.Types, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestTimer.hs @@ -177,3 +167,4 @@ test-suite GenServerTests Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs + diff --git a/tests/Counter.hs b/tests/Counter.hs index fa511380..e0ee8116 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -80,21 +80,15 @@ serverDefinition = defaultProcess { dispatchers = [ handleCallIf (condition (\count Increment -> count >= 10))-- invariant (\_ (_ :: Increment) -> do - say "terminating...." noReply_ (TerminateOther "Count > 10")) , handleCall handleIncrement , handleCall (\count Fetch -> reply count count) , handleCast (\_ Reset -> continue 0) ] - , terminateHandler = (\s r -> do - say $ "terminating counter when state = " ++ (show s) ++ " because " ++ show r) } :: ProcessDefinition State handleIncrement :: State -> Increment -> Process (ProcessReply State Int) -handleIncrement count Increment = do - next <- increment - replyWith (count + 1) $ next - where increment :: Process (ProcessAction State) - !increment = return (ProcessContinue (count + 1)) +handleIncrement count Increment = + let next = count + 1 in continue next >>= replyWith next diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 961ace27..6fa4334e 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -158,20 +158,21 @@ testCounterIncrement pid result = do testCounterExceedsLimit :: ProcessId -> TestResult Bool -> Process () testCounterExceedsLimit pid result = do - _ <- monitor pid - 8 <- incCount pid - 9 <- incCount pid - 10 <- incCount pid - sleep $ seconds 1 - _ <- incCount pid - sleep $ seconds 1 + mref <- monitor pid + 7 <- getCount pid + + -- exceed the limit + 3 `times` (incCount pid >> return ()) + + -- this time we should fail + _ <- (incCount pid) + `catchExit` \_ (TerminateOther _) -> return 1 + r <- receiveWait [ - match (\(ProcessMonitorNotification _ _ r') -> return r') + matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref) + (\(ProcessMonitorNotification _ _ r') -> return r') ] - liftIO $ putStrLn $ "counter died with " ++ (show r) - pInfo <- getProcessInfo pid - liftIO $ putStrLn $ "pinfo = " ++ (show pInfo) - stash result True + stash result (r == DiedNormal) -- utilities @@ -301,10 +302,10 @@ tests transport = do (delayedAssertion "expected the server to return the incremented state as 7" localNode 7 (testCounterIncrement counter)) - -- , testCase "exceed counter limits" - -- (delayedAssertion - -- "expected the server to terminate once the limit was exceeded" - -- localNode True (testCounterExceedsLimit counter)) + , testCase "exceed counter limits" + (delayedAssertion + "expected the server to terminate once the limit was exceeded" + localNode True (testCounterExceedsLimit counter)) ] ] From 30aa8adc483897169320226d7960dbfb013d3f00 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 02:07:29 +0000 Subject: [PATCH 0821/2357] minor doc fixes --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 01731389..5d1a0c4b 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -95,7 +95,7 @@ -- -- All call/cast handlers come in two flavours, those which take the process -- state as an input and those which do not. Handlers that ignore the process --- state have to return a section that takes the state and returns the required +-- state have to return a function that takes the state and returns the required -- action. Versions of the various action generating functions ending in an -- underscore are provided to simplify this: -- From 46e282cd9a52d659d11bf66aaa6f590419d1e07d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 02:07:29 +0000 Subject: [PATCH 0822/2357] minor doc fixes --- src/Control/Distributed/Process/Platform/GenProcess.hs | 2 +- src/Control/Distributed/Process/Platform/Time.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 01731389..5d1a0c4b 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -95,7 +95,7 @@ -- -- All call/cast handlers come in two flavours, those which take the process -- state as an input and those which do not. Handlers that ignore the process --- state have to return a section that takes the state and returns the required +-- state have to return a function that takes the state and returns the required -- action. Versions of the various action generating functions ending in an -- underscore are provided to simplify this: -- diff --git a/src/Control/Distributed/Process/Platform/Time.hs b/src/Control/Distributed/Process/Platform/Time.hs index 4ef83d06..5069c612 100644 --- a/src/Control/Distributed/Process/Platform/Time.hs +++ b/src/Control/Distributed/Process/Platform/Time.hs @@ -8,7 +8,7 @@ -- Copyright : (c) Tim Watson, Jeff Epstein -- License : BSD3 (see the file LICENSE) -- --- Maintainers : Tim Watson +-- Maintainer : Tim Watson -- Stability : experimental -- Portability : non-portable (requires concurrency) -- From 5ae61adfc204d49912288e9b731c96d26b34a1df Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 09:41:50 +0000 Subject: [PATCH 0823/2357] fix escaping in GenProcess documentation code samples --- .../Distributed/Process/Platform/GenProcess.hs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5d1a0c4b..0dc1c607 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -102,11 +102,11 @@ -- @ -- statelessProcess { -- dispatchers = [ --- handleCall_ (\(n :: Int) -> return (n * 2)) --- , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") --- (\("timeout", Delay d) -> timeoutAfter_ d) +-- handleCall_ (\\(n :: Int) -> return (n * 2)) +-- , handleCastIf_ (\\(c :: String, _ :: Delay) -> c == \"timeout\") +-- (\\(\"timeout\", Delay d) -> timeoutAfter_ d) -- ] --- , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" +-- , timeoutHandler = \\_ _ -> stop $ TerminateOther \"timeout\" -- } -- @ -- @@ -173,14 +173,13 @@ module Control.Distributed.Process.Platform.GenProcess , handleCast , handleCastIf , handleInfo + , handleDispatch -- * Stateless handlers , action , handleCall_ , handleCallIf_ , handleCast_ , handleCastIf_ - -- lower level handlers - , handleDispatch ) where import Control.Concurrent (threadDelay) From 24ead04cfda0fffc007289b0eb0eab12f2384b36 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 09:41:50 +0000 Subject: [PATCH 0824/2357] fix escaping in GenProcess documentation code samples --- .../Distributed/Process/Platform/GenProcess.hs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5d1a0c4b..0dc1c607 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -102,11 +102,11 @@ -- @ -- statelessProcess { -- dispatchers = [ --- handleCall_ (\(n :: Int) -> return (n * 2)) --- , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout") --- (\("timeout", Delay d) -> timeoutAfter_ d) +-- handleCall_ (\\(n :: Int) -> return (n * 2)) +-- , handleCastIf_ (\\(c :: String, _ :: Delay) -> c == \"timeout\") +-- (\\(\"timeout\", Delay d) -> timeoutAfter_ d) -- ] --- , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" +-- , timeoutHandler = \\_ _ -> stop $ TerminateOther \"timeout\" -- } -- @ -- @@ -173,14 +173,13 @@ module Control.Distributed.Process.Platform.GenProcess , handleCast , handleCastIf , handleInfo + , handleDispatch -- * Stateless handlers , action , handleCall_ , handleCallIf_ , handleCast_ , handleCastIf_ - -- lower level handlers - , handleDispatch ) where import Control.Concurrent (threadDelay) From f30b6ad3067d0a081017f9413d3c934c8edf2287 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:50:01 +0000 Subject: [PATCH 0825/2357] HumpsInErrorMessages for GenProcess --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- tests/TestGenServer.hs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 0dc1c607..dbdeda12 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -293,7 +293,7 @@ instance MessageMatcher Dispatcher where -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the -- 'handleInfo' handlers. data UnhandledMessagePolicy = - Terminate -- ^ stop immediately, giving @TerminateOther "UNHANDLED_INPUT"@ as the reason + Terminate -- ^ stop immediately, giving @TerminateOther "UnhandledInput"@ as the reason | DeadLetter ProcessId -- ^ forward the message to the given recipient | Drop -- ^ dequeue and then drop/ignore the message @@ -763,7 +763,7 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = case p of - Terminate -> stop $ TerminateOther "UNHANDLED_INPUT" + Terminate -> stop $ TerminateOther "UnhandledInput" DeadLetter pid -> forward m pid >> continue s Drop -> continue s diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 6fa4334e..a990d45c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -264,7 +264,7 @@ tests transport = do , testCase "unhandled input when policy = Terminate" (delayedAssertion "expected the server to stop upon receiving unhandled input" - localNode (Just (TerminateOther "UNHANDLED_INPUT")) + localNode (Just (TerminateOther "UnhandledInput")) testTerminatePolicy) , testCase "unhandled input when policy = Drop" (delayedAssertion From 95ac567c550772f58761f61e4fb2d0063d0ab742 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:50:01 +0000 Subject: [PATCH 0826/2357] HumpsInErrorMessages for GenProcess --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 0dc1c607..dbdeda12 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -293,7 +293,7 @@ instance MessageMatcher Dispatcher where -- sent using the 'call' or 'cast' APIs, and which are not handled by any of the -- 'handleInfo' handlers. data UnhandledMessagePolicy = - Terminate -- ^ stop immediately, giving @TerminateOther "UNHANDLED_INPUT"@ as the reason + Terminate -- ^ stop immediately, giving @TerminateOther "UnhandledInput"@ as the reason | DeadLetter ProcessId -- ^ forward the message to the given recipient | Drop -- ^ dequeue and then drop/ignore the message @@ -763,7 +763,7 @@ applyPolicy :: s -> Process (ProcessAction s) applyPolicy s p m = case p of - Terminate -> stop $ TerminateOther "UNHANDLED_INPUT" + Terminate -> stop $ TerminateOther "UnhandledInput" DeadLetter pid -> forward m pid >> continue s Drop -> continue s From 390d9d60ea26a7e70aa653b858143d4627616f79 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:50:01 +0000 Subject: [PATCH 0827/2357] HumpsInErrorMessages for GenProcess --- tests/TestGenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 6fa4334e..a990d45c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -264,7 +264,7 @@ tests transport = do , testCase "unhandled input when policy = Terminate" (delayedAssertion "expected the server to stop upon receiving unhandled input" - localNode (Just (TerminateOther "UNHANDLED_INPUT")) + localNode (Just (TerminateOther "UnhandledInput")) testTerminatePolicy) , testCase "unhandled input when policy = Drop" (delayedAssertion From 18edcbe6d16aec7bd4cbb2542a6b9eb3d58dcff6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:50:01 +0000 Subject: [PATCH 0828/2357] HumpsInErrorMessages for GenProcess --- tests/TestGenServer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 6fa4334e..a990d45c 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -264,7 +264,7 @@ tests transport = do , testCase "unhandled input when policy = Terminate" (delayedAssertion "expected the server to stop upon receiving unhandled input" - localNode (Just (TerminateOther "UNHANDLED_INPUT")) + localNode (Just (TerminateOther "UnhandledInput")) testTerminatePolicy) , testCase "unhandled input when policy = Drop" (delayedAssertion From 6cb2aa12f8853bed53994996d327a1a865f620b1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:51:36 +0000 Subject: [PATCH 0829/2357] GenProcess documentation --- .../Process/Platform/GenProcess.hs | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index dbdeda12..93258557 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -16,42 +16,45 @@ -- This module provides a high(er) level API for building complex 'Process' -- implementations by abstracting out the management of the process' mailbox, -- reply/response handling, timeouts, process hiberation, error handling --- and shutdown/stop procedures. Whilst this API is intended to provide a --- higher level of abstraction that vanilla Cloud Haskell, it is intended --- for use primarilly as a building block. +-- and shutdown/stop procedures. -- -- [API Overview] -- -- Once started, a generic process will consume messages from its mailbox and -- pass them on to user defined /handlers/ based on the types received (mapped --- to those accepted by the handlers). Each handler returns a 'ProcessAction', --- which specifies how we should proceed. If none of the handlers is able to --- process a message (because their types are incompatible) then the process --- 'unhandledMessagePolicy' will be applied. +-- to those accepted by the handlers) and optionally by also evaluating user +-- supplied predicates to determine which handlers are valid. +-- Each handler returns a 'ProcessAction' which specifies how we should proceed. +-- If none of the handlers is able to process a message (because their types are +-- incompatible) then the process 'unhandledMessagePolicy' will be applied. -- -- The 'ProcessAction' type defines the ways in which a process can respond --- to its inputs, either by continuing to wait for incoming messages (with an --- optional timeout), sleeping (i.e., @threadDelay@ for a while) then waiting --- or by stopping. If a handler returns @ProcessTimeout@ and no messages are --- received within the time window, a specific 'timeoutHandler' is called, --- which by default instructs the process to go back to waiting without a --- timeout. --- --- To instruct a process to stop unless messages are received within a given --- time window, a simple timeout handler would look something like this: --- --- > \_state _lastTimeWindow -> stop $ TerminateOther "timeout" +-- to its inputs, either by continuing to read incoming messages, setting an +-- optional timeout, sleeping for a while or by stopping. The optional timeout +-- behaves a little differently to the other process actions. If no messages +-- are received within the specified time span, the process 'timeoutHandler' +-- will be called in order to determine the next action. -- -- Generic processes are defined by the 'ProcessDefinition' type, using record -- syntax. The 'ProcessDefinition' fields contain handlers (or lists of them) --- for specific tasks. The @timeoutHandler@ and @terminateHandler@ are called --- when the process handles these respectively. The other handlers are split --- into two groups: /dispatchers/ and /infoHandlers/. +-- for specific tasks. In addtion to the @timeoutHandler@, a 'ProcessDefinition' +-- may also define a @terminateHandler@ which is called just before the process +-- exits. This handler will be called /whenever/ the process is stopping, i.e., +-- when a callback returns 'stop' as the next action /or/ if an unhandled exit +-- signal or similar asynchronous exception is thrown in (or to) the process +-- itself. +-- +-- The other handlers are split into two groups: /dispatchers/ and /infoHandlers/. +-- The former contains handlers for the 'cast' and 'call' protocols, whilst the +-- latter contains handlers that deal with input messages which are not sent +-- via these API calls (i.e., messages sent using bare 'send' or signals put +-- into the process mailbox by the node controller, such as +-- 'ProcessMonitorNotification' and the like). -- -- [The Cast/Call Protocol] -- --- Client interactions with the process will usually fall into one of two --- categories. A 'cast' interaction involves the client sending a message +-- Deliberate interactions with the process will usually fall into one of two +-- categories. A 'cast' interaction involves a client sending a message -- asynchronously and the server handling this input. No reply is sent to -- the client. On the other hand, a 'call' interaction is a kind of /rpc/ -- where the client sends a message and waits for a reply. @@ -251,10 +254,13 @@ type CastHandler s = s -> Process () -- type InfoHandler a = forall a b. (Serializable a, Serializable b) => a -> Process b +-- | Wraps a predicate that is used to determine whether or not a handler +-- is valid based on some combination of the current process state, the +-- type and/or value of the input message or both. data Condition s m = - Condition (s -> m -> Bool) - | State (s -> Bool) - | Input (m -> Bool) + Condition (s -> m -> Bool) -- ^ predicated on the process state /and/ the message + | State (s -> Bool) -- ^ predicated on the process state only + | Input (m -> Bool) -- ^ predicated on the input message only -- | An expression used to initialise a process with its state. type InitHandler a s = a -> Process (InitResult s) @@ -387,9 +393,8 @@ tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing --- | Make a synchronous calls, but timeout and return @Nothing@ if the reply --- is not received within the specified time interval. The reply may be sent --- later on, or the call can be cancelled using the async @cancel@ API. +-- | Make a synchronous call, but timeout and return @Nothing@ if the reply +-- is not received within the specified time interval. -- -- If the 'AsyncResult' for the call indicates a failure (or cancellation) then -- the calling process will exit, with the 'AsyncResult' given as the reason. @@ -433,8 +438,8 @@ callAsync sid msg = do -- distinguish between inputs but this is easy to forge, as is tagging the -- response with the sender's pid. -- --- The approach we take here is to rely on AsyncSTM to insulate us from --- erroneous incoming messages without the need for tagging. The /async handle/ +-- The approach we take here is to rely on AsyncSTM (by default) to insulate us +-- from erroneous incoming messages without the need for tagging. The /handle/ -- returned uses an @STM (AsyncResult a)@ field to handle the response /and/ -- the implementation spawns a new process to perform the actual call and -- await the reply before atomically updating the result. Whilst in theory, @@ -456,14 +461,24 @@ cast sid msg = send sid (CastMessage msg) -- Producing ProcessAction and ProcessReply from inside handler expressions -- -------------------------------------------------------------------------------- +-- | Creates a 'Conditon' from a function that takes a process state @a@ and +-- an input message @b@ and returns a 'Bool' indicating whether the associated +-- handler should run. +-- condition :: forall a b. (Serializable a, Serializable b) => (a -> b -> Bool) -> Condition a b condition = Condition +-- | Create a 'Condition' from a function that takes a process state @a@ and +-- returns a 'Bool' indicating whether the associated handler should run. +-- state :: forall s m. (Serializable m) => (s -> Bool) -> Condition s m state = State +-- | Creates a 'Condition' from a function that takes an input message @m@ and +-- returns a 'Bool' indicating whether the associated handler should run. +-- input :: forall s m. (Serializable m) => (m -> Bool) -> Condition s m input = Input @@ -751,10 +766,8 @@ decode :: Message a -> a decode (CallMessage a _) = a decode (CastMessage a) = a --- wrapping /normal/ functions with InfoDispatcher - -------------------------------------------------------------------------------- --- Process Implementation -- +-- Internal Process Implementation -- -------------------------------------------------------------------------------- applyPolicy :: s From b31b12cf967b9b2bc4a4aa31671f815b80502d9b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:51:36 +0000 Subject: [PATCH 0830/2357] GenProcess documentation --- .../Process/Platform/GenProcess.hs | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index dbdeda12..93258557 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -16,42 +16,45 @@ -- This module provides a high(er) level API for building complex 'Process' -- implementations by abstracting out the management of the process' mailbox, -- reply/response handling, timeouts, process hiberation, error handling --- and shutdown/stop procedures. Whilst this API is intended to provide a --- higher level of abstraction that vanilla Cloud Haskell, it is intended --- for use primarilly as a building block. +-- and shutdown/stop procedures. -- -- [API Overview] -- -- Once started, a generic process will consume messages from its mailbox and -- pass them on to user defined /handlers/ based on the types received (mapped --- to those accepted by the handlers). Each handler returns a 'ProcessAction', --- which specifies how we should proceed. If none of the handlers is able to --- process a message (because their types are incompatible) then the process --- 'unhandledMessagePolicy' will be applied. +-- to those accepted by the handlers) and optionally by also evaluating user +-- supplied predicates to determine which handlers are valid. +-- Each handler returns a 'ProcessAction' which specifies how we should proceed. +-- If none of the handlers is able to process a message (because their types are +-- incompatible) then the process 'unhandledMessagePolicy' will be applied. -- -- The 'ProcessAction' type defines the ways in which a process can respond --- to its inputs, either by continuing to wait for incoming messages (with an --- optional timeout), sleeping (i.e., @threadDelay@ for a while) then waiting --- or by stopping. If a handler returns @ProcessTimeout@ and no messages are --- received within the time window, a specific 'timeoutHandler' is called, --- which by default instructs the process to go back to waiting without a --- timeout. --- --- To instruct a process to stop unless messages are received within a given --- time window, a simple timeout handler would look something like this: --- --- > \_state _lastTimeWindow -> stop $ TerminateOther "timeout" +-- to its inputs, either by continuing to read incoming messages, setting an +-- optional timeout, sleeping for a while or by stopping. The optional timeout +-- behaves a little differently to the other process actions. If no messages +-- are received within the specified time span, the process 'timeoutHandler' +-- will be called in order to determine the next action. -- -- Generic processes are defined by the 'ProcessDefinition' type, using record -- syntax. The 'ProcessDefinition' fields contain handlers (or lists of them) --- for specific tasks. The @timeoutHandler@ and @terminateHandler@ are called --- when the process handles these respectively. The other handlers are split --- into two groups: /dispatchers/ and /infoHandlers/. +-- for specific tasks. In addtion to the @timeoutHandler@, a 'ProcessDefinition' +-- may also define a @terminateHandler@ which is called just before the process +-- exits. This handler will be called /whenever/ the process is stopping, i.e., +-- when a callback returns 'stop' as the next action /or/ if an unhandled exit +-- signal or similar asynchronous exception is thrown in (or to) the process +-- itself. +-- +-- The other handlers are split into two groups: /dispatchers/ and /infoHandlers/. +-- The former contains handlers for the 'cast' and 'call' protocols, whilst the +-- latter contains handlers that deal with input messages which are not sent +-- via these API calls (i.e., messages sent using bare 'send' or signals put +-- into the process mailbox by the node controller, such as +-- 'ProcessMonitorNotification' and the like). -- -- [The Cast/Call Protocol] -- --- Client interactions with the process will usually fall into one of two --- categories. A 'cast' interaction involves the client sending a message +-- Deliberate interactions with the process will usually fall into one of two +-- categories. A 'cast' interaction involves a client sending a message -- asynchronously and the server handling this input. No reply is sent to -- the client. On the other hand, a 'call' interaction is a kind of /rpc/ -- where the client sends a message and waits for a reply. @@ -251,10 +254,13 @@ type CastHandler s = s -> Process () -- type InfoHandler a = forall a b. (Serializable a, Serializable b) => a -> Process b +-- | Wraps a predicate that is used to determine whether or not a handler +-- is valid based on some combination of the current process state, the +-- type and/or value of the input message or both. data Condition s m = - Condition (s -> m -> Bool) - | State (s -> Bool) - | Input (m -> Bool) + Condition (s -> m -> Bool) -- ^ predicated on the process state /and/ the message + | State (s -> Bool) -- ^ predicated on the process state only + | Input (m -> Bool) -- ^ predicated on the input message only -- | An expression used to initialise a process with its state. type InitHandler a s = a -> Process (InitResult s) @@ -387,9 +393,8 @@ tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing --- | Make a synchronous calls, but timeout and return @Nothing@ if the reply --- is not received within the specified time interval. The reply may be sent --- later on, or the call can be cancelled using the async @cancel@ API. +-- | Make a synchronous call, but timeout and return @Nothing@ if the reply +-- is not received within the specified time interval. -- -- If the 'AsyncResult' for the call indicates a failure (or cancellation) then -- the calling process will exit, with the 'AsyncResult' given as the reason. @@ -433,8 +438,8 @@ callAsync sid msg = do -- distinguish between inputs but this is easy to forge, as is tagging the -- response with the sender's pid. -- --- The approach we take here is to rely on AsyncSTM to insulate us from --- erroneous incoming messages without the need for tagging. The /async handle/ +-- The approach we take here is to rely on AsyncSTM (by default) to insulate us +-- from erroneous incoming messages without the need for tagging. The /handle/ -- returned uses an @STM (AsyncResult a)@ field to handle the response /and/ -- the implementation spawns a new process to perform the actual call and -- await the reply before atomically updating the result. Whilst in theory, @@ -456,14 +461,24 @@ cast sid msg = send sid (CastMessage msg) -- Producing ProcessAction and ProcessReply from inside handler expressions -- -------------------------------------------------------------------------------- +-- | Creates a 'Conditon' from a function that takes a process state @a@ and +-- an input message @b@ and returns a 'Bool' indicating whether the associated +-- handler should run. +-- condition :: forall a b. (Serializable a, Serializable b) => (a -> b -> Bool) -> Condition a b condition = Condition +-- | Create a 'Condition' from a function that takes a process state @a@ and +-- returns a 'Bool' indicating whether the associated handler should run. +-- state :: forall s m. (Serializable m) => (s -> Bool) -> Condition s m state = State +-- | Creates a 'Condition' from a function that takes an input message @m@ and +-- returns a 'Bool' indicating whether the associated handler should run. +-- input :: forall s m. (Serializable m) => (m -> Bool) -> Condition s m input = Input @@ -751,10 +766,8 @@ decode :: Message a -> a decode (CallMessage a _) = a decode (CastMessage a) = a --- wrapping /normal/ functions with InfoDispatcher - -------------------------------------------------------------------------------- --- Process Implementation -- +-- Internal Process Implementation -- -------------------------------------------------------------------------------- applyPolicy :: s From 4388678e71374e005012c85efde84b088e704185 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:53:09 +0000 Subject: [PATCH 0831/2357] Add 'handleCallFrom' and 'replyTo' support to GenProcess Provide support for deferred replies, add a simple worker pool prototype to demonstrate the API usage and provide a basis for testing. --- distributed-process-platform.cabal | 1 + .../Process/Platform/GenProcess.hs | 101 ++++++++++++---- tests/SimplePool.hs | 111 ++++++++++++++++++ tests/TestGenServer.hs | 33 ++---- 4 files changed, 205 insertions(+), 41 deletions(-) create mode 100644 tests/SimplePool.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3f6f7a7e..ab8a1438 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -164,6 +164,7 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, + SimplePool, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 93258557..5732909f 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -162,6 +162,7 @@ module Control.Distributed.Process.Platform.GenProcess , replyWith , noReply , noReply_ + , haltNoReply_ , continue , continue_ , timeoutAfter @@ -170,9 +171,12 @@ module Control.Distributed.Process.Platform.GenProcess , hibernate_ , stop , stop_ + , replyTo -- * Handler callback creation , handleCall , handleCallIf + , handleCallFrom + , handleCallFromIf , handleCast , handleCastIf , handleInfo @@ -415,8 +419,26 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Async b) -callAsync sid msg = do - async $ do -- note [call using async] +callAsync = callAsyncUsing async + +-- | As 'callAsync' but takes a function that can be used to generate an async +-- task and return an async handle to it. This can be used to switch between +-- async implementations, by e.g., using an async channel instead of the default +-- STM based handle. +-- +-- See 'callAsync' +-- +-- See "Control.Distributed.Process.Platform.Async" +-- +-- See "Control.Distributed.Process.Platform.Async.AsyncChan" +-- +-- See "Control.Distributed.Process.Platform.Async.AsyncSTM" +-- +callAsyncUsing :: forall a b . (Serializable a, Serializable b) + => (Process b -> Process (Async b)) + -> ProcessId -> a -> Process (Async b) +callAsyncUsing asyncStart sid msg = do + asyncStart $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) @@ -497,9 +519,14 @@ replyWith msg st = return $ ProcessReply msg st noReply :: (Serializable r) => ProcessAction s -> Process (ProcessReply s r) noReply = return . NoReply +-- | Continue without giving a reply to the caller - equivalent to 'continue', +-- but usable in a callback passed to the 'handleCall' family of functions. +noReply_ :: forall s r . (Serializable r) => s -> Process (ProcessReply s r) +noReply_ s = continue s >>= noReply + -- | Halt a call handler without regard for the expected return type. -noReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) -noReply_ r = stop r >>= noReply +haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) +haltNoReply_ r = stop r >>= noReply -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) @@ -549,6 +576,14 @@ stop r = return $ ProcessStop r stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r +sendTo :: (Serializable m) => Recipient -> m -> Process () +sendTo (SendToPid p) m = send p m +sendTo (SendToService s) m = nsend s m +sendTo (SendToRemoteService s n) m = nsendRemote n s m + +replyTo :: (Serializable m) => Recipient -> m -> Process () +replyTo client msg = sendTo client (CallResponse msg) + -------------------------------------------------------------------------------- -- Wrapping handler expressions in Dispatcher and InfoDispatcher -- -------------------------------------------------------------------------------- @@ -585,7 +620,7 @@ handleCallIf_ cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -623,14 +658,39 @@ handleCallIf cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - -- handling 'reply-to' in the main process loop is awkward at best, - -- so we handle it here instead and return the 'action' to the loop - mkReply :: (Serializable b) - => Recipient -> ProcessReply s b -> Process (ProcessAction s) - mkReply _ (NoReply a) = return a - mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a +-- | As 'handleCall' but passes the 'Recipient' to the handler function. +-- This can be useful if you wish to /reply later/ to the caller by, e.g., +-- spawning a process to do some work and have it @replyTo caller response@ +-- out of band. In this case the callback can pass the 'Recipient' to the +-- worker (or stash it away itself) and return 'noReply'. +-- +handleCallFrom :: forall s a b . (Serializable a, Serializable b) + => (s -> Recipient -> a -> Process (ProcessReply s b)) + -> Dispatcher s +handleCallFrom = handleCallFromIf $ state (const True) + +-- | As 'handleCallFrom' but only runs the handler if the supplied 'Condition' +-- evaluates to @True@. +-- +handleCallFromIf :: forall s a b . (Serializable a, Serializable b) + => Condition s a -- ^ predicate that must be satisfied for the handler to run + -> (s -> Recipient -> a -> Process (ProcessReply s b)) + -- ^ a reply yielding function over the process state, sender and input message + -> Dispatcher s +handleCallFromIf cond handler + = DispatchIf { + dispatch = doHandle handler + , dispatchIf = checkCall cond + } + where doHandle :: (Serializable a, Serializable b) + => (s -> Recipient -> a -> Process (ProcessReply s b)) + -> s + -> Message a + -> Process (ProcessAction s) + doHandle h s (CallMessage p c) = (h s c p) >>= mkReply c + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. @@ -737,6 +797,15 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) +-- handling 'reply-to' in the main process loop is awkward at best, +-- so we handle it here instead and return the 'action' to the loop +mkReply :: (Serializable b) + => Recipient -> ProcessReply s b -> Process (ProcessAction s) +mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a +mkReply _ (NoReply a) = return a + +-- these functions are the inverse of 'condition', 'state' and 'input' + check :: forall s m . (Serializable m) => Condition s m -> s @@ -841,11 +910,3 @@ processReceive ms handleTimeout st d = do case d' of Infinity -> receiveWait matches >>= return . Just Delay t' -> receiveTimeout (asTimeout t') matches - --- internal/utility - -sendTo :: (Serializable m) => Recipient -> m -> Process () -sendTo (SendToPid p) m = send p m -sendTo (SendToService s) m = nsend s m -sendTo (SendToRemoteService s n) m = nsendRemote n s m - diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs new file mode 100644 index 00000000..ab39542d --- /dev/null +++ b/tests/SimplePool.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- | Simple bounded (size) worker pool that accepts tasks and blocks +-- the caller until they've completed. Partly a /spike/ for that 'Task' API +-- and partly just a test bed for handling 'replyTo' in GenProcess. +-- +module SimplePool where + +import Control.Distributed.Process +import Control.Distributed.Process.Closure() +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Serializable + +import Data.Binary() +import Data.List + ( deleteBy + , find + ) +import Data.Typeable + +type PoolSize = Int +type SimpleTask a = Closure (Process a) + +data State a = State { + poolSize :: PoolSize + , active :: [(ProcessId, Recipient, Async a)] + , accepted :: [(Recipient, Closure (Process a))] + } deriving (Typeable) + +-- | Start a worker pool with an upper bound on the # of concurrent workers. +simplePool :: forall a . (Serializable a) + => PoolSize + -> Process (Either (InitResult (State a)) TerminateReason) +simplePool sz = + let server = defaultProcess { + dispatchers = [ + handleCallFrom (\s f (p :: Closure (Process a)) -> acceptTask s f p) + ] + } :: ProcessDefinition (State a) + in start sz init' server + where init' :: PoolSize -> Process (InitResult (State a)) + init' sz' = return $ InitOk (State sz' [] []) Infinity + +-- /call/ handler: accept a task and defer responding until "later" +acceptTask :: Serializable a + => State a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (State a) ()) +acceptTask s@(State sz' runQueue taskQueue) from task' = + let currentSz = length runQueue + in case currentSz >= sz' of + True -> do + s2 <- return $ s{ accepted = ((from, task'):taskQueue) } + noReply_ s2 + False -> do + proc <- unClosure task' + asyncHandle <- async proc + pid <- return $ asyncWorker asyncHandle + taskEntry <- return (pid, from, asyncHandle) + _ <- monitor pid + noReply_ s { accepted = ((from, task'):taskQueue) + , active = (taskEntry:runQueue) + } + +-- /info/ handler: a worker has exited, process the AsyncResult and send a reply +-- to the waiting client (who is still stuck in 'call' awaiting a response). +taskComplete :: forall a . Serializable a + => State a + -> ProcessMonitorNotification + -> Process (ProcessAction (State a)) +taskComplete s@(State _ runQ _) + (ProcessMonitorNotification _ pid _) = + let worker = findWorker pid runQ in + case worker of + Nothing -> continue s + Just t@(_, client, handle) -> + wait handle >>= respond client >> bump s t >>= continue + + where + respond :: Recipient + -> AsyncResult a + -> Process () + respond c (AsyncDone r) = replyTo c ((Right r) :: (Either String a)) + respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond _ _ = die $ TerminateOther "IllegalState" + + bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) + bump (State maxSz runQueue taskQueue) c@(pid', _, _) = + let runLen = (length runQueue) - 1 + _runQ2 = deleteBy (\_ (b, _, _) -> b == pid') c runQ + slots = maxSz - runLen + runnable = ((length taskQueue > 0) && (slots > 0)) in + case runnable of + True -> {- pull 'slots' tasks into the run queue -} die $ "WHAT!" + False -> die $ "oh, that!" + + -- take this task out of the run queue and bump pending tasks if needed + -- deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] + +findWorker :: ProcessId + -> [(ProcessId, Recipient, Async a)] + -> Maybe (ProcessId, Recipient, Async a) +findWorker key = find (\(pid,_,_) -> pid == key) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index a990d45c..82b526ea 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -25,6 +25,7 @@ import Data.Typeable (Typeable) import Data.DeriveTH import MathsDemo import Counter +import SimplePool import Prelude hiding (catch) @@ -127,16 +128,6 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False -testStateHandling :: TestResult Bool -> Process () -testStateHandling result = do - pid <- statefulServer "ok" - cast pid ("ko" :: String) -- updateState - liftIO $ putStrLn "cast sent!" - s2 <- call pid GetState -- getState - say $ "s2 = " ++ s2 - sleep $ seconds 2 - stash result (s2 == "ko") - -- MathDemo tests testAdd :: ProcessId -> TestResult Double -> Process () @@ -222,17 +213,17 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) -statefulServer :: String -> Process ProcessId -statefulServer st = - let b = defaultProcess { - dispatchers = [ - handleCast (\_ new -> continue new) - , handleCall (\s GetState -> reply s s) - ] - } :: ProcessDefinition String - in spawnLocal $ start st init' b >> return () - where init' :: String -> Process (InitResult String) - init' initS = return $ InitOk initS Infinity +-- workerPool :: Process ProcessId +-- workerPool = +-- let b = defaultProcess { +-- dispatchers = [ +-- handleCast (\_ new -> continue new) +-- , handleCall (\s GetState -> reply s s) +-- ] +-- } :: ProcessDefinition String +-- in spawnLocal $ start () init' b >> return () +-- where init' :: () -> Process (InitResult String) +-- init' = const (return $ InitOk () Infinity) tests :: NT.Transport -> IO [Test] tests transport = do From 956135b9ec44b1ddd5dbba9ae93848edb137355a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:53:09 +0000 Subject: [PATCH 0832/2357] Add 'handleCallFrom' and 'replyTo' support to GenProcess Provide support for deferred replies, add a simple worker pool prototype to demonstrate the API usage and provide a basis for testing. --- distributed-process-platform.cabal | 1 + tests/SimplePool.hs | 111 +++++++++++++++++++++++++++++ tests/TestGenServer.hs | 33 ++++----- 3 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 tests/SimplePool.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3f6f7a7e..ab8a1438 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -164,6 +164,7 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, + SimplePool, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs new file mode 100644 index 00000000..ab39542d --- /dev/null +++ b/tests/SimplePool.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- | Simple bounded (size) worker pool that accepts tasks and blocks +-- the caller until they've completed. Partly a /spike/ for that 'Task' API +-- and partly just a test bed for handling 'replyTo' in GenProcess. +-- +module SimplePool where + +import Control.Distributed.Process +import Control.Distributed.Process.Closure() +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Serializable + +import Data.Binary() +import Data.List + ( deleteBy + , find + ) +import Data.Typeable + +type PoolSize = Int +type SimpleTask a = Closure (Process a) + +data State a = State { + poolSize :: PoolSize + , active :: [(ProcessId, Recipient, Async a)] + , accepted :: [(Recipient, Closure (Process a))] + } deriving (Typeable) + +-- | Start a worker pool with an upper bound on the # of concurrent workers. +simplePool :: forall a . (Serializable a) + => PoolSize + -> Process (Either (InitResult (State a)) TerminateReason) +simplePool sz = + let server = defaultProcess { + dispatchers = [ + handleCallFrom (\s f (p :: Closure (Process a)) -> acceptTask s f p) + ] + } :: ProcessDefinition (State a) + in start sz init' server + where init' :: PoolSize -> Process (InitResult (State a)) + init' sz' = return $ InitOk (State sz' [] []) Infinity + +-- /call/ handler: accept a task and defer responding until "later" +acceptTask :: Serializable a + => State a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (State a) ()) +acceptTask s@(State sz' runQueue taskQueue) from task' = + let currentSz = length runQueue + in case currentSz >= sz' of + True -> do + s2 <- return $ s{ accepted = ((from, task'):taskQueue) } + noReply_ s2 + False -> do + proc <- unClosure task' + asyncHandle <- async proc + pid <- return $ asyncWorker asyncHandle + taskEntry <- return (pid, from, asyncHandle) + _ <- monitor pid + noReply_ s { accepted = ((from, task'):taskQueue) + , active = (taskEntry:runQueue) + } + +-- /info/ handler: a worker has exited, process the AsyncResult and send a reply +-- to the waiting client (who is still stuck in 'call' awaiting a response). +taskComplete :: forall a . Serializable a + => State a + -> ProcessMonitorNotification + -> Process (ProcessAction (State a)) +taskComplete s@(State _ runQ _) + (ProcessMonitorNotification _ pid _) = + let worker = findWorker pid runQ in + case worker of + Nothing -> continue s + Just t@(_, client, handle) -> + wait handle >>= respond client >> bump s t >>= continue + + where + respond :: Recipient + -> AsyncResult a + -> Process () + respond c (AsyncDone r) = replyTo c ((Right r) :: (Either String a)) + respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond _ _ = die $ TerminateOther "IllegalState" + + bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) + bump (State maxSz runQueue taskQueue) c@(pid', _, _) = + let runLen = (length runQueue) - 1 + _runQ2 = deleteBy (\_ (b, _, _) -> b == pid') c runQ + slots = maxSz - runLen + runnable = ((length taskQueue > 0) && (slots > 0)) in + case runnable of + True -> {- pull 'slots' tasks into the run queue -} die $ "WHAT!" + False -> die $ "oh, that!" + + -- take this task out of the run queue and bump pending tasks if needed + -- deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] + +findWorker :: ProcessId + -> [(ProcessId, Recipient, Async a)] + -> Maybe (ProcessId, Recipient, Async a) +findWorker key = find (\(pid,_,_) -> pid == key) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index a990d45c..82b526ea 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -25,6 +25,7 @@ import Data.Typeable (Typeable) import Data.DeriveTH import MathsDemo import Counter +import SimplePool import Prelude hiding (catch) @@ -127,16 +128,6 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False -testStateHandling :: TestResult Bool -> Process () -testStateHandling result = do - pid <- statefulServer "ok" - cast pid ("ko" :: String) -- updateState - liftIO $ putStrLn "cast sent!" - s2 <- call pid GetState -- getState - say $ "s2 = " ++ s2 - sleep $ seconds 2 - stash result (s2 == "ko") - -- MathDemo tests testAdd :: ProcessId -> TestResult Double -> Process () @@ -222,17 +213,17 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) -statefulServer :: String -> Process ProcessId -statefulServer st = - let b = defaultProcess { - dispatchers = [ - handleCast (\_ new -> continue new) - , handleCall (\s GetState -> reply s s) - ] - } :: ProcessDefinition String - in spawnLocal $ start st init' b >> return () - where init' :: String -> Process (InitResult String) - init' initS = return $ InitOk initS Infinity +-- workerPool :: Process ProcessId +-- workerPool = +-- let b = defaultProcess { +-- dispatchers = [ +-- handleCast (\_ new -> continue new) +-- , handleCall (\s GetState -> reply s s) +-- ] +-- } :: ProcessDefinition String +-- in spawnLocal $ start () init' b >> return () +-- where init' :: () -> Process (InitResult String) +-- init' = const (return $ InitOk () Infinity) tests :: NT.Transport -> IO [Test] tests transport = do From 67bb894506883e539f10e5506976b13cbec0c9e4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:53:09 +0000 Subject: [PATCH 0833/2357] Add 'handleCallFrom' and 'replyTo' support to GenProcess Provide support for deferred replies, add a simple worker pool prototype to demonstrate the API usage and provide a basis for testing. --- distributed-process-platform.cabal | 1 + tests/Counter.hs | 2 +- tests/SimplePool.hs | 111 +++++++++++++++++++++++++++++ tests/TestGenServer.hs | 33 ++++----- 4 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 tests/SimplePool.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3f6f7a7e..ab8a1438 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -164,6 +164,7 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, + SimplePool, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/tests/Counter.hs b/tests/Counter.hs index e0ee8116..89690101 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -80,7 +80,7 @@ serverDefinition = defaultProcess { dispatchers = [ handleCallIf (condition (\count Increment -> count >= 10))-- invariant (\_ (_ :: Increment) -> do - noReply_ (TerminateOther "Count > 10")) + haltNoReply_ (TerminateOther "Count > 10")) , handleCall handleIncrement , handleCall (\count Fetch -> reply count count) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs new file mode 100644 index 00000000..ab39542d --- /dev/null +++ b/tests/SimplePool.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- | Simple bounded (size) worker pool that accepts tasks and blocks +-- the caller until they've completed. Partly a /spike/ for that 'Task' API +-- and partly just a test bed for handling 'replyTo' in GenProcess. +-- +module SimplePool where + +import Control.Distributed.Process +import Control.Distributed.Process.Closure() +import Control.Distributed.Process.Platform.Async +import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.Time +import Control.Distributed.Process.Serializable + +import Data.Binary() +import Data.List + ( deleteBy + , find + ) +import Data.Typeable + +type PoolSize = Int +type SimpleTask a = Closure (Process a) + +data State a = State { + poolSize :: PoolSize + , active :: [(ProcessId, Recipient, Async a)] + , accepted :: [(Recipient, Closure (Process a))] + } deriving (Typeable) + +-- | Start a worker pool with an upper bound on the # of concurrent workers. +simplePool :: forall a . (Serializable a) + => PoolSize + -> Process (Either (InitResult (State a)) TerminateReason) +simplePool sz = + let server = defaultProcess { + dispatchers = [ + handleCallFrom (\s f (p :: Closure (Process a)) -> acceptTask s f p) + ] + } :: ProcessDefinition (State a) + in start sz init' server + where init' :: PoolSize -> Process (InitResult (State a)) + init' sz' = return $ InitOk (State sz' [] []) Infinity + +-- /call/ handler: accept a task and defer responding until "later" +acceptTask :: Serializable a + => State a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (State a) ()) +acceptTask s@(State sz' runQueue taskQueue) from task' = + let currentSz = length runQueue + in case currentSz >= sz' of + True -> do + s2 <- return $ s{ accepted = ((from, task'):taskQueue) } + noReply_ s2 + False -> do + proc <- unClosure task' + asyncHandle <- async proc + pid <- return $ asyncWorker asyncHandle + taskEntry <- return (pid, from, asyncHandle) + _ <- monitor pid + noReply_ s { accepted = ((from, task'):taskQueue) + , active = (taskEntry:runQueue) + } + +-- /info/ handler: a worker has exited, process the AsyncResult and send a reply +-- to the waiting client (who is still stuck in 'call' awaiting a response). +taskComplete :: forall a . Serializable a + => State a + -> ProcessMonitorNotification + -> Process (ProcessAction (State a)) +taskComplete s@(State _ runQ _) + (ProcessMonitorNotification _ pid _) = + let worker = findWorker pid runQ in + case worker of + Nothing -> continue s + Just t@(_, client, handle) -> + wait handle >>= respond client >> bump s t >>= continue + + where + respond :: Recipient + -> AsyncResult a + -> Process () + respond c (AsyncDone r) = replyTo c ((Right r) :: (Either String a)) + respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond _ _ = die $ TerminateOther "IllegalState" + + bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) + bump (State maxSz runQueue taskQueue) c@(pid', _, _) = + let runLen = (length runQueue) - 1 + _runQ2 = deleteBy (\_ (b, _, _) -> b == pid') c runQ + slots = maxSz - runLen + runnable = ((length taskQueue > 0) && (slots > 0)) in + case runnable of + True -> {- pull 'slots' tasks into the run queue -} die $ "WHAT!" + False -> die $ "oh, that!" + + -- take this task out of the run queue and bump pending tasks if needed + -- deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] + +findWorker :: ProcessId + -> [(ProcessId, Recipient, Async a)] + -> Maybe (ProcessId, Recipient, Async a) +findWorker key = find (\(pid,_,_) -> pid == key) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index a990d45c..82b526ea 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -25,6 +25,7 @@ import Data.Typeable (Typeable) import Data.DeriveTH import MathsDemo import Counter +import SimplePool import Prelude hiding (catch) @@ -127,16 +128,6 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False -testStateHandling :: TestResult Bool -> Process () -testStateHandling result = do - pid <- statefulServer "ok" - cast pid ("ko" :: String) -- updateState - liftIO $ putStrLn "cast sent!" - s2 <- call pid GetState -- getState - say $ "s2 = " ++ s2 - sleep $ seconds 2 - stash result (s2 == "ko") - -- MathDemo tests testAdd :: ProcessId -> TestResult Double -> Process () @@ -222,17 +213,17 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) -statefulServer :: String -> Process ProcessId -statefulServer st = - let b = defaultProcess { - dispatchers = [ - handleCast (\_ new -> continue new) - , handleCall (\s GetState -> reply s s) - ] - } :: ProcessDefinition String - in spawnLocal $ start st init' b >> return () - where init' :: String -> Process (InitResult String) - init' initS = return $ InitOk initS Infinity +-- workerPool :: Process ProcessId +-- workerPool = +-- let b = defaultProcess { +-- dispatchers = [ +-- handleCast (\_ new -> continue new) +-- , handleCall (\s GetState -> reply s s) +-- ] +-- } :: ProcessDefinition String +-- in spawnLocal $ start () init' b >> return () +-- where init' :: () -> Process (InitResult String) +-- init' = const (return $ InitOk () Infinity) tests :: NT.Transport -> IO [Test] tests transport = do From b9637909f0d379e8b571092edff323060a76810b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 17:53:09 +0000 Subject: [PATCH 0834/2357] Add 'handleCallFrom' and 'replyTo' support to GenProcess Provide support for deferred replies, add a simple worker pool prototype to demonstrate the API usage and provide a basis for testing. --- distributed-process-platform.cabal | 1 + .../Process/Platform/GenProcess.hs | 101 ++++++++++++++---- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 3f6f7a7e..ab8a1438 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -164,6 +164,7 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, + SimplePool, Control.Distributed.Process.Platform.Async.Types extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 93258557..5732909f 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -162,6 +162,7 @@ module Control.Distributed.Process.Platform.GenProcess , replyWith , noReply , noReply_ + , haltNoReply_ , continue , continue_ , timeoutAfter @@ -170,9 +171,12 @@ module Control.Distributed.Process.Platform.GenProcess , hibernate_ , stop , stop_ + , replyTo -- * Handler callback creation , handleCall , handleCallIf + , handleCallFrom + , handleCallFromIf , handleCast , handleCastIf , handleInfo @@ -415,8 +419,26 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Async b) -callAsync sid msg = do - async $ do -- note [call using async] +callAsync = callAsyncUsing async + +-- | As 'callAsync' but takes a function that can be used to generate an async +-- task and return an async handle to it. This can be used to switch between +-- async implementations, by e.g., using an async channel instead of the default +-- STM based handle. +-- +-- See 'callAsync' +-- +-- See "Control.Distributed.Process.Platform.Async" +-- +-- See "Control.Distributed.Process.Platform.Async.AsyncChan" +-- +-- See "Control.Distributed.Process.Platform.Async.AsyncSTM" +-- +callAsyncUsing :: forall a b . (Serializable a, Serializable b) + => (Process b -> Process (Async b)) + -> ProcessId -> a -> Process (Async b) +callAsyncUsing asyncStart sid msg = do + asyncStart $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) @@ -497,9 +519,14 @@ replyWith msg st = return $ ProcessReply msg st noReply :: (Serializable r) => ProcessAction s -> Process (ProcessReply s r) noReply = return . NoReply +-- | Continue without giving a reply to the caller - equivalent to 'continue', +-- but usable in a callback passed to the 'handleCall' family of functions. +noReply_ :: forall s r . (Serializable r) => s -> Process (ProcessReply s r) +noReply_ s = continue s >>= noReply + -- | Halt a call handler without regard for the expected return type. -noReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) -noReply_ r = stop r >>= noReply +haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) +haltNoReply_ r = stop r >>= noReply -- | Instructs the process to continue running and receiving messages. continue :: s -> Process (ProcessAction s) @@ -549,6 +576,14 @@ stop r = return $ ProcessStop r stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r +sendTo :: (Serializable m) => Recipient -> m -> Process () +sendTo (SendToPid p) m = send p m +sendTo (SendToService s) m = nsend s m +sendTo (SendToRemoteService s n) m = nsendRemote n s m + +replyTo :: (Serializable m) => Recipient -> m -> Process () +replyTo client msg = sendTo client (CallResponse msg) + -------------------------------------------------------------------------------- -- Wrapping handler expressions in Dispatcher and InfoDispatcher -- -------------------------------------------------------------------------------- @@ -585,7 +620,7 @@ handleCallIf_ cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h p) >>= mkReply c s - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop @@ -623,14 +658,39 @@ handleCallIf cond handler -> Message a -> Process (ProcessAction s) doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - -- handling 'reply-to' in the main process loop is awkward at best, - -- so we handle it here instead and return the 'action' to the loop - mkReply :: (Serializable b) - => Recipient -> ProcessReply s b -> Process (ProcessAction s) - mkReply _ (NoReply a) = return a - mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a +-- | As 'handleCall' but passes the 'Recipient' to the handler function. +-- This can be useful if you wish to /reply later/ to the caller by, e.g., +-- spawning a process to do some work and have it @replyTo caller response@ +-- out of band. In this case the callback can pass the 'Recipient' to the +-- worker (or stash it away itself) and return 'noReply'. +-- +handleCallFrom :: forall s a b . (Serializable a, Serializable b) + => (s -> Recipient -> a -> Process (ProcessReply s b)) + -> Dispatcher s +handleCallFrom = handleCallFromIf $ state (const True) + +-- | As 'handleCallFrom' but only runs the handler if the supplied 'Condition' +-- evaluates to @True@. +-- +handleCallFromIf :: forall s a b . (Serializable a, Serializable b) + => Condition s a -- ^ predicate that must be satisfied for the handler to run + -> (s -> Recipient -> a -> Process (ProcessReply s b)) + -- ^ a reply yielding function over the process state, sender and input message + -> Dispatcher s +handleCallFromIf cond handler + = DispatchIf { + dispatch = doHandle handler + , dispatchIf = checkCall cond + } + where doHandle :: (Serializable a, Serializable b) + => (s -> Recipient -> a -> Process (ProcessReply s b)) + -> s + -> Message a + -> Process (ProcessAction s) + doHandle h s (CallMessage p c) = (h s c p) >>= mkReply c + doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! -- | Constructs a 'cast' handler from an ordinary function in the 'Process' -- monad. @@ -737,6 +797,15 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) +-- handling 'reply-to' in the main process loop is awkward at best, +-- so we handle it here instead and return the 'action' to the loop +mkReply :: (Serializable b) + => Recipient -> ProcessReply s b -> Process (ProcessAction s) +mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a +mkReply _ (NoReply a) = return a + +-- these functions are the inverse of 'condition', 'state' and 'input' + check :: forall s m . (Serializable m) => Condition s m -> s @@ -841,11 +910,3 @@ processReceive ms handleTimeout st d = do case d' of Infinity -> receiveWait matches >>= return . Just Delay t' -> receiveTimeout (asTimeout t') matches - --- internal/utility - -sendTo :: (Serializable m) => Recipient -> m -> Process () -sendTo (SendToPid p) m = send p m -sendTo (SendToService s) m = nsend s m -sendTo (SendToRemoteService s n) m = nsendRemote n s m - From 26664eef9c5d43fb50dd5f4840fe5849a5e67634 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:22:47 +0000 Subject: [PATCH 0835/2357] cosmetic --- tests/SimplePool.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index ab39542d..10e0741a 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -79,9 +79,8 @@ taskComplete s@(State _ runQ _) (ProcessMonitorNotification _ pid _) = let worker = findWorker pid runQ in case worker of - Nothing -> continue s - Just t@(_, client, handle) -> - wait handle >>= respond client >> bump s t >>= continue + Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue + Nothing -> continue s where respond :: Recipient @@ -99,7 +98,7 @@ taskComplete s@(State _ runQ _) slots = maxSz - runLen runnable = ((length taskQueue > 0) && (slots > 0)) in case runnable of - True -> {- pull 'slots' tasks into the run queue -} die $ "WHAT!" + True -> {- pull `slots' tasks over to the run queue -} die $ "WHAT!" False -> die $ "oh, that!" -- take this task out of the run queue and bump pending tasks if needed From cc7d51d5a574ffecff8762a9cd93e8792db333ef Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:22:47 +0000 Subject: [PATCH 0836/2357] cosmetic --- tests/SimplePool.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index ab39542d..10e0741a 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -79,9 +79,8 @@ taskComplete s@(State _ runQ _) (ProcessMonitorNotification _ pid _) = let worker = findWorker pid runQ in case worker of - Nothing -> continue s - Just t@(_, client, handle) -> - wait handle >>= respond client >> bump s t >>= continue + Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue + Nothing -> continue s where respond :: Recipient @@ -99,7 +98,7 @@ taskComplete s@(State _ runQ _) slots = maxSz - runLen runnable = ((length taskQueue > 0) && (slots > 0)) in case runnable of - True -> {- pull 'slots' tasks into the run queue -} die $ "WHAT!" + True -> {- pull `slots' tasks over to the run queue -} die $ "WHAT!" False -> die $ "oh, that!" -- take this task out of the run queue and bump pending tasks if needed From a3f3300e9e5bc1d7cccb28f29d74c9a0a25b28c9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:22:47 +0000 Subject: [PATCH 0837/2357] cosmetic --- tests/SimplePool.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index ab39542d..10e0741a 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -79,9 +79,8 @@ taskComplete s@(State _ runQ _) (ProcessMonitorNotification _ pid _) = let worker = findWorker pid runQ in case worker of - Nothing -> continue s - Just t@(_, client, handle) -> - wait handle >>= respond client >> bump s t >>= continue + Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue + Nothing -> continue s where respond :: Recipient @@ -99,7 +98,7 @@ taskComplete s@(State _ runQ _) slots = maxSz - runLen runnable = ((length taskQueue > 0) && (slots > 0)) in case runnable of - True -> {- pull 'slots' tasks into the run queue -} die $ "WHAT!" + True -> {- pull `slots' tasks over to the run queue -} die $ "WHAT!" False -> die $ "oh, that!" -- take this task out of the run queue and bump pending tasks if needed From 401c589f1bdaa9eff117f1ee89ed2a9546f9c43d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:24:05 +0000 Subject: [PATCH 0838/2357] silence compiler warnings --- src/Control/Distributed/Process/Platform/GenProcess.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5732909f..c8ecf1a7 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -619,14 +619,14 @@ handleCallIf_ cond handler -> s -> Message a -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h p) >>= mkReply c s + doHandle h s (CallMessage p c) = (h p) >>= mkCallReply c s doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop - mkReply :: (Serializable b) + mkCallReply :: (Serializable b) => Recipient -> s -> b -> Process (ProcessAction s) - mkReply c s m = sendTo c (CallResponse m) >> continue s + mkCallReply c s m = sendTo c (CallResponse m) >> continue s -- | Constructs a 'call' handler from a function in the 'Process' monad. -- > handleCall = handleCallIf (const True) From 8d647bbfec499dfd9f43625fa38f7aa1939dee58 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:24:05 +0000 Subject: [PATCH 0839/2357] silence compiler warnings --- src/Control/Distributed/Process/Platform/GenProcess.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 5732909f..c8ecf1a7 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -619,14 +619,14 @@ handleCallIf_ cond handler -> s -> Message a -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h p) >>= mkReply c s + doHandle h s (CallMessage p c) = (h p) >>= mkCallReply c s doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop - mkReply :: (Serializable b) + mkCallReply :: (Serializable b) => Recipient -> s -> b -> Process (ProcessAction s) - mkReply c s m = sendTo c (CallResponse m) >> continue s + mkCallReply c s m = sendTo c (CallResponse m) >> continue s -- | Constructs a 'call' handler from a function in the 'Process' monad. -- > handleCall = handleCallIf (const True) From bb22ba605aaaac6500eedec81d421ec37e81810e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:56:50 +0000 Subject: [PATCH 0840/2357] SimplePool (demo) task submission and blocking waits (deferred reply) --- tests/SimplePool.hs | 60 ++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 10e0741a..de82ff5c 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -10,7 +10,7 @@ -- module SimplePool where -import Control.Distributed.Process +import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess @@ -40,34 +40,46 @@ simplePool :: forall a . (Serializable a) simplePool sz = let server = defaultProcess { dispatchers = [ - handleCallFrom (\s f (p :: Closure (Process a)) -> acceptTask s f p) + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] } :: ProcessDefinition (State a) in start sz init' server where init' :: PoolSize -> Process (InitResult (State a)) init' sz' = return $ InitOk (State sz' [] []) Infinity +-- enqueues the task in the pool and blocks +-- the caller until the task is complete +executeTask :: Serializable a + => ProcessId + -> Closure (Process a) + -> Process (Either String a) +executeTask sid t = call sid t + -- /call/ handler: accept a task and defer responding until "later" +storeTask :: Serializable a + => State a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (State a) ()) +storeTask s r c = acceptTask s r c >>= noReply_ + acceptTask :: Serializable a => State a -> Recipient -> Closure (Process a) - -> Process (ProcessReply (State a) ()) + -> Process (State a) acceptTask s@(State sz' runQueue taskQueue) from task' = let currentSz = length runQueue in case currentSz >= sz' of True -> do - s2 <- return $ s{ accepted = ((from, task'):taskQueue) } - noReply_ s2 + return $ s { accepted = ((from, task'):taskQueue) } False -> do proc <- unClosure task' asyncHandle <- async proc pid <- return $ asyncWorker asyncHandle taskEntry <- return (pid, from, asyncHandle) - _ <- monitor pid - noReply_ s { accepted = ((from, task'):taskQueue) - , active = (taskEntry:runQueue) - } + _ <- monitor pid + return s { active = (taskEntry:runQueue) } -- /info/ handler: a worker has exited, process the AsyncResult and send a reply -- to the waiting client (who is still stuck in 'call' awaiting a response). @@ -90,21 +102,29 @@ taskComplete s@(State _ runQ _) respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - + bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) - bump (State maxSz runQueue taskQueue) c@(pid', _, _) = + bump st@(State maxSz runQueue _) worker = let runLen = (length runQueue) - 1 - _runQ2 = deleteBy (\_ (b, _, _) -> b == pid') c runQ - slots = maxSz - runLen - runnable = ((length taskQueue > 0) && (slots > 0)) in - case runnable of - True -> {- pull `slots' tasks over to the run queue -} die $ "WHAT!" - False -> die $ "oh, that!" - - -- take this task out of the run queue and bump pending tasks if needed - -- deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] + runQ2 = deleteFromRunQueue worker runQueue + slots = (maxSz - runLen) in + case (slots > 0) of + True -> fillSlots slots st { active = runQ2 } + False -> return $ st + + fillSlots :: Int -> State a -> Process (State a) + fillSlots _ st'@(State _ _ []) = return st' + fillSlots 0 st' = return st' + fillSlots n st'@(State _ _ ((tr,tc):ts)) = + let ns = st' { accepted = ts } + in acceptTask ns tr tc >>= fillSlots (n-1) findWorker :: ProcessId -> [(ProcessId, Recipient, Async a)] -> Maybe (ProcessId, Recipient, Async a) findWorker key = find (\(pid,_,_) -> pid == key) + +deleteFromRunQueue :: (ProcessId, Recipient, Async a) + -> [(ProcessId, Recipient, Async a)] + -> [(ProcessId, Recipient, Async a)] +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ From 1de86665550abaa42d6fd68b03716bf95a4a9aa5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:56:50 +0000 Subject: [PATCH 0841/2357] SimplePool (demo) task submission and blocking waits (deferred reply) --- tests/SimplePool.hs | 60 ++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 10e0741a..de82ff5c 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -10,7 +10,7 @@ -- module SimplePool where -import Control.Distributed.Process +import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess @@ -40,34 +40,46 @@ simplePool :: forall a . (Serializable a) simplePool sz = let server = defaultProcess { dispatchers = [ - handleCallFrom (\s f (p :: Closure (Process a)) -> acceptTask s f p) + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] } :: ProcessDefinition (State a) in start sz init' server where init' :: PoolSize -> Process (InitResult (State a)) init' sz' = return $ InitOk (State sz' [] []) Infinity +-- enqueues the task in the pool and blocks +-- the caller until the task is complete +executeTask :: Serializable a + => ProcessId + -> Closure (Process a) + -> Process (Either String a) +executeTask sid t = call sid t + -- /call/ handler: accept a task and defer responding until "later" +storeTask :: Serializable a + => State a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (State a) ()) +storeTask s r c = acceptTask s r c >>= noReply_ + acceptTask :: Serializable a => State a -> Recipient -> Closure (Process a) - -> Process (ProcessReply (State a) ()) + -> Process (State a) acceptTask s@(State sz' runQueue taskQueue) from task' = let currentSz = length runQueue in case currentSz >= sz' of True -> do - s2 <- return $ s{ accepted = ((from, task'):taskQueue) } - noReply_ s2 + return $ s { accepted = ((from, task'):taskQueue) } False -> do proc <- unClosure task' asyncHandle <- async proc pid <- return $ asyncWorker asyncHandle taskEntry <- return (pid, from, asyncHandle) - _ <- monitor pid - noReply_ s { accepted = ((from, task'):taskQueue) - , active = (taskEntry:runQueue) - } + _ <- monitor pid + return s { active = (taskEntry:runQueue) } -- /info/ handler: a worker has exited, process the AsyncResult and send a reply -- to the waiting client (who is still stuck in 'call' awaiting a response). @@ -90,21 +102,29 @@ taskComplete s@(State _ runQ _) respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - + bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) - bump (State maxSz runQueue taskQueue) c@(pid', _, _) = + bump st@(State maxSz runQueue _) worker = let runLen = (length runQueue) - 1 - _runQ2 = deleteBy (\_ (b, _, _) -> b == pid') c runQ - slots = maxSz - runLen - runnable = ((length taskQueue > 0) && (slots > 0)) in - case runnable of - True -> {- pull `slots' tasks over to the run queue -} die $ "WHAT!" - False -> die $ "oh, that!" - - -- take this task out of the run queue and bump pending tasks if needed - -- deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] + runQ2 = deleteFromRunQueue worker runQueue + slots = (maxSz - runLen) in + case (slots > 0) of + True -> fillSlots slots st { active = runQ2 } + False -> return $ st + + fillSlots :: Int -> State a -> Process (State a) + fillSlots _ st'@(State _ _ []) = return st' + fillSlots 0 st' = return st' + fillSlots n st'@(State _ _ ((tr,tc):ts)) = + let ns = st' { accepted = ts } + in acceptTask ns tr tc >>= fillSlots (n-1) findWorker :: ProcessId -> [(ProcessId, Recipient, Async a)] -> Maybe (ProcessId, Recipient, Async a) findWorker key = find (\(pid,_,_) -> pid == key) + +deleteFromRunQueue :: (ProcessId, Recipient, Async a) + -> [(ProcessId, Recipient, Async a)] + -> [(ProcessId, Recipient, Async a)] +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ From 69a7f9ff3438f5ab2ba047ea83d98dec499d7b0c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 19:56:50 +0000 Subject: [PATCH 0842/2357] SimplePool (demo) task submission and blocking waits (deferred reply) --- tests/SimplePool.hs | 60 ++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 10e0741a..de82ff5c 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -10,7 +10,7 @@ -- module SimplePool where -import Control.Distributed.Process +import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess @@ -40,34 +40,46 @@ simplePool :: forall a . (Serializable a) simplePool sz = let server = defaultProcess { dispatchers = [ - handleCallFrom (\s f (p :: Closure (Process a)) -> acceptTask s f p) + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] } :: ProcessDefinition (State a) in start sz init' server where init' :: PoolSize -> Process (InitResult (State a)) init' sz' = return $ InitOk (State sz' [] []) Infinity +-- enqueues the task in the pool and blocks +-- the caller until the task is complete +executeTask :: Serializable a + => ProcessId + -> Closure (Process a) + -> Process (Either String a) +executeTask sid t = call sid t + -- /call/ handler: accept a task and defer responding until "later" +storeTask :: Serializable a + => State a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (State a) ()) +storeTask s r c = acceptTask s r c >>= noReply_ + acceptTask :: Serializable a => State a -> Recipient -> Closure (Process a) - -> Process (ProcessReply (State a) ()) + -> Process (State a) acceptTask s@(State sz' runQueue taskQueue) from task' = let currentSz = length runQueue in case currentSz >= sz' of True -> do - s2 <- return $ s{ accepted = ((from, task'):taskQueue) } - noReply_ s2 + return $ s { accepted = ((from, task'):taskQueue) } False -> do proc <- unClosure task' asyncHandle <- async proc pid <- return $ asyncWorker asyncHandle taskEntry <- return (pid, from, asyncHandle) - _ <- monitor pid - noReply_ s { accepted = ((from, task'):taskQueue) - , active = (taskEntry:runQueue) - } + _ <- monitor pid + return s { active = (taskEntry:runQueue) } -- /info/ handler: a worker has exited, process the AsyncResult and send a reply -- to the waiting client (who is still stuck in 'call' awaiting a response). @@ -90,21 +102,29 @@ taskComplete s@(State _ runQ _) respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - + bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) - bump (State maxSz runQueue taskQueue) c@(pid', _, _) = + bump st@(State maxSz runQueue _) worker = let runLen = (length runQueue) - 1 - _runQ2 = deleteBy (\_ (b, _, _) -> b == pid') c runQ - slots = maxSz - runLen - runnable = ((length taskQueue > 0) && (slots > 0)) in - case runnable of - True -> {- pull `slots' tasks over to the run queue -} die $ "WHAT!" - False -> die $ "oh, that!" - - -- take this task out of the run queue and bump pending tasks if needed - -- deleteBy :: (a -> a -> Bool) -> a -> [a] -> [a] + runQ2 = deleteFromRunQueue worker runQueue + slots = (maxSz - runLen) in + case (slots > 0) of + True -> fillSlots slots st { active = runQ2 } + False -> return $ st + + fillSlots :: Int -> State a -> Process (State a) + fillSlots _ st'@(State _ _ []) = return st' + fillSlots 0 st' = return st' + fillSlots n st'@(State _ _ ((tr,tc):ts)) = + let ns = st' { accepted = ts } + in acceptTask ns tr tc >>= fillSlots (n-1) findWorker :: ProcessId -> [(ProcessId, Recipient, Async a)] -> Maybe (ProcessId, Recipient, Async a) findWorker key = find (\(pid,_,_) -> pid == key) + +deleteFromRunQueue :: (ProcessId, Recipient, Async a) + -> [(ProcessId, Recipient, Async a)] + -> [(ProcessId, Recipient, Async a)] +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ From 84155dfedae275eb339aa4b5c9def3fe37a5b5c4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 20:00:19 +0000 Subject: [PATCH 0843/2357] handle the task-complete monitor notification --- tests/SimplePool.hs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index de82ff5c..9255d181 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -39,10 +39,13 @@ simplePool :: forall a . (Serializable a) -> Process (Either (InitResult (State a)) TerminateReason) simplePool sz = let server = defaultProcess { - dispatchers = [ + dispatchers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) - ] - } :: ProcessDefinition (State a) + ] + , infoHandlers = [ + handleInfo taskComplete + ] + } :: ProcessDefinition (State a) in start sz init' server where init' :: PoolSize -> Process (InitResult (State a)) init' sz' = return $ InitOk (State sz' [] []) Infinity From def0e18176ce08790fe2d7878d3ba79a540eecc1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 20:00:19 +0000 Subject: [PATCH 0844/2357] handle the task-complete monitor notification --- tests/SimplePool.hs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index de82ff5c..9255d181 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -39,10 +39,13 @@ simplePool :: forall a . (Serializable a) -> Process (Either (InitResult (State a)) TerminateReason) simplePool sz = let server = defaultProcess { - dispatchers = [ + dispatchers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) - ] - } :: ProcessDefinition (State a) + ] + , infoHandlers = [ + handleInfo taskComplete + ] + } :: ProcessDefinition (State a) in start sz init' server where init' :: PoolSize -> Process (InitResult (State a)) init' sz' = return $ InitOk (State sz' [] []) Infinity From c232613badc52e285f3276c3ebe23eecf103956f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 20:00:19 +0000 Subject: [PATCH 0845/2357] handle the task-complete monitor notification --- tests/SimplePool.hs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index de82ff5c..9255d181 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -39,10 +39,13 @@ simplePool :: forall a . (Serializable a) -> Process (Either (InitResult (State a)) TerminateReason) simplePool sz = let server = defaultProcess { - dispatchers = [ + dispatchers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) - ] - } :: ProcessDefinition (State a) + ] + , infoHandlers = [ + handleInfo taskComplete + ] + } :: ProcessDefinition (State a) in start sz init' server where init' :: PoolSize -> Process (InitResult (State a)) init' sz' = return $ InitOk (State sz' [] []) Infinity From a39e3cbafa8fa57c40528431faef90b729ce7090 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 20:09:55 +0000 Subject: [PATCH 0846/2357] Refactor SimplePool and fix a bug fillSlots already handles the case for zero, plus remember to strip the completed task from the run queue. --- tests/SimplePool.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 9255d181..abf672cb 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -110,10 +110,8 @@ taskComplete s@(State _ runQ _) bump st@(State maxSz runQueue _) worker = let runLen = (length runQueue) - 1 runQ2 = deleteFromRunQueue worker runQueue - slots = (maxSz - runLen) in - case (slots > 0) of - True -> fillSlots slots st { active = runQ2 } - False -> return $ st + slots = (maxSz - runLen) + in fillSlots slots st { active = runQ2 } fillSlots :: Int -> State a -> Process (State a) fillSlots _ st'@(State _ _ []) = return st' From 3f3f4518a7721f4b984eed5e132e704171a47f3c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 20:09:55 +0000 Subject: [PATCH 0847/2357] Refactor SimplePool and fix a bug fillSlots already handles the case for zero, plus remember to strip the completed task from the run queue. --- tests/SimplePool.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 9255d181..abf672cb 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -110,10 +110,8 @@ taskComplete s@(State _ runQ _) bump st@(State maxSz runQueue _) worker = let runLen = (length runQueue) - 1 runQ2 = deleteFromRunQueue worker runQueue - slots = (maxSz - runLen) in - case (slots > 0) of - True -> fillSlots slots st { active = runQ2 } - False -> return $ st + slots = (maxSz - runLen) + in fillSlots slots st { active = runQ2 } fillSlots :: Int -> State a -> Process (State a) fillSlots _ st'@(State _ _ []) = return st' From 4aae997ae6cb283a6840407abb1f7989eb103767 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 22 Jan 2013 20:09:55 +0000 Subject: [PATCH 0848/2357] Refactor SimplePool and fix a bug fillSlots already handles the case for zero, plus remember to strip the completed task from the run queue. --- tests/SimplePool.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 9255d181..abf672cb 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -110,10 +110,8 @@ taskComplete s@(State _ runQ _) bump st@(State maxSz runQueue _) worker = let runLen = (length runQueue) - 1 runQ2 = deleteFromRunQueue worker runQueue - slots = (maxSz - runLen) in - case (slots > 0) of - True -> fillSlots slots st { active = runQ2 } - False -> return $ st + slots = (maxSz - runLen) + in fillSlots slots st { active = runQ2 } fillSlots :: Int -> State a -> Process (State a) fillSlots _ st'@(State _ _ []) = return st' From 58cf5ac741f5eb7585b8266b83e60c688fdc116a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 16:51:11 +0000 Subject: [PATCH 0849/2357] cosmetic (ish) --- tests/SimplePool.hs | 65 ++++++++++++++++++++---------------- tests/TestGenServer.hs | 75 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 31 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index abf672cb..8f460ab4 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -16,7 +16,7 @@ import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable - +import Control.Exception hiding (catch) import Data.Binary() import Data.List ( deleteBy @@ -24,33 +24,42 @@ import Data.List ) import Data.Typeable +import Prelude hiding (catch) + type PoolSize = Int type SimpleTask a = Closure (Process a) -data State a = State { +data Pool a = Pool { poolSize :: PoolSize , active :: [(ProcessId, Recipient, Async a)] , accepted :: [(Recipient, Closure (Process a))] } deriving (Typeable) --- | Start a worker pool with an upper bound on the # of concurrent workers. -simplePool :: forall a . (Serializable a) - => PoolSize - -> Process (Either (InitResult (State a)) TerminateReason) -simplePool sz = - let server = defaultProcess { +poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) +poolServer = + defaultProcess { dispatchers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] , infoHandlers = [ handleInfo taskComplete ] - } :: ProcessDefinition (State a) - in start sz init' server - where init' :: PoolSize -> Process (InitResult (State a)) - init' sz' = return $ InitOk (State sz' [] []) Infinity + } :: ProcessDefinition (Pool a) + +-- | Start a worker pool with an upper bound on the # of concurrent workers. +simplePool :: forall a . (Serializable a) + => PoolSize + -> ProcessDefinition (Pool a) + -> Process (Either (InitResult (Pool a)) TerminateReason) +simplePool sz server = + start sz init' server + `catch` (\(e :: SomeException) -> do + say $ "terminating with " ++ (show e) + liftIO $ throwIO e) + where init' :: PoolSize -> Process (InitResult (Pool a)) + init' sz' = return $ InitOk (Pool sz' [] []) Infinity --- enqueues the task in the pool and blocks +-- enqueues the task in the pool and blocks -- the caller until the task is complete executeTask :: Serializable a => ProcessId @@ -60,18 +69,18 @@ executeTask sid t = call sid t -- /call/ handler: accept a task and defer responding until "later" storeTask :: Serializable a - => State a + => Pool a -> Recipient -> Closure (Process a) - -> Process (ProcessReply (State a) ()) + -> Process (ProcessReply (Pool a) ()) storeTask s r c = acceptTask s r c >>= noReply_ acceptTask :: Serializable a - => State a + => Pool a -> Recipient -> Closure (Process a) - -> Process (State a) -acceptTask s@(State sz' runQueue taskQueue) from task' = + -> Process (Pool a) +acceptTask s@(Pool sz' runQueue taskQueue) from task' = let currentSz = length runQueue in case currentSz >= sz' of True -> do @@ -87,10 +96,10 @@ acceptTask s@(State sz' runQueue taskQueue) from task' = -- /info/ handler: a worker has exited, process the AsyncResult and send a reply -- to the waiting client (who is still stuck in 'call' awaiting a response). taskComplete :: forall a . Serializable a - => State a + => Pool a -> ProcessMonitorNotification - -> Process (ProcessAction (State a)) -taskComplete s@(State _ runQ _) + -> Process (ProcessAction (Pool a)) +taskComplete s@(Pool _ runQ _) (ProcessMonitorNotification _ pid _) = let worker = findWorker pid runQ in case worker of @@ -106,17 +115,17 @@ taskComplete s@(State _ runQ _) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) - bump st@(State maxSz runQueue _) worker = + bump :: Pool a -> (ProcessId, Recipient, Async a) -> Process (Pool a) + bump st@(Pool maxSz runQueue _) worker = let runLen = (length runQueue) - 1 runQ2 = deleteFromRunQueue worker runQueue slots = (maxSz - runLen) in fillSlots slots st { active = runQ2 } - fillSlots :: Int -> State a -> Process (State a) - fillSlots _ st'@(State _ _ []) = return st' - fillSlots 0 st' = return st' - fillSlots n st'@(State _ _ ((tr,tc):ts)) = + fillSlots :: Int -> Pool a -> Process (Pool a) + fillSlots _ st'@(Pool _ _ []) = return st' + fillSlots 0 st' = return st' + fillSlots n st'@(Pool _ _ ((tr,tc):ts)) = let ns = st' { accepted = ts } in acceptTask ns tr tc >>= fillSlots (n-1) @@ -128,4 +137,4 @@ findWorker key = find (\(pid,_,_) -> pid == key) deleteFromRunQueue :: (ProcessId, Recipient, Async a) -> [(ProcessId, Recipient, Async a)] -> [(ProcessId, Recipient, Async a)] -deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 82b526ea..3a943238 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -11,8 +11,9 @@ module Main where import Control.Concurrent.MVar import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Closure import Control.Distributed.Process.Node -import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform hiding (__remoteTable) import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test @@ -34,11 +35,77 @@ import Test.Framework.Providers.HUnit (testCase) import TestUtils import qualified Network.Transport as NT +import Control.Monad (void) + +-- utilities data GetState = GetState deriving (Typeable, Show, Eq) $(derive makeBinary ''GetState) +waitForExit :: MVar (Either (InitResult ()) TerminateReason) + -> Process (Maybe TerminateReason) +waitForExit exitReason = do + -- we *might* end up blocked here, so ensure the test doesn't jam up! + self <- getSelfPid + tref <- killAfter (within 10 Seconds) self "testcast timed out" + tr <- liftIO $ takeMVar exitReason + cancelTimer tref + case tr of + Right r -> return (Just r) + Left _ -> return Nothing + +server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) +server = mkServer Terminate + +mkServer :: UnhandledMessagePolicy + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +mkServer policy = + let s = statelessProcess { + dispatchers = [ + -- note: state is passed here, as a 'stateless' process is + -- in fact process definition whose state is () + + handleCastIf (input (\msg -> msg == "stop")) + (\_ _ -> stop TerminateNormal) + + , handleCall (\s' (m :: String) -> reply m s') + , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" + + , handleCast (\s' ("ping", pid :: ProcessId) -> + send pid "pong" >> continue s') + , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) + (\("timeout", Delay d) -> timeoutAfter_ d) + + , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) + ] + , unhandledMessagePolicy = policy + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + } + in do + exitReason <- liftIO $ newEmptyMVar + pid <- spawnLocal $ do + catch (start () (statelessInit Infinity) s >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) + return (pid, exitReason) + +startTestPool :: Int -> Process ProcessId +startTestPool s = spawnLocal $ do + _ <- runPool s + return () + +runPool :: Int -> Process (Either (InitResult (Pool String)) TerminateReason) +runPool s = + let s' = poolServer :: ProcessDefinition (Pool String) + in simplePool s s' + +sampleTask :: (TimeInterval, String) -> Process String +sampleTask (t, s) = sleep t >> return s + +$(remotable ['sampleTask]) + +-- test cases + testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -153,7 +220,7 @@ testCounterExceedsLimit pid result = do 7 <- getCount pid -- exceed the limit - 3 `times` (incCount pid >> return ()) + 3 `times` (void $ incCount pid) -- this time we should fail _ <- (incCount pid) @@ -224,10 +291,12 @@ mkServer policy = -- in spawnLocal $ start () init' b >> return () -- where init' :: () -> Process (InitResult String) -- init' = const (return $ InitOk () Infinity) +myRemoteTable :: RemoteTable +myRemoteTable = Main.__remoteTable initRemoteTable tests :: NT.Transport -> IO [Test] tests transport = do - localNode <- newLocalNode transport initRemoteTable + localNode <- newLocalNode transport myRemoteTable mpid <- newEmptyMVar _ <- forkProcess localNode $ launchMathServer >>= stash mpid pid <- takeMVar mpid From 9e23bc85e7d09f6169e64c49f64214de1ae8ee1a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 16:51:11 +0000 Subject: [PATCH 0850/2357] cosmetic (ish) --- tests/SimplePool.hs | 65 ++++++++++++++++++++---------------- tests/TestGenServer.hs | 75 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 31 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index abf672cb..8f460ab4 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -16,7 +16,7 @@ import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable - +import Control.Exception hiding (catch) import Data.Binary() import Data.List ( deleteBy @@ -24,33 +24,42 @@ import Data.List ) import Data.Typeable +import Prelude hiding (catch) + type PoolSize = Int type SimpleTask a = Closure (Process a) -data State a = State { +data Pool a = Pool { poolSize :: PoolSize , active :: [(ProcessId, Recipient, Async a)] , accepted :: [(Recipient, Closure (Process a))] } deriving (Typeable) --- | Start a worker pool with an upper bound on the # of concurrent workers. -simplePool :: forall a . (Serializable a) - => PoolSize - -> Process (Either (InitResult (State a)) TerminateReason) -simplePool sz = - let server = defaultProcess { +poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) +poolServer = + defaultProcess { dispatchers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] , infoHandlers = [ handleInfo taskComplete ] - } :: ProcessDefinition (State a) - in start sz init' server - where init' :: PoolSize -> Process (InitResult (State a)) - init' sz' = return $ InitOk (State sz' [] []) Infinity + } :: ProcessDefinition (Pool a) + +-- | Start a worker pool with an upper bound on the # of concurrent workers. +simplePool :: forall a . (Serializable a) + => PoolSize + -> ProcessDefinition (Pool a) + -> Process (Either (InitResult (Pool a)) TerminateReason) +simplePool sz server = + start sz init' server + `catch` (\(e :: SomeException) -> do + say $ "terminating with " ++ (show e) + liftIO $ throwIO e) + where init' :: PoolSize -> Process (InitResult (Pool a)) + init' sz' = return $ InitOk (Pool sz' [] []) Infinity --- enqueues the task in the pool and blocks +-- enqueues the task in the pool and blocks -- the caller until the task is complete executeTask :: Serializable a => ProcessId @@ -60,18 +69,18 @@ executeTask sid t = call sid t -- /call/ handler: accept a task and defer responding until "later" storeTask :: Serializable a - => State a + => Pool a -> Recipient -> Closure (Process a) - -> Process (ProcessReply (State a) ()) + -> Process (ProcessReply (Pool a) ()) storeTask s r c = acceptTask s r c >>= noReply_ acceptTask :: Serializable a - => State a + => Pool a -> Recipient -> Closure (Process a) - -> Process (State a) -acceptTask s@(State sz' runQueue taskQueue) from task' = + -> Process (Pool a) +acceptTask s@(Pool sz' runQueue taskQueue) from task' = let currentSz = length runQueue in case currentSz >= sz' of True -> do @@ -87,10 +96,10 @@ acceptTask s@(State sz' runQueue taskQueue) from task' = -- /info/ handler: a worker has exited, process the AsyncResult and send a reply -- to the waiting client (who is still stuck in 'call' awaiting a response). taskComplete :: forall a . Serializable a - => State a + => Pool a -> ProcessMonitorNotification - -> Process (ProcessAction (State a)) -taskComplete s@(State _ runQ _) + -> Process (ProcessAction (Pool a)) +taskComplete s@(Pool _ runQ _) (ProcessMonitorNotification _ pid _) = let worker = findWorker pid runQ in case worker of @@ -106,17 +115,17 @@ taskComplete s@(State _ runQ _) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) - bump st@(State maxSz runQueue _) worker = + bump :: Pool a -> (ProcessId, Recipient, Async a) -> Process (Pool a) + bump st@(Pool maxSz runQueue _) worker = let runLen = (length runQueue) - 1 runQ2 = deleteFromRunQueue worker runQueue slots = (maxSz - runLen) in fillSlots slots st { active = runQ2 } - fillSlots :: Int -> State a -> Process (State a) - fillSlots _ st'@(State _ _ []) = return st' - fillSlots 0 st' = return st' - fillSlots n st'@(State _ _ ((tr,tc):ts)) = + fillSlots :: Int -> Pool a -> Process (Pool a) + fillSlots _ st'@(Pool _ _ []) = return st' + fillSlots 0 st' = return st' + fillSlots n st'@(Pool _ _ ((tr,tc):ts)) = let ns = st' { accepted = ts } in acceptTask ns tr tc >>= fillSlots (n-1) @@ -128,4 +137,4 @@ findWorker key = find (\(pid,_,_) -> pid == key) deleteFromRunQueue :: (ProcessId, Recipient, Async a) -> [(ProcessId, Recipient, Async a)] -> [(ProcessId, Recipient, Async a)] -deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 82b526ea..3a943238 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -11,8 +11,9 @@ module Main where import Control.Concurrent.MVar import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Closure import Control.Distributed.Process.Node -import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform hiding (__remoteTable) import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test @@ -34,11 +35,77 @@ import Test.Framework.Providers.HUnit (testCase) import TestUtils import qualified Network.Transport as NT +import Control.Monad (void) + +-- utilities data GetState = GetState deriving (Typeable, Show, Eq) $(derive makeBinary ''GetState) +waitForExit :: MVar (Either (InitResult ()) TerminateReason) + -> Process (Maybe TerminateReason) +waitForExit exitReason = do + -- we *might* end up blocked here, so ensure the test doesn't jam up! + self <- getSelfPid + tref <- killAfter (within 10 Seconds) self "testcast timed out" + tr <- liftIO $ takeMVar exitReason + cancelTimer tref + case tr of + Right r -> return (Just r) + Left _ -> return Nothing + +server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) +server = mkServer Terminate + +mkServer :: UnhandledMessagePolicy + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +mkServer policy = + let s = statelessProcess { + dispatchers = [ + -- note: state is passed here, as a 'stateless' process is + -- in fact process definition whose state is () + + handleCastIf (input (\msg -> msg == "stop")) + (\_ _ -> stop TerminateNormal) + + , handleCall (\s' (m :: String) -> reply m s') + , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" + + , handleCast (\s' ("ping", pid :: ProcessId) -> + send pid "pong" >> continue s') + , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) + (\("timeout", Delay d) -> timeoutAfter_ d) + + , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) + ] + , unhandledMessagePolicy = policy + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + } + in do + exitReason <- liftIO $ newEmptyMVar + pid <- spawnLocal $ do + catch (start () (statelessInit Infinity) s >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) + return (pid, exitReason) + +startTestPool :: Int -> Process ProcessId +startTestPool s = spawnLocal $ do + _ <- runPool s + return () + +runPool :: Int -> Process (Either (InitResult (Pool String)) TerminateReason) +runPool s = + let s' = poolServer :: ProcessDefinition (Pool String) + in simplePool s s' + +sampleTask :: (TimeInterval, String) -> Process String +sampleTask (t, s) = sleep t >> return s + +$(remotable ['sampleTask]) + +-- test cases + testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -153,7 +220,7 @@ testCounterExceedsLimit pid result = do 7 <- getCount pid -- exceed the limit - 3 `times` (incCount pid >> return ()) + 3 `times` (void $ incCount pid) -- this time we should fail _ <- (incCount pid) @@ -224,10 +291,12 @@ mkServer policy = -- in spawnLocal $ start () init' b >> return () -- where init' :: () -> Process (InitResult String) -- init' = const (return $ InitOk () Infinity) +myRemoteTable :: RemoteTable +myRemoteTable = Main.__remoteTable initRemoteTable tests :: NT.Transport -> IO [Test] tests transport = do - localNode <- newLocalNode transport initRemoteTable + localNode <- newLocalNode transport myRemoteTable mpid <- newEmptyMVar _ <- forkProcess localNode $ launchMathServer >>= stash mpid pid <- takeMVar mpid From cdd1a350a43c042ef9c654609d5a53285a7f2bfd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 16:51:11 +0000 Subject: [PATCH 0851/2357] cosmetic (ish) --- tests/SimplePool.hs | 65 ++++++++++++++++++++---------------- tests/TestGenServer.hs | 75 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 31 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index abf672cb..8f460ab4 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -16,7 +16,7 @@ import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable - +import Control.Exception hiding (catch) import Data.Binary() import Data.List ( deleteBy @@ -24,33 +24,42 @@ import Data.List ) import Data.Typeable +import Prelude hiding (catch) + type PoolSize = Int type SimpleTask a = Closure (Process a) -data State a = State { +data Pool a = Pool { poolSize :: PoolSize , active :: [(ProcessId, Recipient, Async a)] , accepted :: [(Recipient, Closure (Process a))] } deriving (Typeable) --- | Start a worker pool with an upper bound on the # of concurrent workers. -simplePool :: forall a . (Serializable a) - => PoolSize - -> Process (Either (InitResult (State a)) TerminateReason) -simplePool sz = - let server = defaultProcess { +poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) +poolServer = + defaultProcess { dispatchers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] , infoHandlers = [ handleInfo taskComplete ] - } :: ProcessDefinition (State a) - in start sz init' server - where init' :: PoolSize -> Process (InitResult (State a)) - init' sz' = return $ InitOk (State sz' [] []) Infinity + } :: ProcessDefinition (Pool a) + +-- | Start a worker pool with an upper bound on the # of concurrent workers. +simplePool :: forall a . (Serializable a) + => PoolSize + -> ProcessDefinition (Pool a) + -> Process (Either (InitResult (Pool a)) TerminateReason) +simplePool sz server = + start sz init' server + `catch` (\(e :: SomeException) -> do + say $ "terminating with " ++ (show e) + liftIO $ throwIO e) + where init' :: PoolSize -> Process (InitResult (Pool a)) + init' sz' = return $ InitOk (Pool sz' [] []) Infinity --- enqueues the task in the pool and blocks +-- enqueues the task in the pool and blocks -- the caller until the task is complete executeTask :: Serializable a => ProcessId @@ -60,18 +69,18 @@ executeTask sid t = call sid t -- /call/ handler: accept a task and defer responding until "later" storeTask :: Serializable a - => State a + => Pool a -> Recipient -> Closure (Process a) - -> Process (ProcessReply (State a) ()) + -> Process (ProcessReply (Pool a) ()) storeTask s r c = acceptTask s r c >>= noReply_ acceptTask :: Serializable a - => State a + => Pool a -> Recipient -> Closure (Process a) - -> Process (State a) -acceptTask s@(State sz' runQueue taskQueue) from task' = + -> Process (Pool a) +acceptTask s@(Pool sz' runQueue taskQueue) from task' = let currentSz = length runQueue in case currentSz >= sz' of True -> do @@ -87,10 +96,10 @@ acceptTask s@(State sz' runQueue taskQueue) from task' = -- /info/ handler: a worker has exited, process the AsyncResult and send a reply -- to the waiting client (who is still stuck in 'call' awaiting a response). taskComplete :: forall a . Serializable a - => State a + => Pool a -> ProcessMonitorNotification - -> Process (ProcessAction (State a)) -taskComplete s@(State _ runQ _) + -> Process (ProcessAction (Pool a)) +taskComplete s@(Pool _ runQ _) (ProcessMonitorNotification _ pid _) = let worker = findWorker pid runQ in case worker of @@ -106,17 +115,17 @@ taskComplete s@(State _ runQ _) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - bump :: State a -> (ProcessId, Recipient, Async a) -> Process (State a) - bump st@(State maxSz runQueue _) worker = + bump :: Pool a -> (ProcessId, Recipient, Async a) -> Process (Pool a) + bump st@(Pool maxSz runQueue _) worker = let runLen = (length runQueue) - 1 runQ2 = deleteFromRunQueue worker runQueue slots = (maxSz - runLen) in fillSlots slots st { active = runQ2 } - fillSlots :: Int -> State a -> Process (State a) - fillSlots _ st'@(State _ _ []) = return st' - fillSlots 0 st' = return st' - fillSlots n st'@(State _ _ ((tr,tc):ts)) = + fillSlots :: Int -> Pool a -> Process (Pool a) + fillSlots _ st'@(Pool _ _ []) = return st' + fillSlots 0 st' = return st' + fillSlots n st'@(Pool _ _ ((tr,tc):ts)) = let ns = st' { accepted = ts } in acceptTask ns tr tc >>= fillSlots (n-1) @@ -128,4 +137,4 @@ findWorker key = find (\(pid,_,_) -> pid == key) deleteFromRunQueue :: (ProcessId, Recipient, Async a) -> [(ProcessId, Recipient, Async a)] -> [(ProcessId, Recipient, Async a)] -deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 82b526ea..3a943238 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -11,8 +11,9 @@ module Main where import Control.Concurrent.MVar import Control.Exception (SomeException) import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Closure import Control.Distributed.Process.Node -import Control.Distributed.Process.Platform +import Control.Distributed.Process.Platform hiding (__remoteTable) import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Test @@ -34,11 +35,77 @@ import Test.Framework.Providers.HUnit (testCase) import TestUtils import qualified Network.Transport as NT +import Control.Monad (void) + +-- utilities data GetState = GetState deriving (Typeable, Show, Eq) $(derive makeBinary ''GetState) +waitForExit :: MVar (Either (InitResult ()) TerminateReason) + -> Process (Maybe TerminateReason) +waitForExit exitReason = do + -- we *might* end up blocked here, so ensure the test doesn't jam up! + self <- getSelfPid + tref <- killAfter (within 10 Seconds) self "testcast timed out" + tr <- liftIO $ takeMVar exitReason + cancelTimer tref + case tr of + Right r -> return (Just r) + Left _ -> return Nothing + +server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) +server = mkServer Terminate + +mkServer :: UnhandledMessagePolicy + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +mkServer policy = + let s = statelessProcess { + dispatchers = [ + -- note: state is passed here, as a 'stateless' process is + -- in fact process definition whose state is () + + handleCastIf (input (\msg -> msg == "stop")) + (\_ _ -> stop TerminateNormal) + + , handleCall (\s' (m :: String) -> reply m s') + , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" + + , handleCast (\s' ("ping", pid :: ProcessId) -> + send pid "pong" >> continue s') + , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) + (\("timeout", Delay d) -> timeoutAfter_ d) + + , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) + ] + , unhandledMessagePolicy = policy + , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" + } + in do + exitReason <- liftIO $ newEmptyMVar + pid <- spawnLocal $ do + catch (start () (statelessInit Infinity) s >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) + return (pid, exitReason) + +startTestPool :: Int -> Process ProcessId +startTestPool s = spawnLocal $ do + _ <- runPool s + return () + +runPool :: Int -> Process (Either (InitResult (Pool String)) TerminateReason) +runPool s = + let s' = poolServer :: ProcessDefinition (Pool String) + in simplePool s s' + +sampleTask :: (TimeInterval, String) -> Process String +sampleTask (t, s) = sleep t >> return s + +$(remotable ['sampleTask]) + +-- test cases + testBasicCall :: TestResult (Maybe String) -> Process () testBasicCall result = do (pid, _) <- server @@ -153,7 +220,7 @@ testCounterExceedsLimit pid result = do 7 <- getCount pid -- exceed the limit - 3 `times` (incCount pid >> return ()) + 3 `times` (void $ incCount pid) -- this time we should fail _ <- (incCount pid) @@ -224,10 +291,12 @@ mkServer policy = -- in spawnLocal $ start () init' b >> return () -- where init' :: () -> Process (InitResult String) -- init' = const (return $ InitOk () Infinity) +myRemoteTable :: RemoteTable +myRemoteTable = Main.__remoteTable initRemoteTable tests :: NT.Transport -> IO [Test] tests transport = do - localNode <- newLocalNode transport initRemoteTable + localNode <- newLocalNode transport myRemoteTable mpid <- newEmptyMVar _ <- forkProcess localNode $ launchMathServer >>= stash mpid pid <- takeMVar mpid From 1a0d557e414a2be3c4da9233edf5c4114d212dbf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 16:51:37 +0000 Subject: [PATCH 0852/2357] basic test of simple pool (gen-proc deferred replies) --- tests/TestGenServer.hs | 74 +++++++++--------------------------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 3a943238..098f404d 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -195,6 +195,15 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False +-- SimplePool tests +testSimplePoolJobBlocksCaller :: TestResult (AsyncResult (Either String String)) + -> Process () +testSimplePoolJobBlocksCaller result = do + pid <- startTestPool 1 + -- we do a non-blocking test first + job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) + callAsync pid job >>= wait >>= \ar -> say (show ar) >> stash result ar + -- MathDemo tests testAdd :: ProcessId -> TestResult Double -> Process () @@ -232,65 +241,6 @@ testCounterExceedsLimit pid result = do ] stash result (r == DiedNormal) --- utilities - -waitForExit :: MVar (Either (InitResult ()) TerminateReason) - -> Process (Maybe TerminateReason) -waitForExit exitReason = do - -- we *might* end up blocked here, so ensure the test doesn't jam up! - self <- getSelfPid - tref <- killAfter (within 10 Seconds) self "testcast timed out" - tr <- liftIO $ takeMVar exitReason - cancelTimer tref - case tr of - Right r -> return (Just r) - Left _ -> return Nothing - -server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) -server = mkServer Terminate - -mkServer :: UnhandledMessagePolicy - -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) -mkServer policy = - let s = statelessProcess { - dispatchers = [ - -- note: state is passed here, as a 'stateless' process is - -- in fact process definition whose state is () - - handleCastIf (input (\msg -> msg == "stop")) - (\_ _ -> stop TerminateNormal) - - , handleCall (\s' (m :: String) -> reply m s') - , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - - , handleCast (\s' ("ping", pid :: ProcessId) -> - send pid "pong" >> continue s') - , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) - (\("timeout", Delay d) -> timeoutAfter_ d) - - , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) - ] - , unhandledMessagePolicy = policy - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" - } - in do - exitReason <- liftIO $ newEmptyMVar - pid <- spawnLocal $ do - catch (start () (statelessInit Infinity) s >>= stash exitReason) - (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) - return (pid, exitReason) - --- workerPool :: Process ProcessId --- workerPool = --- let b = defaultProcess { --- dispatchers = [ --- handleCast (\_ new -> continue new) --- , handleCall (\s GetState -> reply s s) --- ] --- } :: ProcessDefinition String --- in spawnLocal $ start () init' b >> return () --- where init' :: () -> Process (InitResult String) --- init' = const (return $ InitOk () Infinity) myRemoteTable :: RemoteTable myRemoteTable = Main.__remoteTable initRemoteTable @@ -343,6 +293,12 @@ tests transport = do (delayedAssertion "expected to get AsyncCancelled" localNode True testKillMidCall) ] + , testGroup "simple pool examples" [ + testCase "simple pool" + (delayedAssertion + "expected the server to return the task outcome" + localNode (AsyncDone (Right "foobar")) testSimplePoolJobBlocksCaller) + ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" (delayedAssertion From e7f41eb5c197dad87d69cdf68ef3788603683682 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 16:51:37 +0000 Subject: [PATCH 0853/2357] basic test of simple pool (gen-proc deferred replies) --- tests/TestGenServer.hs | 74 +++++++++--------------------------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 3a943238..098f404d 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -195,6 +195,15 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False +-- SimplePool tests +testSimplePoolJobBlocksCaller :: TestResult (AsyncResult (Either String String)) + -> Process () +testSimplePoolJobBlocksCaller result = do + pid <- startTestPool 1 + -- we do a non-blocking test first + job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) + callAsync pid job >>= wait >>= \ar -> say (show ar) >> stash result ar + -- MathDemo tests testAdd :: ProcessId -> TestResult Double -> Process () @@ -232,65 +241,6 @@ testCounterExceedsLimit pid result = do ] stash result (r == DiedNormal) --- utilities - -waitForExit :: MVar (Either (InitResult ()) TerminateReason) - -> Process (Maybe TerminateReason) -waitForExit exitReason = do - -- we *might* end up blocked here, so ensure the test doesn't jam up! - self <- getSelfPid - tref <- killAfter (within 10 Seconds) self "testcast timed out" - tr <- liftIO $ takeMVar exitReason - cancelTimer tref - case tr of - Right r -> return (Just r) - Left _ -> return Nothing - -server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) -server = mkServer Terminate - -mkServer :: UnhandledMessagePolicy - -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) -mkServer policy = - let s = statelessProcess { - dispatchers = [ - -- note: state is passed here, as a 'stateless' process is - -- in fact process definition whose state is () - - handleCastIf (input (\msg -> msg == "stop")) - (\_ _ -> stop TerminateNormal) - - , handleCall (\s' (m :: String) -> reply m s') - , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - - , handleCast (\s' ("ping", pid :: ProcessId) -> - send pid "pong" >> continue s') - , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) - (\("timeout", Delay d) -> timeoutAfter_ d) - - , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) - ] - , unhandledMessagePolicy = policy - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" - } - in do - exitReason <- liftIO $ newEmptyMVar - pid <- spawnLocal $ do - catch (start () (statelessInit Infinity) s >>= stash exitReason) - (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) - return (pid, exitReason) - --- workerPool :: Process ProcessId --- workerPool = --- let b = defaultProcess { --- dispatchers = [ --- handleCast (\_ new -> continue new) --- , handleCall (\s GetState -> reply s s) --- ] --- } :: ProcessDefinition String --- in spawnLocal $ start () init' b >> return () --- where init' :: () -> Process (InitResult String) --- init' = const (return $ InitOk () Infinity) myRemoteTable :: RemoteTable myRemoteTable = Main.__remoteTable initRemoteTable @@ -343,6 +293,12 @@ tests transport = do (delayedAssertion "expected to get AsyncCancelled" localNode True testKillMidCall) ] + , testGroup "simple pool examples" [ + testCase "simple pool" + (delayedAssertion + "expected the server to return the task outcome" + localNode (AsyncDone (Right "foobar")) testSimplePoolJobBlocksCaller) + ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" (delayedAssertion From 625659c9b10b8775a3198c42c18ac9f905d835ad Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 16:51:37 +0000 Subject: [PATCH 0854/2357] basic test of simple pool (gen-proc deferred replies) --- tests/TestGenServer.hs | 74 +++++++++--------------------------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 3a943238..098f404d 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -195,6 +195,15 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False +-- SimplePool tests +testSimplePoolJobBlocksCaller :: TestResult (AsyncResult (Either String String)) + -> Process () +testSimplePoolJobBlocksCaller result = do + pid <- startTestPool 1 + -- we do a non-blocking test first + job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) + callAsync pid job >>= wait >>= \ar -> say (show ar) >> stash result ar + -- MathDemo tests testAdd :: ProcessId -> TestResult Double -> Process () @@ -232,65 +241,6 @@ testCounterExceedsLimit pid result = do ] stash result (r == DiedNormal) --- utilities - -waitForExit :: MVar (Either (InitResult ()) TerminateReason) - -> Process (Maybe TerminateReason) -waitForExit exitReason = do - -- we *might* end up blocked here, so ensure the test doesn't jam up! - self <- getSelfPid - tref <- killAfter (within 10 Seconds) self "testcast timed out" - tr <- liftIO $ takeMVar exitReason - cancelTimer tref - case tr of - Right r -> return (Just r) - Left _ -> return Nothing - -server :: Process ((ProcessId, MVar (Either (InitResult ()) TerminateReason))) -server = mkServer Terminate - -mkServer :: UnhandledMessagePolicy - -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) -mkServer policy = - let s = statelessProcess { - dispatchers = [ - -- note: state is passed here, as a 'stateless' process is - -- in fact process definition whose state is () - - handleCastIf (input (\msg -> msg == "stop")) - (\_ _ -> stop TerminateNormal) - - , handleCall (\s' (m :: String) -> reply m s') - , handleCall_ (\(n :: Int) -> return (n * 2)) -- "stateless" - - , handleCast (\s' ("ping", pid :: ProcessId) -> - send pid "pong" >> continue s') - , handleCastIf_ (input (\(c :: String, _ :: Delay) -> c == "timeout")) - (\("timeout", Delay d) -> timeoutAfter_ d) - - , handleCast_ (\("hibernate", d :: TimeInterval) -> hibernate_ d) - ] - , unhandledMessagePolicy = policy - , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout" - } - in do - exitReason <- liftIO $ newEmptyMVar - pid <- spawnLocal $ do - catch (start () (statelessInit Infinity) s >>= stash exitReason) - (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) - return (pid, exitReason) - --- workerPool :: Process ProcessId --- workerPool = --- let b = defaultProcess { --- dispatchers = [ --- handleCast (\_ new -> continue new) --- , handleCall (\s GetState -> reply s s) --- ] --- } :: ProcessDefinition String --- in spawnLocal $ start () init' b >> return () --- where init' :: () -> Process (InitResult String) --- init' = const (return $ InitOk () Infinity) myRemoteTable :: RemoteTable myRemoteTable = Main.__remoteTable initRemoteTable @@ -343,6 +293,12 @@ tests transport = do (delayedAssertion "expected to get AsyncCancelled" localNode True testKillMidCall) ] + , testGroup "simple pool examples" [ + testCase "simple pool" + (delayedAssertion + "expected the server to return the task outcome" + localNode (AsyncDone (Right "foobar")) testSimplePoolJobBlocksCaller) + ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" (delayedAssertion From 9c12aae5cc828fd24fb029092f1047d09a4afef0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 17:25:36 +0000 Subject: [PATCH 0855/2357] test that simple rate limiting behaviour --- tests/TestGenServer.hs | 53 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 098f404d..564ae233 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -102,7 +102,14 @@ runPool s = sampleTask :: (TimeInterval, String) -> Process String sampleTask (t, s) = sleep t >> return s -$(remotable ['sampleTask]) +namedTask :: (String, String) -> Process String +namedTask (name, result) = do + self <- getSelfPid + register name self + () <- expect + return result + +$(remotable ['sampleTask, 'namedTask]) -- test cases @@ -202,7 +209,41 @@ testSimplePoolJobBlocksCaller result = do pid <- startTestPool 1 -- we do a non-blocking test first job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) - callAsync pid job >>= wait >>= \ar -> say (show ar) >> stash result ar + callAsync pid job >>= wait >>= stash result + +testJobQueueSizeLimiting :: + TestResult (Maybe (AsyncResult (Either String String)), + Maybe (AsyncResult (Either String String))) + -> Process () +testJobQueueSizeLimiting result = do + pid <- startTestPool 1 + job1 <- return $ ($(mkClosure 'namedTask) ("job1", "foo")) + job2 <- return $ ($(mkClosure 'namedTask) ("job2", "bar")) + h1 <- callAsync pid job1 :: Process (Async (Either String String)) + h2 <- callAsync pid job2 :: Process (Async (Either String String)) + + -- despite the fact that we tell job2 to proceed first, + -- the size limit (of 1) will ensure that only job1 can + -- proceed successfully! + nsend "job2" () + AsyncPending <- poll h2 + Nothing <- whereis "job2" + + -- we can get here *very* fast, we give the registration time to kick in + sleep $ milliSeconds 250 + j1p <- whereis "job1" + case j1p of + Nothing -> die $ "timing is out - job1 isn't registered yet" + Just p -> send p () + + -- once job1 completes, we *should* be able to proceed with job2 + -- but we allow a little time for things to catch up + sleep $ milliSeconds 250 + nsend "job2" () + + r2 <- waitTimeout (within 2 Seconds) h2 + r1 <- waitTimeout (within 2 Seconds) h1 + stash result (r1, r2) -- MathDemo tests @@ -294,10 +335,16 @@ tests transport = do localNode True testKillMidCall) ] , testGroup "simple pool examples" [ - testCase "simple pool" + testCase "each task execution blocks the caller" (delayedAssertion "expected the server to return the task outcome" localNode (AsyncDone (Right "foobar")) testSimplePoolJobBlocksCaller) + , testCase "only 'max' tasks can proceed at any time" + (delayedAssertion + "expected the server to block the second job until the first was released" + localNode + (Just (AsyncDone (Right "foo")), + Just (AsyncDone (Right "bar"))) testJobQueueSizeLimiting) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 7b3890f7744fe8532023d7082b9f7f0c56c6b11b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 17:25:36 +0000 Subject: [PATCH 0856/2357] test that simple rate limiting behaviour --- tests/TestGenServer.hs | 53 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 098f404d..564ae233 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -102,7 +102,14 @@ runPool s = sampleTask :: (TimeInterval, String) -> Process String sampleTask (t, s) = sleep t >> return s -$(remotable ['sampleTask]) +namedTask :: (String, String) -> Process String +namedTask (name, result) = do + self <- getSelfPid + register name self + () <- expect + return result + +$(remotable ['sampleTask, 'namedTask]) -- test cases @@ -202,7 +209,41 @@ testSimplePoolJobBlocksCaller result = do pid <- startTestPool 1 -- we do a non-blocking test first job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) - callAsync pid job >>= wait >>= \ar -> say (show ar) >> stash result ar + callAsync pid job >>= wait >>= stash result + +testJobQueueSizeLimiting :: + TestResult (Maybe (AsyncResult (Either String String)), + Maybe (AsyncResult (Either String String))) + -> Process () +testJobQueueSizeLimiting result = do + pid <- startTestPool 1 + job1 <- return $ ($(mkClosure 'namedTask) ("job1", "foo")) + job2 <- return $ ($(mkClosure 'namedTask) ("job2", "bar")) + h1 <- callAsync pid job1 :: Process (Async (Either String String)) + h2 <- callAsync pid job2 :: Process (Async (Either String String)) + + -- despite the fact that we tell job2 to proceed first, + -- the size limit (of 1) will ensure that only job1 can + -- proceed successfully! + nsend "job2" () + AsyncPending <- poll h2 + Nothing <- whereis "job2" + + -- we can get here *very* fast, we give the registration time to kick in + sleep $ milliSeconds 250 + j1p <- whereis "job1" + case j1p of + Nothing -> die $ "timing is out - job1 isn't registered yet" + Just p -> send p () + + -- once job1 completes, we *should* be able to proceed with job2 + -- but we allow a little time for things to catch up + sleep $ milliSeconds 250 + nsend "job2" () + + r2 <- waitTimeout (within 2 Seconds) h2 + r1 <- waitTimeout (within 2 Seconds) h1 + stash result (r1, r2) -- MathDemo tests @@ -294,10 +335,16 @@ tests transport = do localNode True testKillMidCall) ] , testGroup "simple pool examples" [ - testCase "simple pool" + testCase "each task execution blocks the caller" (delayedAssertion "expected the server to return the task outcome" localNode (AsyncDone (Right "foobar")) testSimplePoolJobBlocksCaller) + , testCase "only 'max' tasks can proceed at any time" + (delayedAssertion + "expected the server to block the second job until the first was released" + localNode + (Just (AsyncDone (Right "foo")), + Just (AsyncDone (Right "bar"))) testJobQueueSizeLimiting) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 4fe83129edca2b8fe2508bd79730ee6466f5b752 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 17:25:36 +0000 Subject: [PATCH 0857/2357] test that simple rate limiting behaviour --- tests/TestGenServer.hs | 53 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 098f404d..564ae233 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -102,7 +102,14 @@ runPool s = sampleTask :: (TimeInterval, String) -> Process String sampleTask (t, s) = sleep t >> return s -$(remotable ['sampleTask]) +namedTask :: (String, String) -> Process String +namedTask (name, result) = do + self <- getSelfPid + register name self + () <- expect + return result + +$(remotable ['sampleTask, 'namedTask]) -- test cases @@ -202,7 +209,41 @@ testSimplePoolJobBlocksCaller result = do pid <- startTestPool 1 -- we do a non-blocking test first job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) - callAsync pid job >>= wait >>= \ar -> say (show ar) >> stash result ar + callAsync pid job >>= wait >>= stash result + +testJobQueueSizeLimiting :: + TestResult (Maybe (AsyncResult (Either String String)), + Maybe (AsyncResult (Either String String))) + -> Process () +testJobQueueSizeLimiting result = do + pid <- startTestPool 1 + job1 <- return $ ($(mkClosure 'namedTask) ("job1", "foo")) + job2 <- return $ ($(mkClosure 'namedTask) ("job2", "bar")) + h1 <- callAsync pid job1 :: Process (Async (Either String String)) + h2 <- callAsync pid job2 :: Process (Async (Either String String)) + + -- despite the fact that we tell job2 to proceed first, + -- the size limit (of 1) will ensure that only job1 can + -- proceed successfully! + nsend "job2" () + AsyncPending <- poll h2 + Nothing <- whereis "job2" + + -- we can get here *very* fast, we give the registration time to kick in + sleep $ milliSeconds 250 + j1p <- whereis "job1" + case j1p of + Nothing -> die $ "timing is out - job1 isn't registered yet" + Just p -> send p () + + -- once job1 completes, we *should* be able to proceed with job2 + -- but we allow a little time for things to catch up + sleep $ milliSeconds 250 + nsend "job2" () + + r2 <- waitTimeout (within 2 Seconds) h2 + r1 <- waitTimeout (within 2 Seconds) h1 + stash result (r1, r2) -- MathDemo tests @@ -294,10 +335,16 @@ tests transport = do localNode True testKillMidCall) ] , testGroup "simple pool examples" [ - testCase "simple pool" + testCase "each task execution blocks the caller" (delayedAssertion "expected the server to return the task outcome" localNode (AsyncDone (Right "foobar")) testSimplePoolJobBlocksCaller) + , testCase "only 'max' tasks can proceed at any time" + (delayedAssertion + "expected the server to block the second job until the first was released" + localNode + (Just (AsyncDone (Right "foo")), + Just (AsyncDone (Right "bar"))) testJobQueueSizeLimiting) ] , testGroup "math server examples" [ testCase "error (Left) returned from x / 0" From 8eba9be6c86ee59730ca5d5248672a55634ebce4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 19:16:22 +0000 Subject: [PATCH 0858/2357] cosmetic --- tests/SimplePool.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 8f460ab4..399705d6 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -138,3 +138,4 @@ deleteFromRunQueue :: (ProcessId, Recipient, Async a) -> [(ProcessId, Recipient, Async a)] -> [(ProcessId, Recipient, Async a)] deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ + From bba4333ef6fc101740960644fb2ab996c69750d9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 19:16:22 +0000 Subject: [PATCH 0859/2357] cosmetic --- tests/SimplePool.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 8f460ab4..399705d6 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -138,3 +138,4 @@ deleteFromRunQueue :: (ProcessId, Recipient, Async a) -> [(ProcessId, Recipient, Async a)] -> [(ProcessId, Recipient, Async a)] deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ + From 026f9eb9cd9cf40c3bab64093905fdf03eb5e49e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 19:16:22 +0000 Subject: [PATCH 0860/2357] cosmetic --- tests/SimplePool.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 8f460ab4..399705d6 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -138,3 +138,4 @@ deleteFromRunQueue :: (ProcessId, Recipient, Async a) -> [(ProcessId, Recipient, Async a)] -> [(ProcessId, Recipient, Async a)] deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ + From 3afb5a9940bdb986885a7fac12c790e1ad03897f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 21:39:27 +0000 Subject: [PATCH 0861/2357] cosmetic --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c8ecf1a7..e7d21ed5 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -263,8 +263,8 @@ type CastHandler s = s -> Process () -- type and/or value of the input message or both. data Condition s m = Condition (s -> m -> Bool) -- ^ predicated on the process state /and/ the message - | State (s -> Bool) -- ^ predicated on the process state only - | Input (m -> Bool) -- ^ predicated on the input message only + | State (s -> Bool) -- ^ predicated on the process state only + | Input (m -> Bool) -- ^ predicated on the input message only -- | An expression used to initialise a process with its state. type InitHandler a s = a -> Process (InitResult s) From 4d998b4f713684da12b422707ca53a7d5f8e681c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 23 Jan 2013 21:39:27 +0000 Subject: [PATCH 0862/2357] cosmetic --- src/Control/Distributed/Process/Platform/GenProcess.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c8ecf1a7..e7d21ed5 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -263,8 +263,8 @@ type CastHandler s = s -> Process () -- type and/or value of the input message or both. data Condition s m = Condition (s -> m -> Bool) -- ^ predicated on the process state /and/ the message - | State (s -> Bool) -- ^ predicated on the process state only - | Input (m -> Bool) -- ^ predicated on the input message only + | State (s -> Bool) -- ^ predicated on the process state only + | Input (m -> Bool) -- ^ predicated on the input message only -- | An expression used to initialise a process with its state. type InitHandler a s = a -> Process (InitResult s) From 0240cbffd9507744ec10860b6ed33142915e52bb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:33:15 +0000 Subject: [PATCH 0863/2357] tidy up the cabal file --- distributed-process-platform.cabal | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ab8a1438..ca9f6d07 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -47,6 +47,7 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, Control.Distributed.Process.Platform.Async.Types test-suite TimerTests @@ -99,13 +100,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestPrimitives.hs @@ -133,8 +129,7 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestAsync.hs @@ -161,11 +156,24 @@ test-suite GenServerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Async + Control.Distributed.Process.Platform.Async.AsyncChan + Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.GenProcess, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Test, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, + Control.Distributed.Process.Platform.Async.Types, + TestUtils, + MathsDemo, + Counter, + SimplePool extensions: CPP main-is: TestGenServer.hs From 90c46d88a791505538921be049addb4a112f18cb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:33:15 +0000 Subject: [PATCH 0864/2357] tidy up the cabal file --- distributed-process-platform.cabal | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ab8a1438..ca9f6d07 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -47,6 +47,7 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, Control.Distributed.Process.Platform.Async.Types test-suite TimerTests @@ -99,13 +100,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestPrimitives.hs @@ -133,8 +129,7 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestAsync.hs @@ -161,11 +156,24 @@ test-suite GenServerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Async + Control.Distributed.Process.Platform.Async.AsyncChan + Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.GenProcess, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Test, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, + Control.Distributed.Process.Platform.Async.Types, + TestUtils, + MathsDemo, + Counter, + SimplePool extensions: CPP main-is: TestGenServer.hs From 74236237c8e3328bf0ae3462f745c0f15b8fff8a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:33:15 +0000 Subject: [PATCH 0865/2357] tidy up the cabal file --- distributed-process-platform.cabal | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ab8a1438..ca9f6d07 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -47,6 +47,7 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, Control.Distributed.Process.Platform.Async.Types test-suite TimerTests @@ -99,13 +100,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestPrimitives.hs @@ -133,8 +129,7 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestAsync.hs @@ -161,11 +156,24 @@ test-suite GenServerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Async + Control.Distributed.Process.Platform.Async.AsyncChan + Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.GenProcess, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Test, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, + Control.Distributed.Process.Platform.Async.Types, + TestUtils, + MathsDemo, + Counter, + SimplePool extensions: CPP main-is: TestGenServer.hs From 7a41f228c5bd472e066c1d9da7911dfddecf59c0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:33:15 +0000 Subject: [PATCH 0866/2357] tidy up the cabal file --- distributed-process-platform.cabal | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ab8a1438..ca9f6d07 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -47,6 +47,7 @@ library other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, Control.Distributed.Process.Platform.Async.Types test-suite TimerTests @@ -99,13 +100,8 @@ test-suite PrimitivesTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Time, TestUtils, - Control.Distributed.Process.Platform.Test, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestPrimitives.hs @@ -133,8 +129,7 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.Async.Types + TestGenServer extensions: CPP main-is: TestAsync.hs @@ -161,11 +156,24 @@ test-suite GenServerTests tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform, + Control.Distributed.Process.Platform.Async + Control.Distributed.Process.Platform.Async.AsyncChan + Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Call, + Control.Distributed.Process.Platform.GenProcess, + Control.Distributed.Process.Platform.GenServer, + Control.Distributed.Process.Platform.Test, + Control.Distributed.Process.Platform.Time, + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.Internal.Primitives, + Control.Distributed.Process.Platform.Internal.Types, + Control.Distributed.Process.Platform.Internal.Common, + Control.Distributed.Process.Platform.Async.Types, + TestUtils, + MathsDemo, + Counter, + SimplePool extensions: CPP main-is: TestGenServer.hs From 68b6c0eb615d185c02925447ecda2b0506d42bc1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:13 +0000 Subject: [PATCH 0867/2357] Refactor GenProcess, add support for Shutdown and 'exitHandlers' Rename: s/dispatchers/apiHandlers/g. Add generic handling of `Shutdown` messages. Add `handleExit` and `exitHandlers` to the API and test. --- .../Process/Platform/GenProcess.hs | 172 +++++++++++------- tests/SimplePool.hs | 2 +- tests/TestGenServer.hs | 61 ++++++- 3 files changed, 170 insertions(+), 65 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index e7d21ed5..adcbc250 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -44,7 +44,7 @@ -- signal or similar asynchronous exception is thrown in (or to) the process -- itself. -- --- The other handlers are split into two groups: /dispatchers/ and /infoHandlers/. +-- The other handlers are split into two groups: /apiHandlers/ and /infoHandlers/. -- The former contains handlers for the 'cast' and 'call' protocols, whilst the -- latter contains handlers that deal with input messages which are not sent -- via these API calls (i.e., messages sent using bare 'send' or signals put @@ -59,9 +59,9 @@ -- the client. On the other hand, a 'call' interaction is a kind of /rpc/ -- where the client sends a message and waits for a reply. -- --- The expressions given /dispatchers/ have to conform to the /cast|call/ +-- The expressions given to @apiHandlers@ have to conform to the /cast|call/ -- protocol. The details of this are, however, hidden from the user. A set --- of API functions for creating /dispatchers/ are given instead, which +-- of API functions for creating @apiHandlers@ are given instead, which -- take expressions (i.e., a function or lambda expression) and create the -- appropriate @Dispatcher@ for handling the cast (or call). -- @@ -104,7 +104,7 @@ -- -- @ -- statelessProcess { --- dispatchers = [ +-- apiHandlers = [ -- handleCall_ (\\(n :: Int) -> return (n * 2)) -- , handleCastIf_ (\\(c :: String, _ :: Delay) -> c == \"timeout\") -- (\\(\"timeout\", Delay d) -> timeoutAfter_ d) @@ -145,6 +145,7 @@ module Control.Distributed.Process.Platform.GenProcess , ProcessDefinition(..) -- * Client interaction with the process , start + , shutdown , defaultProcess , statelessProcess , statelessInit @@ -181,6 +182,7 @@ module Control.Distributed.Process.Platform.GenProcess , handleCastIf , handleInfo , handleDispatch + , handleExit -- * Stateless handlers , action , handleCall_ @@ -194,7 +196,9 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Async hiding (check) import Control.Distributed.Process.Platform.Internal.Types - ( TerminateReason(..)) + ( TerminateReason(..) + , Shutdown(..) + ) import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time @@ -288,8 +292,18 @@ data Dispatcher s = } -- | Provides dispatch for any input, returns 'Nothing' for unhandled messages. -data InfoDispatcher s = InfoDispatcher { - dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) +data DeferredDispatcher s = DeferredDispatcher { + dispatchInfo :: s + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) + } + +-- | Provides dispatch for any exit signal - returns 'Nothing' for unhandled exceptions +data ExitSignalDispatcher s = ExitSignalDispatcher { + dispatchExit :: s + -> ProcessId + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) } class MessageMatcher d where @@ -310,8 +324,9 @@ data UnhandledMessagePolicy = -- | Stores the functions that determine runtime behaviour in response to -- incoming messages and a policy for responding to unhandled messages. data ProcessDefinition s = ProcessDefinition { - dispatchers :: [Dispatcher s] -- ^ functions that handle call/cast messages - , infoHandlers :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages + apiHandlers :: [Dispatcher s] -- ^ functions that handle call/cast messages + , infoHandlers :: [DeferredDispatcher s] -- ^ functions that handle non call/cast messages + , exitHandlers :: [ExitSignalDispatcher s] -- ^ functions that handle exit signals , timeoutHandler :: TimeoutHandler s -- ^ a function that handles timeouts , terminateHandler :: TerminateHandler s -- ^ a function that is run just before the process exits , unhandledMessagePolicy :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages @@ -335,13 +350,23 @@ start :: a start args init behave = do ir <- init args case ir of - InitOk s d -> loop behave s d >>= return . Right + InitOk s d -> recvLoop behave s d >>= return . Right f@(InitFail _) -> return $ Left f +-- | Send a signal instructing the process to terminate. The /receive loop/ which +-- manages the process mailbox will prioritise @Shutdown@ signals higher than +-- any other incoming messages, but the server might be busy (i.e., still in the +-- process of excuting a handler) at the time of sending however, so the caller +-- should not make any assumptions about the timeliness with which the shutdown +-- signal will be handled. +shutdown :: ProcessId -> Process () +shutdown pid = cast pid Shutdown + defaultProcess :: ProcessDefinition s defaultProcess = ProcessDefinition { - dispatchers = [] + apiHandlers = [] , infoHandlers = [] + , exitHandlers = [] , timeoutHandler = \s _ -> continue s , terminateHandler = \_ _ -> return () , unhandledMessagePolicy = Terminate @@ -352,8 +377,9 @@ defaultProcess = ProcessDefinition { -- same as calling @continue ()@ and the terminate handler is a no-op. statelessProcess :: ProcessDefinition () statelessProcess = ProcessDefinition { - dispatchers = [] + apiHandlers = [] , infoHandlers = [] + , exitHandlers = [] , timeoutHandler = \s _ -> continue s , terminateHandler = \_ _ -> return () , unhandledMessagePolicy = Terminate @@ -585,7 +611,7 @@ replyTo :: (Serializable m) => Recipient -> m -> Process () replyTo client msg = sendTo client (CallResponse msg) -------------------------------------------------------------------------------- --- Wrapping handler expressions in Dispatcher and InfoDispatcher -- +-- Wrapping handler expressions in Dispatcher and DeferredDispatcher -- -------------------------------------------------------------------------------- -- | Constructs a 'call' handler from a function in the 'Process' monad. @@ -601,7 +627,7 @@ handleCall_ = handleCallIf_ $ input (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. This variant ignores the state argument present in 'handleCall' and --- 'handleCallIf' and is therefore useful in a stateless server. Messages are +-- 'handleCallIf' and is therefore useful in a stateless server. Messges are -- only dispatched to the handler if the supplied condition evaluates to @True@ -- -- See 'handleCall' @@ -787,8 +813,8 @@ handleDispatchIf cond handler = DispatchIf { -- 'Process' monad. handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) - -> InfoDispatcher s -handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } + -> DeferredDispatcher s +handleInfo h = DeferredDispatcher { dispatchInfo = doHandleInfo h } where doHandleInfo :: forall s2 a2. (Serializable a2) => (s2 -> a2 -> Process (ProcessAction s2)) @@ -797,6 +823,20 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) +-- | Creates an /exit handler/ scoped to the execution of any and all the +-- registered call, cast and info handlers for the process. +handleExit :: forall s a. (Serializable a) + => (s -> ProcessId -> a -> Process (ProcessAction s)) + -> ExitSignalDispatcher s +handleExit h = ExitSignalDispatcher { dispatchExit = doHandleExit h } + where + doHandleExit :: (s -> ProcessId -> a -> Process (ProcessAction s)) + -> s + -> ProcessId + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) + doHandleExit h' s p msg = maybeHandleMessage msg (h' s p) + -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -839,60 +879,65 @@ decode (CastMessage a) = a -- Internal Process Implementation -- -------------------------------------------------------------------------------- -applyPolicy :: s - -> UnhandledMessagePolicy +recvLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason +recvLoop pDef pState recvDelay = + let p = unhandledMessagePolicy pDef + handleTimeout = timeoutHandler pDef + handleStop = terminateHandler pDef + shutdown' = matchMessage p pState shutdownHandler + matchers = map (matchMessage p pState) (apiHandlers pDef) + ms' = (shutdown':matchers) ++ matchAux p pState (infoHandlers pDef) + in do + ac <- catchesExit (processReceive ms' handleTimeout pState recvDelay) + (map (\d' -> (dispatchExit d') pState) (exitHandlers pDef)) + case ac of + (ProcessContinue s') -> recvLoop pDef s' recvDelay + (ProcessTimeout t' s') -> recvLoop pDef s' (Delay t') + (ProcessHibernate d' s') -> block d' >> recvLoop pDef s' recvDelay + (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) + +shutdownHandler :: Dispatcher s +shutdownHandler = handleCast (\_ Shutdown -> stop $ TerminateShutdown) + +block :: TimeInterval -> Process () +block i = liftIO $ threadDelay (asTimeout i) + +applyPolicy :: UnhandledMessagePolicy + -> s -> AbstractMessage -> Process (ProcessAction s) -applyPolicy s p m = +applyPolicy p s m = case p of Terminate -> stop $ TerminateOther "UnhandledInput" DeadLetter pid -> forward m pid >> continue s Drop -> continue s -loop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason -loop pDef pState recvDelay = - let p = unhandledMessagePolicy pDef - handleTimeout = timeoutHandler pDef - handleStop = terminateHandler pDef - ms = map (matchMessage p pState) (dispatchers pDef) - ms' = ms ++ addInfoAux p pState (infoHandlers pDef) - in do - ac <- processReceive ms' handleTimeout pState recvDelay - case ac of - (ProcessContinue s') -> loop pDef s' recvDelay - (ProcessTimeout t' s') -> loop pDef s' (Delay t') - (ProcessHibernate d' s') -> block d' >> loop pDef s' recvDelay - (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) - where - block :: TimeInterval -> Process () - block i = liftIO $ threadDelay (asTimeout i) - - addInfoAux :: UnhandledMessagePolicy - -> s - -> [InfoDispatcher s] - -> [Match (ProcessAction s)] - addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] - - infoHandler :: UnhandledMessagePolicy - -> s - -> [InfoDispatcher s] - -> AbstractMessage - -> Process (ProcessAction s) - infoHandler pol st [] msg = applyPolicy st pol msg - infoHandler pol st (d:ds :: [InfoDispatcher s]) msg - | length ds > 0 = let dh = dispatchInfo d in do - -- NB: we *do not* want to terminate/dead-letter messages until - -- we've exhausted all the possible info handlers - m <- dh st msg - case m of - Nothing -> infoHandler pol st ds msg - Just act -> return act - -- but here we *do* let the policy kick in - | otherwise = let dh = dispatchInfo d in do - m <- dh st msg - case m of - Nothing -> applyPolicy st pol msg - Just act -> return act +matchAux :: UnhandledMessagePolicy + -> s + -> [DeferredDispatcher s] + -> [Match (ProcessAction s)] +matchAux p ps ds = [matchAny (auxHandler (applyPolicy p ps) ps ds)] + +auxHandler :: (AbstractMessage -> Process (ProcessAction s)) + -> s + -> [DeferredDispatcher s] + -> AbstractMessage + -> Process (ProcessAction s) +auxHandler policy _ [] msg = policy msg +auxHandler policy st (d:ds :: [DeferredDispatcher s]) msg + | length ds > 0 = let dh = dispatchInfo d in do + -- NB: we *do not* want to terminate/dead-letter messages until + -- we've exhausted all the possible info handlers + m <- dh st msg + case m of + Nothing -> auxHandler policy st ds msg + Just act -> return act + -- but here we *do* let the policy kick in + | otherwise = let dh = dispatchInfo d in do + m <- dh st msg + case m of + Nothing -> policy msg + Just act -> return act processReceive :: [Match (ProcessAction s)] -> TimeoutHandler s -> s @@ -910,3 +955,4 @@ processReceive ms handleTimeout st d = do case d' of Infinity -> receiveWait matches >>= return . Just Delay t' -> receiveTimeout (asTimeout t') matches + diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 399705d6..d6cb0071 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -38,7 +38,7 @@ data Pool a = Pool { poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) poolServer = defaultProcess { - dispatchers = [ + apiHandlers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] , infoHandlers = [ diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 564ae233..774e8de3 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -62,7 +62,7 @@ mkServer :: UnhandledMessagePolicy -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) mkServer policy = let s = statelessProcess { - dispatchers = [ + apiHandlers = [ -- note: state is passed here, as a 'stateless' process is -- in fact process definition whose state is () @@ -89,6 +89,30 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) +explodingServer :: ProcessId + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +explodingServer pid = + let srv = statelessProcess { + apiHandlers = [ + handleCall_ (\(s :: String) -> + (die s) :: Process String) + , handleCast (\_ (i :: Int) -> + getSelfPid >>= \p -> die (p, i)) + ] + , exitHandlers = [ + handleExit (\s _ (m :: String) -> send pid (m :: String) >> + continue s) + , handleExit (\s _ m@((_ :: ProcessId), + (_ :: Int)) -> send pid m >> continue s) + ] + } + in do + exitReason <- liftIO $ newEmptyMVar + spid <- spawnLocal $ do + catch (start () (statelessInit Infinity) srv >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) + return (spid, exitReason) + startTestPool :: Int -> Process ProcessId startTestPool s = spawnLocal $ do _ <- runPool s @@ -202,7 +226,36 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False +testSimpleErrorHandling :: TestResult (Maybe TerminateReason) -> Process () +testSimpleErrorHandling result = do + self <- getSelfPid + (pid, exitReason) <- explodingServer self + + -- this should be *altered* because of the exit handler + Nothing <- callTimeout pid "foobar" (within 1 Seconds) :: Process (Maybe String) + "foobar" <- expect + + shutdown pid + waitForExit exitReason >>= stash result + +testAlternativeErrorHandling :: TestResult (Maybe TerminateReason) -> Process () +testAlternativeErrorHandling result = do + self <- getSelfPid + (pid, exitReason) <- explodingServer self + + -- this should be ignored/altered because of the second exit handler + cast pid (42 :: Int) + (Just True) <- receiveTimeout (after 2 Seconds) [ + matchIf (\((p :: ProcessId), (i :: Int)) -> p == pid && i == 42) + (\_ -> return True) + ] + + shutdown pid + waitForExit exitReason >>= stash result + + -- SimplePool tests + testSimplePoolJobBlocksCaller :: TestResult (AsyncResult (Either String String)) -> Process () testSimplePoolJobBlocksCaller result = do @@ -333,6 +386,12 @@ tests transport = do , testCase "long running call cancellation" (delayedAssertion "expected to get AsyncCancelled" localNode True testKillMidCall) + , testCase "simple exit handling" + (delayedAssertion "expected handler to catch exception and continue" + localNode (Just TerminateShutdown) testSimpleErrorHandling) + , testCase "alternative exit handlers" + (delayedAssertion "expected handler to catch exception and continue" + localNode (Just TerminateShutdown) testAlternativeErrorHandling) ] , testGroup "simple pool examples" [ testCase "each task execution blocks the caller" From 851b6fac532e1c4388e782899a69d4f4900f1eec Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:13 +0000 Subject: [PATCH 0868/2357] Refactor GenProcess, add support for Shutdown and 'exitHandlers' Rename: s/dispatchers/apiHandlers/g. Add generic handling of `Shutdown` messages. Add `handleExit` and `exitHandlers` to the API and test. --- .../Process/Platform/GenProcess.hs | 172 +++++++++++------- .../Process/Platform/Internal/Types.hs | 1 + 2 files changed, 110 insertions(+), 63 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index e7d21ed5..adcbc250 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -44,7 +44,7 @@ -- signal or similar asynchronous exception is thrown in (or to) the process -- itself. -- --- The other handlers are split into two groups: /dispatchers/ and /infoHandlers/. +-- The other handlers are split into two groups: /apiHandlers/ and /infoHandlers/. -- The former contains handlers for the 'cast' and 'call' protocols, whilst the -- latter contains handlers that deal with input messages which are not sent -- via these API calls (i.e., messages sent using bare 'send' or signals put @@ -59,9 +59,9 @@ -- the client. On the other hand, a 'call' interaction is a kind of /rpc/ -- where the client sends a message and waits for a reply. -- --- The expressions given /dispatchers/ have to conform to the /cast|call/ +-- The expressions given to @apiHandlers@ have to conform to the /cast|call/ -- protocol. The details of this are, however, hidden from the user. A set --- of API functions for creating /dispatchers/ are given instead, which +-- of API functions for creating @apiHandlers@ are given instead, which -- take expressions (i.e., a function or lambda expression) and create the -- appropriate @Dispatcher@ for handling the cast (or call). -- @@ -104,7 +104,7 @@ -- -- @ -- statelessProcess { --- dispatchers = [ +-- apiHandlers = [ -- handleCall_ (\\(n :: Int) -> return (n * 2)) -- , handleCastIf_ (\\(c :: String, _ :: Delay) -> c == \"timeout\") -- (\\(\"timeout\", Delay d) -> timeoutAfter_ d) @@ -145,6 +145,7 @@ module Control.Distributed.Process.Platform.GenProcess , ProcessDefinition(..) -- * Client interaction with the process , start + , shutdown , defaultProcess , statelessProcess , statelessInit @@ -181,6 +182,7 @@ module Control.Distributed.Process.Platform.GenProcess , handleCastIf , handleInfo , handleDispatch + , handleExit -- * Stateless handlers , action , handleCall_ @@ -194,7 +196,9 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Async hiding (check) import Control.Distributed.Process.Platform.Internal.Types - ( TerminateReason(..)) + ( TerminateReason(..) + , Shutdown(..) + ) import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time @@ -288,8 +292,18 @@ data Dispatcher s = } -- | Provides dispatch for any input, returns 'Nothing' for unhandled messages. -data InfoDispatcher s = InfoDispatcher { - dispatchInfo :: s -> AbstractMessage -> Process (Maybe (ProcessAction s)) +data DeferredDispatcher s = DeferredDispatcher { + dispatchInfo :: s + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) + } + +-- | Provides dispatch for any exit signal - returns 'Nothing' for unhandled exceptions +data ExitSignalDispatcher s = ExitSignalDispatcher { + dispatchExit :: s + -> ProcessId + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) } class MessageMatcher d where @@ -310,8 +324,9 @@ data UnhandledMessagePolicy = -- | Stores the functions that determine runtime behaviour in response to -- incoming messages and a policy for responding to unhandled messages. data ProcessDefinition s = ProcessDefinition { - dispatchers :: [Dispatcher s] -- ^ functions that handle call/cast messages - , infoHandlers :: [InfoDispatcher s] -- ^ functions that handle non call/cast messages + apiHandlers :: [Dispatcher s] -- ^ functions that handle call/cast messages + , infoHandlers :: [DeferredDispatcher s] -- ^ functions that handle non call/cast messages + , exitHandlers :: [ExitSignalDispatcher s] -- ^ functions that handle exit signals , timeoutHandler :: TimeoutHandler s -- ^ a function that handles timeouts , terminateHandler :: TerminateHandler s -- ^ a function that is run just before the process exits , unhandledMessagePolicy :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages @@ -335,13 +350,23 @@ start :: a start args init behave = do ir <- init args case ir of - InitOk s d -> loop behave s d >>= return . Right + InitOk s d -> recvLoop behave s d >>= return . Right f@(InitFail _) -> return $ Left f +-- | Send a signal instructing the process to terminate. The /receive loop/ which +-- manages the process mailbox will prioritise @Shutdown@ signals higher than +-- any other incoming messages, but the server might be busy (i.e., still in the +-- process of excuting a handler) at the time of sending however, so the caller +-- should not make any assumptions about the timeliness with which the shutdown +-- signal will be handled. +shutdown :: ProcessId -> Process () +shutdown pid = cast pid Shutdown + defaultProcess :: ProcessDefinition s defaultProcess = ProcessDefinition { - dispatchers = [] + apiHandlers = [] , infoHandlers = [] + , exitHandlers = [] , timeoutHandler = \s _ -> continue s , terminateHandler = \_ _ -> return () , unhandledMessagePolicy = Terminate @@ -352,8 +377,9 @@ defaultProcess = ProcessDefinition { -- same as calling @continue ()@ and the terminate handler is a no-op. statelessProcess :: ProcessDefinition () statelessProcess = ProcessDefinition { - dispatchers = [] + apiHandlers = [] , infoHandlers = [] + , exitHandlers = [] , timeoutHandler = \s _ -> continue s , terminateHandler = \_ _ -> return () , unhandledMessagePolicy = Terminate @@ -585,7 +611,7 @@ replyTo :: (Serializable m) => Recipient -> m -> Process () replyTo client msg = sendTo client (CallResponse msg) -------------------------------------------------------------------------------- --- Wrapping handler expressions in Dispatcher and InfoDispatcher -- +-- Wrapping handler expressions in Dispatcher and DeferredDispatcher -- -------------------------------------------------------------------------------- -- | Constructs a 'call' handler from a function in the 'Process' monad. @@ -601,7 +627,7 @@ handleCall_ = handleCallIf_ $ input (const True) -- | Constructs a 'call' handler from an ordinary function in the 'Process' -- monad. This variant ignores the state argument present in 'handleCall' and --- 'handleCallIf' and is therefore useful in a stateless server. Messages are +-- 'handleCallIf' and is therefore useful in a stateless server. Messges are -- only dispatched to the handler if the supplied condition evaluates to @True@ -- -- See 'handleCall' @@ -787,8 +813,8 @@ handleDispatchIf cond handler = DispatchIf { -- 'Process' monad. handleInfo :: forall s a. (Serializable a) => (s -> a -> Process (ProcessAction s)) - -> InfoDispatcher s -handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } + -> DeferredDispatcher s +handleInfo h = DeferredDispatcher { dispatchInfo = doHandleInfo h } where doHandleInfo :: forall s2 a2. (Serializable a2) => (s2 -> a2 -> Process (ProcessAction s2)) @@ -797,6 +823,20 @@ handleInfo h = InfoDispatcher { dispatchInfo = doHandleInfo h } -> Process (Maybe (ProcessAction s2)) doHandleInfo h' s msg = maybeHandleMessage msg (h' s) +-- | Creates an /exit handler/ scoped to the execution of any and all the +-- registered call, cast and info handlers for the process. +handleExit :: forall s a. (Serializable a) + => (s -> ProcessId -> a -> Process (ProcessAction s)) + -> ExitSignalDispatcher s +handleExit h = ExitSignalDispatcher { dispatchExit = doHandleExit h } + where + doHandleExit :: (s -> ProcessId -> a -> Process (ProcessAction s)) + -> s + -> ProcessId + -> AbstractMessage + -> Process (Maybe (ProcessAction s)) + doHandleExit h' s p msg = maybeHandleMessage msg (h' s p) + -- handling 'reply-to' in the main process loop is awkward at best, -- so we handle it here instead and return the 'action' to the loop mkReply :: (Serializable b) @@ -839,60 +879,65 @@ decode (CastMessage a) = a -- Internal Process Implementation -- -------------------------------------------------------------------------------- -applyPolicy :: s - -> UnhandledMessagePolicy +recvLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason +recvLoop pDef pState recvDelay = + let p = unhandledMessagePolicy pDef + handleTimeout = timeoutHandler pDef + handleStop = terminateHandler pDef + shutdown' = matchMessage p pState shutdownHandler + matchers = map (matchMessage p pState) (apiHandlers pDef) + ms' = (shutdown':matchers) ++ matchAux p pState (infoHandlers pDef) + in do + ac <- catchesExit (processReceive ms' handleTimeout pState recvDelay) + (map (\d' -> (dispatchExit d') pState) (exitHandlers pDef)) + case ac of + (ProcessContinue s') -> recvLoop pDef s' recvDelay + (ProcessTimeout t' s') -> recvLoop pDef s' (Delay t') + (ProcessHibernate d' s') -> block d' >> recvLoop pDef s' recvDelay + (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) + +shutdownHandler :: Dispatcher s +shutdownHandler = handleCast (\_ Shutdown -> stop $ TerminateShutdown) + +block :: TimeInterval -> Process () +block i = liftIO $ threadDelay (asTimeout i) + +applyPolicy :: UnhandledMessagePolicy + -> s -> AbstractMessage -> Process (ProcessAction s) -applyPolicy s p m = +applyPolicy p s m = case p of Terminate -> stop $ TerminateOther "UnhandledInput" DeadLetter pid -> forward m pid >> continue s Drop -> continue s -loop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason -loop pDef pState recvDelay = - let p = unhandledMessagePolicy pDef - handleTimeout = timeoutHandler pDef - handleStop = terminateHandler pDef - ms = map (matchMessage p pState) (dispatchers pDef) - ms' = ms ++ addInfoAux p pState (infoHandlers pDef) - in do - ac <- processReceive ms' handleTimeout pState recvDelay - case ac of - (ProcessContinue s') -> loop pDef s' recvDelay - (ProcessTimeout t' s') -> loop pDef s' (Delay t') - (ProcessHibernate d' s') -> block d' >> loop pDef s' recvDelay - (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) - where - block :: TimeInterval -> Process () - block i = liftIO $ threadDelay (asTimeout i) - - addInfoAux :: UnhandledMessagePolicy - -> s - -> [InfoDispatcher s] - -> [Match (ProcessAction s)] - addInfoAux p ps ds = [matchAny (infoHandler p ps ds)] - - infoHandler :: UnhandledMessagePolicy - -> s - -> [InfoDispatcher s] - -> AbstractMessage - -> Process (ProcessAction s) - infoHandler pol st [] msg = applyPolicy st pol msg - infoHandler pol st (d:ds :: [InfoDispatcher s]) msg - | length ds > 0 = let dh = dispatchInfo d in do - -- NB: we *do not* want to terminate/dead-letter messages until - -- we've exhausted all the possible info handlers - m <- dh st msg - case m of - Nothing -> infoHandler pol st ds msg - Just act -> return act - -- but here we *do* let the policy kick in - | otherwise = let dh = dispatchInfo d in do - m <- dh st msg - case m of - Nothing -> applyPolicy st pol msg - Just act -> return act +matchAux :: UnhandledMessagePolicy + -> s + -> [DeferredDispatcher s] + -> [Match (ProcessAction s)] +matchAux p ps ds = [matchAny (auxHandler (applyPolicy p ps) ps ds)] + +auxHandler :: (AbstractMessage -> Process (ProcessAction s)) + -> s + -> [DeferredDispatcher s] + -> AbstractMessage + -> Process (ProcessAction s) +auxHandler policy _ [] msg = policy msg +auxHandler policy st (d:ds :: [DeferredDispatcher s]) msg + | length ds > 0 = let dh = dispatchInfo d in do + -- NB: we *do not* want to terminate/dead-letter messages until + -- we've exhausted all the possible info handlers + m <- dh st msg + case m of + Nothing -> auxHandler policy st ds msg + Just act -> return act + -- but here we *do* let the policy kick in + | otherwise = let dh = dispatchInfo d in do + m <- dh st msg + case m of + Nothing -> policy msg + Just act -> return act processReceive :: [Match (ProcessAction s)] -> TimeoutHandler s -> s @@ -910,3 +955,4 @@ processReceive ms handleTimeout st d = do case d' of Infinity -> receiveWait matches >>= return . Just Delay t' -> receiveTimeout (asTimeout t') matches + diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index 6ca7a626..287180d2 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -11,6 +11,7 @@ module Control.Distributed.Process.Platform.Internal.Types , RegisterSelf(..) , CancelWait(..) , Channel + , Shutdown(..) , TerminateReason(..) ) where From c89b2dcfbbafbefa8f76df17531cfad4e37f58a8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:13 +0000 Subject: [PATCH 0869/2357] Refactor GenProcess, add support for Shutdown and 'exitHandlers' Rename: s/dispatchers/apiHandlers/g. Add generic handling of `Shutdown` messages. Add `handleExit` and `exitHandlers` to the API and test. --- tests/SimplePool.hs | 2 +- tests/TestGenServer.hs | 61 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 399705d6..d6cb0071 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -38,7 +38,7 @@ data Pool a = Pool { poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) poolServer = defaultProcess { - dispatchers = [ + apiHandlers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] , infoHandlers = [ diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 564ae233..774e8de3 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -62,7 +62,7 @@ mkServer :: UnhandledMessagePolicy -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) mkServer policy = let s = statelessProcess { - dispatchers = [ + apiHandlers = [ -- note: state is passed here, as a 'stateless' process is -- in fact process definition whose state is () @@ -89,6 +89,30 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) +explodingServer :: ProcessId + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +explodingServer pid = + let srv = statelessProcess { + apiHandlers = [ + handleCall_ (\(s :: String) -> + (die s) :: Process String) + , handleCast (\_ (i :: Int) -> + getSelfPid >>= \p -> die (p, i)) + ] + , exitHandlers = [ + handleExit (\s _ (m :: String) -> send pid (m :: String) >> + continue s) + , handleExit (\s _ m@((_ :: ProcessId), + (_ :: Int)) -> send pid m >> continue s) + ] + } + in do + exitReason <- liftIO $ newEmptyMVar + spid <- spawnLocal $ do + catch (start () (statelessInit Infinity) srv >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) + return (spid, exitReason) + startTestPool :: Int -> Process ProcessId startTestPool s = spawnLocal $ do _ <- runPool s @@ -202,7 +226,36 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False +testSimpleErrorHandling :: TestResult (Maybe TerminateReason) -> Process () +testSimpleErrorHandling result = do + self <- getSelfPid + (pid, exitReason) <- explodingServer self + + -- this should be *altered* because of the exit handler + Nothing <- callTimeout pid "foobar" (within 1 Seconds) :: Process (Maybe String) + "foobar" <- expect + + shutdown pid + waitForExit exitReason >>= stash result + +testAlternativeErrorHandling :: TestResult (Maybe TerminateReason) -> Process () +testAlternativeErrorHandling result = do + self <- getSelfPid + (pid, exitReason) <- explodingServer self + + -- this should be ignored/altered because of the second exit handler + cast pid (42 :: Int) + (Just True) <- receiveTimeout (after 2 Seconds) [ + matchIf (\((p :: ProcessId), (i :: Int)) -> p == pid && i == 42) + (\_ -> return True) + ] + + shutdown pid + waitForExit exitReason >>= stash result + + -- SimplePool tests + testSimplePoolJobBlocksCaller :: TestResult (AsyncResult (Either String String)) -> Process () testSimplePoolJobBlocksCaller result = do @@ -333,6 +386,12 @@ tests transport = do , testCase "long running call cancellation" (delayedAssertion "expected to get AsyncCancelled" localNode True testKillMidCall) + , testCase "simple exit handling" + (delayedAssertion "expected handler to catch exception and continue" + localNode (Just TerminateShutdown) testSimpleErrorHandling) + , testCase "alternative exit handlers" + (delayedAssertion "expected handler to catch exception and continue" + localNode (Just TerminateShutdown) testAlternativeErrorHandling) ] , testGroup "simple pool examples" [ testCase "each task execution blocks the caller" From 6146496177a39a94a4ceabcc7e2d67d6dd61f468 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:13 +0000 Subject: [PATCH 0870/2357] Refactor GenProcess, add support for Shutdown and 'exitHandlers' Rename: s/dispatchers/apiHandlers/g. Add generic handling of `Shutdown` messages. Add `handleExit` and `exitHandlers` to the API and test. --- tests/Counter.hs | 2 +- tests/MathsDemo.hs | 2 +- tests/SimplePool.hs | 2 +- tests/TestGenServer.hs | 61 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/tests/Counter.hs b/tests/Counter.hs index 89690101..d37b6e5b 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -77,7 +77,7 @@ startCounter startCount = serverDefinition :: ProcessDefinition State serverDefinition = defaultProcess { - dispatchers = [ + apiHandlers = [ handleCallIf (condition (\count Increment -> count >= 10))-- invariant (\_ (_ :: Increment) -> do haltNoReply_ (TerminateOther "Count > 10")) diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index 6ca7b555..e6d7ef6c 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -43,7 +43,7 @@ divide sid x y = call sid (Divide x y ) launchMathServer :: Process ProcessId launchMathServer = let server = statelessProcess { - dispatchers = [ + apiHandlers = [ handleCall_ (\(Add x y) -> return (x + y)) , handleCallIf_ (input (\(Divide _ y) -> y /= 0)) handleDivide , handleCall_ (\(Divide _ _) -> divByZero) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 399705d6..d6cb0071 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -38,7 +38,7 @@ data Pool a = Pool { poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) poolServer = defaultProcess { - dispatchers = [ + apiHandlers = [ handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) ] , infoHandlers = [ diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 564ae233..774e8de3 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -62,7 +62,7 @@ mkServer :: UnhandledMessagePolicy -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) mkServer policy = let s = statelessProcess { - dispatchers = [ + apiHandlers = [ -- note: state is passed here, as a 'stateless' process is -- in fact process definition whose state is () @@ -89,6 +89,30 @@ mkServer policy = (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) return (pid, exitReason) +explodingServer :: ProcessId + -> Process (ProcessId, MVar (Either (InitResult ()) TerminateReason)) +explodingServer pid = + let srv = statelessProcess { + apiHandlers = [ + handleCall_ (\(s :: String) -> + (die s) :: Process String) + , handleCast (\_ (i :: Int) -> + getSelfPid >>= \p -> die (p, i)) + ] + , exitHandlers = [ + handleExit (\s _ (m :: String) -> send pid (m :: String) >> + continue s) + , handleExit (\s _ m@((_ :: ProcessId), + (_ :: Int)) -> send pid m >> continue s) + ] + } + in do + exitReason <- liftIO $ newEmptyMVar + spid <- spawnLocal $ do + catch (start () (statelessInit Infinity) srv >>= stash exitReason) + (\(e :: SomeException) -> stash exitReason $ Right (TerminateOther (show e))) + return (spid, exitReason) + startTestPool :: Int -> Process ProcessId startTestPool s = spawnLocal $ do _ <- runPool s @@ -202,7 +226,36 @@ testKillMidCall result = do unpack res sid AsyncCancelled = kill sid "stop" >> stash res True unpack res sid _ = kill sid "stop" >> stash res False +testSimpleErrorHandling :: TestResult (Maybe TerminateReason) -> Process () +testSimpleErrorHandling result = do + self <- getSelfPid + (pid, exitReason) <- explodingServer self + + -- this should be *altered* because of the exit handler + Nothing <- callTimeout pid "foobar" (within 1 Seconds) :: Process (Maybe String) + "foobar" <- expect + + shutdown pid + waitForExit exitReason >>= stash result + +testAlternativeErrorHandling :: TestResult (Maybe TerminateReason) -> Process () +testAlternativeErrorHandling result = do + self <- getSelfPid + (pid, exitReason) <- explodingServer self + + -- this should be ignored/altered because of the second exit handler + cast pid (42 :: Int) + (Just True) <- receiveTimeout (after 2 Seconds) [ + matchIf (\((p :: ProcessId), (i :: Int)) -> p == pid && i == 42) + (\_ -> return True) + ] + + shutdown pid + waitForExit exitReason >>= stash result + + -- SimplePool tests + testSimplePoolJobBlocksCaller :: TestResult (AsyncResult (Either String String)) -> Process () testSimplePoolJobBlocksCaller result = do @@ -333,6 +386,12 @@ tests transport = do , testCase "long running call cancellation" (delayedAssertion "expected to get AsyncCancelled" localNode True testKillMidCall) + , testCase "simple exit handling" + (delayedAssertion "expected handler to catch exception and continue" + localNode (Just TerminateShutdown) testSimpleErrorHandling) + , testCase "alternative exit handlers" + (delayedAssertion "expected handler to catch exception and continue" + localNode (Just TerminateShutdown) testAlternativeErrorHandling) ] , testGroup "simple pool examples" [ testCase "each task execution blocks the caller" From e533037f4a8669171877632bdbbafc46435b7b22 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:33 +0000 Subject: [PATCH 0871/2357] ignote hpc artefacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7a995ff5..fafda718 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ cabal-dev *.lksh? .dist-buildwrapper .project +.hpc +*.tix From f881e3261212f4cf9b79fb3a9455030b6e6b6a1d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:33 +0000 Subject: [PATCH 0872/2357] ignote hpc artefacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7a995ff5..fafda718 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ cabal-dev *.lksh? .dist-buildwrapper .project +.hpc +*.tix From 8d42fa9c40db5d4a428d4b2cbd47818b08db807e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:33 +0000 Subject: [PATCH 0873/2357] ignote hpc artefacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7a995ff5..fafda718 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ cabal-dev *.lksh? .dist-buildwrapper .project +.hpc +*.tix From 6a34899aff7f23cb743629fe5e358d47522dfa33 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 25 Jan 2013 19:37:33 +0000 Subject: [PATCH 0874/2357] ignote hpc artefacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7a995ff5..fafda718 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ cabal-dev *.lksh? .dist-buildwrapper .project +.hpc +*.tix From b98b0fa8d8dcdae663bd4957432e93ea34322f0e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 01:42:32 +0000 Subject: [PATCH 0875/2357] GenProcess documentation --- distributed-process-platform.cabal | 5 +- .../Process/Platform/GenProcess.hs | 88 ++++++++++++++----- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ca9f6d07..e570e449 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -12,7 +12,10 @@ Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform Bug-Reports: mailto:watson.timothy@gmail.com synopsis: The Cloud Haskell Application Platform -description: TBD +description: Modelled after Erlang's OTP, this framework provides similar + facilities for Cloud Haskell, grouping essential practices + into a set of modules and standards designed to help you build + concurrent, distributed applications with relative ease. category: Control tested-with: GHC == 7.4.2 data-dir: "" diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index adcbc250..05b0ac4b 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -16,7 +16,8 @@ -- This module provides a high(er) level API for building complex 'Process' -- implementations by abstracting out the management of the process' mailbox, -- reply/response handling, timeouts, process hiberation, error handling --- and shutdown/stop procedures. +-- and shutdown/stop procedures. It is modelled along similar lines to OTP's +-- gen_server API - . -- -- [API Overview] -- @@ -117,15 +118,46 @@ -- -- Error handling appears in several contexts and process definitions can -- hook into these with relative ease. Only process failures as a result of --- asynchronous exceptions are supported by the API, so /error/ handling --- code is the responsibility of the programmer. +-- asynchronous exceptions are supported by the API, which provides several +-- scopes for error handling. -- --- The API provides several scopes for error handling. There is obviously --- nothing to stop the programmer from catching exceptions in various --- handlers, and this is fine, as is using the 'catchExit' API from --- 'Control.Distributed.Process'. +-- Catching exceptions inside handler functions is no different to ordinary +-- exception handling in monadic code. -- +-- @ +-- handleCall (\\x y -> +-- catch (hereBeDragons x y) +-- (\\(e :: SmaugTheTerribleException) -> +-- return (Left (show e)))) +-- @ -- +-- The caveats mentioned in "Control.Distributed.Process.Platform" about +-- exit signal handling obviously apply here as well. +-- +-- [Structured Exit Signal Handling] +-- +-- Because "Control.Distributed.Process.ProcessExitException" is a ubiquitous +-- /signalling mechanism/ in Cloud Haskell, it is treated unlike other +-- asynchronous exceptions. The 'ProcessDefinition' 'exitHandlers' field +-- accepts a list of handlers that, for a specific exit reason, can decide +-- how the process should respond. If none of these handlers matches the +-- type of @reason@ then the process will exit with @DiedException why@. In +-- addition, a default /exit handler/ is installed for exit signals where the +-- @reason == Shutdown@, because this is an /exit signal/ used explicitly and +-- extensively throughout the platform. The default behaviour is to gracefully +-- shut down the process, calling the @terminateHandler@ as usual, before +-- stopping with @TerminateShutdown@ given as the final outcome. +-- +-- /Example: How to annoy your supervisor and end up force-killed:/ +-- +-- > handleExit (\state from (sigExit :: Shutdown) -> continue s) +-- +-- That code is, of course, very silly. Under some circumstances, handling +-- exit signals is perfectly legitimate. Handling of /other/ forms of +-- asynchronous exception is not supported. +-- +-- If any asynchronous exception goes unhandled, the process will immediately +-- exit without running the @terminateHandler@. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess @@ -333,7 +365,7 @@ data ProcessDefinition s = ProcessDefinition { } -------------------------------------------------------------------------------- --- Client facing API functions -- +-- Client API -- -------------------------------------------------------------------------------- -- TODO: automatic registration @@ -358,7 +390,10 @@ start args init behave = do -- any other incoming messages, but the server might be busy (i.e., still in the -- process of excuting a handler) at the time of sending however, so the caller -- should not make any assumptions about the timeliness with which the shutdown --- signal will be handled. +-- signal will be handled. If responsiveness is important, a better approach +-- might be to send an /exit signal/ with 'Shutdown' as the reason. An exit +-- signal will interrupt any operation currently underway and force the running +-- process to clean up and terminate. shutdown :: ProcessId -> Process () shutdown pid = cast pid Shutdown @@ -426,7 +461,7 @@ tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] -- | Make a synchronous call, but timeout and return @Nothing@ if the reply -- is not received within the specified time interval. -- --- If the 'AsyncResult' for the call indicates a failure (or cancellation) then +-- If the result of the call is a failure (or the call was cancelled) then -- the calling process will exit, with the 'AsyncResult' given as the reason. -- callTimeout :: forall a b . (Serializable a, Serializable b) @@ -441,7 +476,7 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- call is made /out of band/ and an async handle is returned immediately. This -- can be passed to functions in the /Async/ API in order to obtain the result. -- --- see "Control.Distributed.Process.Platform.Async" +-- See "Control.Distributed.Process.Platform.Async" -- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Async b) @@ -452,14 +487,8 @@ callAsync = callAsyncUsing async -- async implementations, by e.g., using an async channel instead of the default -- STM based handle. -- --- See 'callAsync' --- -- See "Control.Distributed.Process.Platform.Async" -- --- See "Control.Distributed.Process.Platform.Async.AsyncChan" --- --- See "Control.Distributed.Process.Platform.Async.AsyncSTM" --- callAsyncUsing :: forall a b . (Serializable a, Serializable b) => (Process b -> Process (Async b)) -> ProcessId -> a -> Process (Async b) @@ -500,7 +529,7 @@ callAsyncUsing asyncStart sid msg = do -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent --- (e.g., dead) process will not generate any errors. +-- (e.g., dead) server process will not generate an error. cast :: forall a . (Serializable a) => ProcessId -> a -> Process () cast sid msg = send sid (CastMessage msg) @@ -550,7 +579,8 @@ noReply = return . NoReply noReply_ :: forall s r . (Serializable r) => s -> Process (ProcessReply s r) noReply_ s = continue s >>= noReply --- | Halt a call handler without regard for the expected return type. +-- | Halt process execution during a call handler, without paying any attention +-- to the expected return type. haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) haltNoReply_ r = stop r >>= noReply @@ -591,7 +621,9 @@ hibernate d s = return $ ProcessHibernate d s hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) hibernate_ d = return . ProcessHibernate d --- | Instructs the process to cease, giving the supplied reason for termination. +-- | Instructs the process to terminate, giving the supplied reason. If a valid +-- 'terminateHandler' is installed, it will be called with the 'TerminateReason' +-- returned from this call, along with the process state. stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r @@ -824,7 +856,7 @@ handleInfo h = DeferredDispatcher { dispatchInfo = doHandleInfo h } doHandleInfo h' s msg = maybeHandleMessage msg (h' s) -- | Creates an /exit handler/ scoped to the execution of any and all the --- registered call, cast and info handlers for the process. +-- registered call, cast and info handlers for the process. handleExit :: forall s a. (Serializable a) => (s -> ProcessId -> a -> Process (ProcessAction s)) -> ExitSignalDispatcher s @@ -886,19 +918,25 @@ recvLoop pDef pState recvDelay = handleStop = terminateHandler pDef shutdown' = matchMessage p pState shutdownHandler matchers = map (matchMessage p pState) (apiHandlers pDef) + ex' = (exitHandlers pDef) ++ [trapExit] ms' = (shutdown':matchers) ++ matchAux p pState (infoHandlers pDef) in do ac <- catchesExit (processReceive ms' handleTimeout pState recvDelay) - (map (\d' -> (dispatchExit d') pState) (exitHandlers pDef)) + (map (\d' -> (dispatchExit d') pState) ex') case ac of (ProcessContinue s') -> recvLoop pDef s' recvDelay (ProcessTimeout t' s') -> recvLoop pDef s' (Delay t') (ProcessHibernate d' s') -> block d' >> recvLoop pDef s' recvDelay (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) +-- an explicit 'cast' giving 'Shutdown' will stop the server gracefully shutdownHandler :: Dispatcher s shutdownHandler = handleCast (\_ Shutdown -> stop $ TerminateShutdown) +-- @(ProcessExitException from Shutdown)@ will stop the server gracefully +trapExit :: ExitSignalDispatcher s +trapExit = handleExit (\_ (_ :: ProcessId) Shutdown -> stop $ TerminateShutdown) + block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) @@ -940,8 +978,10 @@ auxHandler policy st (d:ds :: [DeferredDispatcher s]) msg Just act -> return act processReceive :: [Match (ProcessAction s)] - -> TimeoutHandler s -> s - -> Delay -> Process (ProcessAction s) + -> TimeoutHandler s + -> s + -> Delay + -> Process (ProcessAction s) processReceive ms handleTimeout st d = do next <- recv ms d case next of From a02cec82c74f20aa21b37d2ba3b122ec307af8d6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 01:42:32 +0000 Subject: [PATCH 0876/2357] GenProcess documentation --- distributed-process-platform.cabal | 5 +- .../Process/Platform/GenProcess.hs | 88 ++++++++++++++----- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ca9f6d07..e570e449 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -12,7 +12,10 @@ Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform Bug-Reports: mailto:watson.timothy@gmail.com synopsis: The Cloud Haskell Application Platform -description: TBD +description: Modelled after Erlang's OTP, this framework provides similar + facilities for Cloud Haskell, grouping essential practices + into a set of modules and standards designed to help you build + concurrent, distributed applications with relative ease. category: Control tested-with: GHC == 7.4.2 data-dir: "" diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index adcbc250..05b0ac4b 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -16,7 +16,8 @@ -- This module provides a high(er) level API for building complex 'Process' -- implementations by abstracting out the management of the process' mailbox, -- reply/response handling, timeouts, process hiberation, error handling --- and shutdown/stop procedures. +-- and shutdown/stop procedures. It is modelled along similar lines to OTP's +-- gen_server API - . -- -- [API Overview] -- @@ -117,15 +118,46 @@ -- -- Error handling appears in several contexts and process definitions can -- hook into these with relative ease. Only process failures as a result of --- asynchronous exceptions are supported by the API, so /error/ handling --- code is the responsibility of the programmer. +-- asynchronous exceptions are supported by the API, which provides several +-- scopes for error handling. -- --- The API provides several scopes for error handling. There is obviously --- nothing to stop the programmer from catching exceptions in various --- handlers, and this is fine, as is using the 'catchExit' API from --- 'Control.Distributed.Process'. +-- Catching exceptions inside handler functions is no different to ordinary +-- exception handling in monadic code. -- +-- @ +-- handleCall (\\x y -> +-- catch (hereBeDragons x y) +-- (\\(e :: SmaugTheTerribleException) -> +-- return (Left (show e)))) +-- @ -- +-- The caveats mentioned in "Control.Distributed.Process.Platform" about +-- exit signal handling obviously apply here as well. +-- +-- [Structured Exit Signal Handling] +-- +-- Because "Control.Distributed.Process.ProcessExitException" is a ubiquitous +-- /signalling mechanism/ in Cloud Haskell, it is treated unlike other +-- asynchronous exceptions. The 'ProcessDefinition' 'exitHandlers' field +-- accepts a list of handlers that, for a specific exit reason, can decide +-- how the process should respond. If none of these handlers matches the +-- type of @reason@ then the process will exit with @DiedException why@. In +-- addition, a default /exit handler/ is installed for exit signals where the +-- @reason == Shutdown@, because this is an /exit signal/ used explicitly and +-- extensively throughout the platform. The default behaviour is to gracefully +-- shut down the process, calling the @terminateHandler@ as usual, before +-- stopping with @TerminateShutdown@ given as the final outcome. +-- +-- /Example: How to annoy your supervisor and end up force-killed:/ +-- +-- > handleExit (\state from (sigExit :: Shutdown) -> continue s) +-- +-- That code is, of course, very silly. Under some circumstances, handling +-- exit signals is perfectly legitimate. Handling of /other/ forms of +-- asynchronous exception is not supported. +-- +-- If any asynchronous exception goes unhandled, the process will immediately +-- exit without running the @terminateHandler@. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess @@ -333,7 +365,7 @@ data ProcessDefinition s = ProcessDefinition { } -------------------------------------------------------------------------------- --- Client facing API functions -- +-- Client API -- -------------------------------------------------------------------------------- -- TODO: automatic registration @@ -358,7 +390,10 @@ start args init behave = do -- any other incoming messages, but the server might be busy (i.e., still in the -- process of excuting a handler) at the time of sending however, so the caller -- should not make any assumptions about the timeliness with which the shutdown --- signal will be handled. +-- signal will be handled. If responsiveness is important, a better approach +-- might be to send an /exit signal/ with 'Shutdown' as the reason. An exit +-- signal will interrupt any operation currently underway and force the running +-- process to clean up and terminate. shutdown :: ProcessId -> Process () shutdown pid = cast pid Shutdown @@ -426,7 +461,7 @@ tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] -- | Make a synchronous call, but timeout and return @Nothing@ if the reply -- is not received within the specified time interval. -- --- If the 'AsyncResult' for the call indicates a failure (or cancellation) then +-- If the result of the call is a failure (or the call was cancelled) then -- the calling process will exit, with the 'AsyncResult' given as the reason. -- callTimeout :: forall a b . (Serializable a, Serializable b) @@ -441,7 +476,7 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- call is made /out of band/ and an async handle is returned immediately. This -- can be passed to functions in the /Async/ API in order to obtain the result. -- --- see "Control.Distributed.Process.Platform.Async" +-- See "Control.Distributed.Process.Platform.Async" -- callAsync :: forall a b . (Serializable a, Serializable b) => ProcessId -> a -> Process (Async b) @@ -452,14 +487,8 @@ callAsync = callAsyncUsing async -- async implementations, by e.g., using an async channel instead of the default -- STM based handle. -- --- See 'callAsync' --- -- See "Control.Distributed.Process.Platform.Async" -- --- See "Control.Distributed.Process.Platform.Async.AsyncChan" --- --- See "Control.Distributed.Process.Platform.Async.AsyncSTM" --- callAsyncUsing :: forall a b . (Serializable a, Serializable b) => (Process b -> Process (Async b)) -> ProcessId -> a -> Process (Async b) @@ -500,7 +529,7 @@ callAsyncUsing asyncStart sid msg = do -- | Sends a /cast/ message to the server identified by 'ServerId'. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent --- (e.g., dead) process will not generate any errors. +-- (e.g., dead) server process will not generate an error. cast :: forall a . (Serializable a) => ProcessId -> a -> Process () cast sid msg = send sid (CastMessage msg) @@ -550,7 +579,8 @@ noReply = return . NoReply noReply_ :: forall s r . (Serializable r) => s -> Process (ProcessReply s r) noReply_ s = continue s >>= noReply --- | Halt a call handler without regard for the expected return type. +-- | Halt process execution during a call handler, without paying any attention +-- to the expected return type. haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) haltNoReply_ r = stop r >>= noReply @@ -591,7 +621,9 @@ hibernate d s = return $ ProcessHibernate d s hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) hibernate_ d = return . ProcessHibernate d --- | Instructs the process to cease, giving the supplied reason for termination. +-- | Instructs the process to terminate, giving the supplied reason. If a valid +-- 'terminateHandler' is installed, it will be called with the 'TerminateReason' +-- returned from this call, along with the process state. stop :: TerminateReason -> Process (ProcessAction s) stop r = return $ ProcessStop r @@ -824,7 +856,7 @@ handleInfo h = DeferredDispatcher { dispatchInfo = doHandleInfo h } doHandleInfo h' s msg = maybeHandleMessage msg (h' s) -- | Creates an /exit handler/ scoped to the execution of any and all the --- registered call, cast and info handlers for the process. +-- registered call, cast and info handlers for the process. handleExit :: forall s a. (Serializable a) => (s -> ProcessId -> a -> Process (ProcessAction s)) -> ExitSignalDispatcher s @@ -886,19 +918,25 @@ recvLoop pDef pState recvDelay = handleStop = terminateHandler pDef shutdown' = matchMessage p pState shutdownHandler matchers = map (matchMessage p pState) (apiHandlers pDef) + ex' = (exitHandlers pDef) ++ [trapExit] ms' = (shutdown':matchers) ++ matchAux p pState (infoHandlers pDef) in do ac <- catchesExit (processReceive ms' handleTimeout pState recvDelay) - (map (\d' -> (dispatchExit d') pState) (exitHandlers pDef)) + (map (\d' -> (dispatchExit d') pState) ex') case ac of (ProcessContinue s') -> recvLoop pDef s' recvDelay (ProcessTimeout t' s') -> recvLoop pDef s' (Delay t') (ProcessHibernate d' s') -> block d' >> recvLoop pDef s' recvDelay (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) +-- an explicit 'cast' giving 'Shutdown' will stop the server gracefully shutdownHandler :: Dispatcher s shutdownHandler = handleCast (\_ Shutdown -> stop $ TerminateShutdown) +-- @(ProcessExitException from Shutdown)@ will stop the server gracefully +trapExit :: ExitSignalDispatcher s +trapExit = handleExit (\_ (_ :: ProcessId) Shutdown -> stop $ TerminateShutdown) + block :: TimeInterval -> Process () block i = liftIO $ threadDelay (asTimeout i) @@ -940,8 +978,10 @@ auxHandler policy st (d:ds :: [DeferredDispatcher s]) msg Just act -> return act processReceive :: [Match (ProcessAction s)] - -> TimeoutHandler s -> s - -> Delay -> Process (ProcessAction s) + -> TimeoutHandler s + -> s + -> Delay + -> Process (ProcessAction s) processReceive ms handleTimeout st d = do next <- recv ms d case next of From fd536332faf74dc3d61b5aecfd9f2fbef4874b34 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 01:42:32 +0000 Subject: [PATCH 0877/2357] GenProcess documentation --- distributed-process-platform.cabal | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ca9f6d07..e570e449 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -12,7 +12,10 @@ Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform Bug-Reports: mailto:watson.timothy@gmail.com synopsis: The Cloud Haskell Application Platform -description: TBD +description: Modelled after Erlang's OTP, this framework provides similar + facilities for Cloud Haskell, grouping essential practices + into a set of modules and standards designed to help you build + concurrent, distributed applications with relative ease. category: Control tested-with: GHC == 7.4.2 data-dir: "" From a05c616f6d393c71f2ab0c36e73096210ce76f50 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 01:42:32 +0000 Subject: [PATCH 0878/2357] GenProcess documentation --- distributed-process-platform.cabal | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index ca9f6d07..e570e449 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -12,7 +12,10 @@ Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform Bug-Reports: mailto:watson.timothy@gmail.com synopsis: The Cloud Haskell Application Platform -description: TBD +description: Modelled after Erlang's OTP, this framework provides similar + facilities for Cloud Haskell, grouping essential practices + into a set of modules and standards designed to help you build + concurrent, distributed applications with relative ease. category: Control tested-with: GHC == 7.4.2 data-dir: "" From 04e0304edc659bb4a90e3651457eff9e18ce8238 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 11:42:59 +0000 Subject: [PATCH 0879/2357] push Recipient into Internal.Types --- .../Process/Platform/GenProcess.hs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 05b0ac4b..02a11151 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -155,7 +155,7 @@ -- That code is, of course, very silly. Under some circumstances, handling -- exit signals is perfectly legitimate. Handling of /other/ forms of -- asynchronous exception is not supported. --- +-- -- If any asynchronous exception goes unhandled, the process will immediately -- exit without running the @terminateHandler@. ----------------------------------------------------------------------------- @@ -177,6 +177,7 @@ module Control.Distributed.Process.Platform.GenProcess , ProcessDefinition(..) -- * Client interaction with the process , start + , runProcess , shutdown , defaultProcess , statelessProcess @@ -230,6 +231,8 @@ import Control.Distributed.Process.Platform.Async hiding (check) import Control.Distributed.Process.Platform.Internal.Types ( TerminateReason(..) , Shutdown(..) + , Recipient(..) + , sendTo ) import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time @@ -245,13 +248,6 @@ import Prelude hiding (init) data ServerId = ServerId ProcessId | ServerName String -data Recipient = - SendToPid ProcessId - | SendToService String - | SendToRemoteService String NodeId - deriving (Typeable) -$(derive makeBinary ''Recipient) - data Message a = CastMessage a | CallMessage a Recipient @@ -379,10 +375,17 @@ start :: a -> InitHandler a s -> ProcessDefinition s -> Process (Either (InitResult s) TerminateReason) -start args init behave = do +start = runProcess recvLoop + +runProcess :: (ProcessDefinition s -> s -> Delay -> Process TerminateReason) + -> a + -> InitHandler a s + -> ProcessDefinition s + -> Process (Either (InitResult s) TerminateReason) +runProcess loop args init def = do ir <- init args case ir of - InitOk s d -> recvLoop behave s d >>= return . Right + InitOk s d -> loop def s d >>= return . Right f@(InitFail _) -> return $ Left f -- | Send a signal instructing the process to terminate. The /receive loop/ which @@ -496,7 +499,7 @@ callAsyncUsing asyncStart sid msg = do asyncStart $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid - sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) + sendTo (Pid sid) (CallMessage msg (Pid wpid)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) @@ -634,11 +637,6 @@ stop r = return $ ProcessStop r stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r -sendTo :: (Serializable m) => Recipient -> m -> Process () -sendTo (SendToPid p) m = send p m -sendTo (SendToService s) m = nsend s m -sendTo (SendToRemoteService s n) m = nsendRemote n s m - replyTo :: (Serializable m) => Recipient -> m -> Process () replyTo client msg = sendTo client (CallResponse msg) From 436c7c355da911175ebe3be4d67ab4174fad5e43 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 11:42:59 +0000 Subject: [PATCH 0880/2357] push Recipient into Internal.Types --- .../Process/Platform/GenProcess.hs | 30 +++++---- .../Process/Platform/Internal/Types.hs | 61 +++++++++++++------ 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 05b0ac4b..02a11151 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -155,7 +155,7 @@ -- That code is, of course, very silly. Under some circumstances, handling -- exit signals is perfectly legitimate. Handling of /other/ forms of -- asynchronous exception is not supported. --- +-- -- If any asynchronous exception goes unhandled, the process will immediately -- exit without running the @terminateHandler@. ----------------------------------------------------------------------------- @@ -177,6 +177,7 @@ module Control.Distributed.Process.Platform.GenProcess , ProcessDefinition(..) -- * Client interaction with the process , start + , runProcess , shutdown , defaultProcess , statelessProcess @@ -230,6 +231,8 @@ import Control.Distributed.Process.Platform.Async hiding (check) import Control.Distributed.Process.Platform.Internal.Types ( TerminateReason(..) , Shutdown(..) + , Recipient(..) + , sendTo ) import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time @@ -245,13 +248,6 @@ import Prelude hiding (init) data ServerId = ServerId ProcessId | ServerName String -data Recipient = - SendToPid ProcessId - | SendToService String - | SendToRemoteService String NodeId - deriving (Typeable) -$(derive makeBinary ''Recipient) - data Message a = CastMessage a | CallMessage a Recipient @@ -379,10 +375,17 @@ start :: a -> InitHandler a s -> ProcessDefinition s -> Process (Either (InitResult s) TerminateReason) -start args init behave = do +start = runProcess recvLoop + +runProcess :: (ProcessDefinition s -> s -> Delay -> Process TerminateReason) + -> a + -> InitHandler a s + -> ProcessDefinition s + -> Process (Either (InitResult s) TerminateReason) +runProcess loop args init def = do ir <- init args case ir of - InitOk s d -> recvLoop behave s d >>= return . Right + InitOk s d -> loop def s d >>= return . Right f@(InitFail _) -> return $ Left f -- | Send a signal instructing the process to terminate. The /receive loop/ which @@ -496,7 +499,7 @@ callAsyncUsing asyncStart sid msg = do asyncStart $ do -- note [call using async] mRef <- monitor sid wpid <- getSelfPid - sendTo (SendToPid sid) (CallMessage msg (SendToPid wpid)) + sendTo (Pid sid) (CallMessage msg (Pid wpid)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) @@ -634,11 +637,6 @@ stop r = return $ ProcessStop r stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) stop_ r _ = stop r -sendTo :: (Serializable m) => Recipient -> m -> Process () -sendTo (SendToPid p) m = send p m -sendTo (SendToService s) m = nsend s m -sendTo (SendToRemoteService s n) m = nsendRemote n s m - replyTo :: (Serializable m) => Recipient -> m -> Process () replyTo client msg = sendTo client (CallResponse msg) diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index 287180d2..fcce6355 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -4,11 +4,16 @@ -- | Types used throughout the Cloud Haskell framework -- module Control.Distributed.Process.Platform.Internal.Types - ( Tag + ( -- * Tagging + Tag , TagPool , newTagPool , getTag + -- * Addressing + , sendTo + , Recipient(..) , RegisterSelf(..) + -- * Interactions , CancelWait(..) , Channel , Shutdown(..) @@ -17,7 +22,7 @@ module Control.Distributed.Process.Platform.Internal.Types import Control.Concurrent.MVar (MVar, newMVar, modifyMVar) import Control.Distributed.Process -import Control.Distributed.Process.Serializable () +import Control.Distributed.Process.Serializable import Data.Binary ( Binary(put, get) , putWord8 @@ -25,14 +30,9 @@ import Data.Binary import Data.DeriveTH import Data.Typeable (Typeable) --- | Simple representation of a channel. -type Channel a = (SendPort a, ReceivePort a) - --- | Used internally in whereisOrStart. Send as (RegisterSelf,ProcessId). -data RegisterSelf = RegisterSelf deriving Typeable -instance Binary RegisterSelf where - put _ = return () - get = return RegisterSelf +-------------------------------------------------------------------------------- +-- API -- +-------------------------------------------------------------------------------- -- | Tags provide uniqueness for messages, so that they can be -- matched with their response. @@ -58,9 +58,23 @@ getTag tp = liftIO $ modifyMVar tp (\tag -> return (tag+1,tag)) data CancelWait = CancelWait deriving (Eq, Show, Typeable) -instance Binary CancelWait where - put CancelWait = return () - get = return CancelWait +-- | Simple representation of a channel. +type Channel a = (SendPort a, ReceivePort a) + +-- | Used internally in whereisOrStart. Send as (RegisterSelf,ProcessId). +data RegisterSelf = RegisterSelf deriving Typeable + +data Recipient = + Pid ProcessId + | Service String + | RemoteService String NodeId + deriving (Typeable) +$(derive makeBinary ''Recipient) + +sendTo :: (Serializable m) => Recipient -> m -> Process () +sendTo (Pid p) m = send p m +sendTo (Service s) m = nsend s m +sendTo (RemoteService s n) m = nsendRemote n s m -- | A ubiquitous /shutdown signal/ that can be used -- to maintain a consistent shutdown/stop protocol for @@ -68,10 +82,6 @@ instance Binary CancelWait where data Shutdown = Shutdown deriving (Typeable, Show, Eq) -instance Binary Shutdown where - get = return Shutdown - put _ = return () - -- | Provides a /reason/ for process termination. data TerminateReason = TerminateNormal -- ^ indicates normal exit @@ -79,3 +89,20 @@ data TerminateReason = | TerminateOther !String -- ^ abnormal (error) shutdown deriving (Typeable, Eq, Show) $(derive makeBinary ''TerminateReason) + +-------------------------------------------------------------------------------- +-- Binary Instances -- +-------------------------------------------------------------------------------- + +instance Binary CancelWait where + put CancelWait = return () + get = return CancelWait + +instance Binary Shutdown where + get = return Shutdown + put _ = return () + +instance Binary RegisterSelf where + put _ = return () + get = return RegisterSelf + From b1867f9dbf29dccccf58ae45ba063e0cbf122a5d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 15:52:31 +0000 Subject: [PATCH 0881/2357] Introduce the Addressable type class An abstraction for sending messages to an arbitrary type that can be resolved to a ProcessId. --- .../Process/Platform/GenProcess.hs | 40 +++++++++---------- tests/SimplePool.hs | 1 + 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 02a11151..c3288947 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -162,10 +162,7 @@ module Control.Distributed.Process.Platform.GenProcess ( -- * Exported data types - ServerId(..) - , Recipient(..) - , TerminateReason(..) - , InitResult(..) + InitResult(..) , ProcessAction(..) , ProcessReply , CallHandler @@ -228,11 +225,11 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Async hiding (check) +import Control.Distributed.Process.Platform.Internal.Primitives import Control.Distributed.Process.Platform.Internal.Types - ( TerminateReason(..) + ( Recipient(..) + , TerminateReason(..) , Shutdown(..) - , Recipient(..) - , sendTo ) import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time @@ -246,8 +243,6 @@ import Prelude hiding (init) -- API -- -------------------------------------------------------------------------------- -data ServerId = ServerId ProcessId | ServerName String - data Message a = CastMessage a | CallMessage a Recipient @@ -455,8 +450,8 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use -- 'safeCall' or combine @catchExit@ and @call@ instead. -tryCall :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Maybe b) +tryCall :: forall s a b . (Addressable s, Serializable a, Serializable b) + => s -> a -> Process (Maybe b) tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing @@ -467,8 +462,8 @@ tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] -- If the result of the call is a failure (or the call was cancelled) then -- the calling process will exit, with the 'AsyncResult' given as the reason. -- -callTimeout :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> TimeInterval -> Process (Maybe b) +callTimeout :: forall s a b . (Addressable s, Serializable a, Serializable b) + => s -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) unpack Nothing = return Nothing @@ -481,8 +476,8 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- -- See "Control.Distributed.Process.Platform.Async" -- -callAsync :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Async b) +callAsync :: forall s a b . (Addressable s, Serializable a, Serializable b) + => s -> a -> Process (Async b) callAsync = callAsyncUsing async -- | As 'callAsync' but takes a function that can be used to generate an async @@ -492,14 +487,15 @@ callAsync = callAsyncUsing async -- -- See "Control.Distributed.Process.Platform.Async" -- -callAsyncUsing :: forall a b . (Serializable a, Serializable b) +callAsyncUsing :: forall s a b . (Addressable s, Serializable a, Serializable b) => (Process b -> Process (Async b)) - -> ProcessId -> a -> Process (Async b) + -> s -> a -> Process (Async b) callAsyncUsing asyncStart sid msg = do asyncStart $ do -- note [call using async] - mRef <- monitor sid + (Just pid) <- resolve sid + mRef <- monitor pid wpid <- getSelfPid - sendTo (Pid sid) (CallMessage msg (Pid wpid)) + sendTo sid (CallMessage msg (Pid wpid)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) @@ -533,9 +529,9 @@ callAsyncUsing asyncStart sid msg = do -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent -- (e.g., dead) server process will not generate an error. -cast :: forall a . (Serializable a) - => ProcessId -> a -> Process () -cast sid msg = send sid (CastMessage msg) +cast :: forall a m . (Addressable a, Serializable m) + => a -> m -> Process () +cast sid msg = sendTo sid (CastMessage msg) -------------------------------------------------------------------------------- -- Producing ProcessAction and ProcessReply from inside handler expressions -- diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index d6cb0071..32453238 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -12,6 +12,7 @@ module SimplePool where import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time From 518ae0d2d6ffa6ff63ec2591b8b99cd0009a50fc Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 15:52:31 +0000 Subject: [PATCH 0882/2357] Introduce the Addressable type class An abstraction for sending messages to an arbitrary type that can be resolved to a ProcessId. --- src/Control/Distributed/Process/Platform.hs | 7 ++- .../Process/Platform/GenProcess.hs | 40 ++++++++--------- .../Process/Platform/Internal/Primitives.hs | 39 ++++++++++++++-- .../Process/Platform/Internal/Types.hs | 44 +++++++++++++++---- 4 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index 854ef364..bce944b2 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -3,7 +3,9 @@ module Control.Distributed.Process.Platform ( -- * Exported Types - TerminateReason(..) + Addressable(..) + , Recipient(..) + , TerminateReason(..) , Tag , TagPool @@ -28,7 +30,8 @@ module Control.Distributed.Process.Platform import Control.Distributed.Process import Control.Distributed.Process.Platform.Internal.Types - ( TerminateReason(..) + ( Recipient(..) + , TerminateReason(..) , Tag , TagPool , newTagPool diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index 02a11151..c3288947 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -162,10 +162,7 @@ module Control.Distributed.Process.Platform.GenProcess ( -- * Exported data types - ServerId(..) - , Recipient(..) - , TerminateReason(..) - , InitResult(..) + InitResult(..) , ProcessAction(..) , ProcessReply , CallHandler @@ -228,11 +225,11 @@ import Control.Concurrent (threadDelay) import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Platform.Async hiding (check) +import Control.Distributed.Process.Platform.Internal.Primitives import Control.Distributed.Process.Platform.Internal.Types - ( TerminateReason(..) + ( Recipient(..) + , TerminateReason(..) , Shutdown(..) - , Recipient(..) - , sendTo ) import Control.Distributed.Process.Platform.Internal.Common import Control.Distributed.Process.Platform.Time @@ -246,8 +243,6 @@ import Prelude hiding (init) -- API -- -------------------------------------------------------------------------------- -data ServerId = ServerId ProcessId | ServerName String - data Message a = CastMessage a | CallMessage a Recipient @@ -455,8 +450,8 @@ safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use -- 'safeCall' or combine @catchExit@ and @call@ instead. -tryCall :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Maybe b) +tryCall :: forall s a b . (Addressable s, Serializable a, Serializable b) + => s -> a -> Process (Maybe b) tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] where unpack (AsyncDone r) = return $ Just r unpack _ = return Nothing @@ -467,8 +462,8 @@ tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] -- If the result of the call is a failure (or the call was cancelled) then -- the calling process will exit, with the 'AsyncResult' given as the reason. -- -callTimeout :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> TimeInterval -> Process (Maybe b) +callTimeout :: forall s a b . (Addressable s, Serializable a, Serializable b) + => s -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) unpack Nothing = return Nothing @@ -481,8 +476,8 @@ callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack -- -- See "Control.Distributed.Process.Platform.Async" -- -callAsync :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Async b) +callAsync :: forall s a b . (Addressable s, Serializable a, Serializable b) + => s -> a -> Process (Async b) callAsync = callAsyncUsing async -- | As 'callAsync' but takes a function that can be used to generate an async @@ -492,14 +487,15 @@ callAsync = callAsyncUsing async -- -- See "Control.Distributed.Process.Platform.Async" -- -callAsyncUsing :: forall a b . (Serializable a, Serializable b) +callAsyncUsing :: forall s a b . (Addressable s, Serializable a, Serializable b) => (Process b -> Process (Async b)) - -> ProcessId -> a -> Process (Async b) + -> s -> a -> Process (Async b) callAsyncUsing asyncStart sid msg = do asyncStart $ do -- note [call using async] - mRef <- monitor sid + (Just pid) <- resolve sid + mRef <- monitor pid wpid <- getSelfPid - sendTo (Pid sid) (CallMessage msg (Pid wpid)) + sendTo sid (CallMessage msg (Pid wpid)) r <- receiveWait [ match (\((CallResponse m) :: CallResponse b) -> return (Right m)) , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) @@ -533,9 +529,9 @@ callAsyncUsing asyncStart sid msg = do -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent -- (e.g., dead) server process will not generate an error. -cast :: forall a . (Serializable a) - => ProcessId -> a -> Process () -cast sid msg = send sid (CastMessage msg) +cast :: forall a m . (Addressable a, Serializable m) + => a -> m -> Process () +cast sid msg = sendTo sid (CastMessage msg) -------------------------------------------------------------------------------- -- Producing ProcessAction and ProcessReply from inside handler expressions -- diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index 3b17a50a..cf967d89 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -1,5 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeSynonymInstances #-} +{-# LANGUAGE FlexibleInstances #-} ----------------------------------------------------------------------------- -- | @@ -16,12 +18,15 @@ ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.Internal.Primitives - ( -- spawning/linking - spawnLinkLocal + ( -- * Exported Types + Addressable(..) + -- spawning/linking + , spawnLinkLocal , spawnMonitorLocal , linkOnFailure -- registration/start + , whereisRemote , whereisOrStart , whereisOrStartRemote @@ -33,8 +38,7 @@ module Control.Distributed.Process.Platform.Internal.Primitives -- remote table , __remoteTable - ) -where + ) where import Control.Concurrent (myThreadId, throwTo) import Control.Distributed.Process @@ -42,6 +46,11 @@ import Control.Distributed.Process.Internal.Closure.BuiltIn (seqCP) import Control.Distributed.Process.Closure (remotable, mkClosure) import Control.Distributed.Process.Serializable (Serializable) import Control.Distributed.Process.Platform.Internal.Types + ( Recipient(..) + , RegisterSelf(..) + , sendToRecipient + , whereisRemote + ) import Control.Monad (void) import Data.Maybe (isJust, fromJust) @@ -53,6 +62,27 @@ n `times` proc = runP proc n runP _ 0 = return () runP p n' = p >> runP p (n' - 1) +-- | Provides a unified API for addressing processes +class Addressable a where + -- | Send a message to the target asynchronously + sendTo :: (Serializable m) => a -> m -> Process () + -- | Resolve the reference to a process id, or @Nothing@ if resolution fails + resolve :: a -> Process (Maybe ProcessId) + +instance Addressable Recipient where + sendTo = sendToRecipient + resolve (Pid p) = return (Just p) + resolve (Registered n) = whereis n + resolve (RemoteRegistered s n) = whereisRemote n s + +instance Addressable ProcessId where + sendTo = send + resolve p = return (Just p) + +instance Addressable String where + sendTo = nsend + resolve = whereis + -- spawning, linking and generic server startup -- | Node local version of 'Control.Distributed.Process.spawnLink'. @@ -185,3 +215,4 @@ matchCond cond = let v n = (isJust n, fromJust n) res = v . cond in matchIf (fst . res) (snd . res) + diff --git a/src/Control/Distributed/Process/Platform/Internal/Types.hs b/src/Control/Distributed/Process/Platform/Internal/Types.hs index fcce6355..06768b58 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Types.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Types.hs @@ -10,19 +10,33 @@ module Control.Distributed.Process.Platform.Internal.Types , newTagPool , getTag -- * Addressing - , sendTo + , sendToRecipient , Recipient(..) , RegisterSelf(..) -- * Interactions + , whereisRemote , CancelWait(..) , Channel , Shutdown(..) , TerminateReason(..) + -- remote table + , __remoteTable ) where -import Control.Concurrent.MVar (MVar, newMVar, modifyMVar) -import Control.Distributed.Process +import Control.Concurrent.MVar + ( MVar + , newMVar + , modifyMVar + ) +import Control.Distributed.Process hiding (send) +import qualified Control.Distributed.Process as P (send) +import Control.Distributed.Process.Closure + ( remotable + , mkClosure + , functionTDict + ) import Control.Distributed.Process.Serializable + import Data.Binary ( Binary(put, get) , putWord8 @@ -66,15 +80,23 @@ data RegisterSelf = RegisterSelf deriving Typeable data Recipient = Pid ProcessId - | Service String - | RemoteService String NodeId + | Registered String + | RemoteRegistered String NodeId deriving (Typeable) $(derive makeBinary ''Recipient) -sendTo :: (Serializable m) => Recipient -> m -> Process () -sendTo (Pid p) m = send p m -sendTo (Service s) m = nsend s m -sendTo (RemoteService s n) m = nsendRemote n s m +sendToRecipient :: (Serializable m) => Recipient -> m -> Process () +sendToRecipient (Pid p) m = P.send p m +sendToRecipient (Registered s) m = nsend s m +sendToRecipient (RemoteRegistered s n) m = nsendRemote n s m + +$(remotable ['whereis]) + +-- | A synchronous version of 'whereis', this relies on 'call' +-- to perform the relevant monitoring of the remote node. +whereisRemote :: NodeId -> String -> Process (Maybe ProcessId) +whereisRemote node name = + call $(functionTDict 'whereis) node ($(mkClosure 'whereis) name) -- | A ubiquitous /shutdown signal/ that can be used -- to maintain a consistent shutdown/stop protocol for @@ -106,3 +128,7 @@ instance Binary RegisterSelf where put _ = return () get = return RegisterSelf +-------------------------------------------------------------------------------- +-- Static Serialisation Dicts and RemoteTable -- +-------------------------------------------------------------------------------- + From 673a630f44112d6fdd92422e24594b8e9dad0c75 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 15:52:31 +0000 Subject: [PATCH 0883/2357] Introduce the Addressable type class An abstraction for sending messages to an arbitrary type that can be resolved to a ProcessId. --- tests/SimplePool.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index d6cb0071..32453238 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -12,6 +12,7 @@ module SimplePool where import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time From ceb73af9fe30f9d1be7d8f8b43d4c7cbecc22d30 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sat, 26 Jan 2013 15:52:31 +0000 Subject: [PATCH 0884/2357] Introduce the Addressable type class An abstraction for sending messages to an arbitrary type that can be resolved to a ProcessId. --- tests/Counter.hs | 1 + tests/MathsDemo.hs | 1 + tests/SimplePool.hs | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/Counter.hs b/tests/Counter.hs index d37b6e5b..56e573ad 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -14,6 +14,7 @@ module Counter ) where import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index e6d7ef6c..ef90d869 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -9,6 +9,7 @@ module MathsDemo import Control.Applicative import Control.Distributed.Process hiding (call) +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index d6cb0071..32453238 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -12,6 +12,7 @@ module SimplePool where import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() +import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async import Control.Distributed.Process.Platform.GenProcess import Control.Distributed.Process.Platform.Time From 872c2accb55590849364ae1fdf72e30ff6f0cf25 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 04:22:30 +0000 Subject: [PATCH 0885/2357] documentation fixes/improvements --- src/Control/Distributed/Process/Platform/GenProcess.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c3288947..91c97898 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -154,10 +154,16 @@ -- -- That code is, of course, very silly. Under some circumstances, handling -- exit signals is perfectly legitimate. Handling of /other/ forms of --- asynchronous exception is not supported. +-- asynchronous exception is not supported by this API. -- -- If any asynchronous exception goes unhandled, the process will immediately --- exit without running the @terminateHandler@. +-- exit without running the @terminateHandler@. It is very important to note +-- that in Cloud Haskell, link failures generate asynchronous exceptions in +-- the target and these will NOT be caught by the API and will therefore +-- cause the process to exit /without running the termination handler/ +-- callback. If your termination handler is set up to do important work +-- (such as resource cleanup) then you should avoid linking you process +-- and use monitors instead. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess From 7a07831076f8fdd5b312868b0db70eecfda42236 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 04:22:30 +0000 Subject: [PATCH 0886/2357] documentation fixes/improvements --- src/Control/Distributed/Process/Platform.hs | 12 ++++++++++-- .../Distributed/Process/Platform/GenProcess.hs | 10 ++++++++-- .../Process/Platform/Internal/Primitives.hs | 18 ++++++++++-------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Control/Distributed/Process/Platform.hs b/src/Control/Distributed/Process/Platform.hs index bce944b2..7a5438e8 100644 --- a/src/Control/Distributed/Process/Platform.hs +++ b/src/Control/Distributed/Process/Platform.hs @@ -1,5 +1,13 @@ --- | [Cloud Haskell Platform] --- +{- | [Cloud Haskell Platform] + +It is /important/ not to be too general when catching exceptions in +handler code, because asynchonous exceptions provide cloud haskell with +its process termination mechanism. Two exceptions in particular, signal +the instigators intention to stop a process immediately, these are raised +in response to the @kill@ and @exit@ primitives provided by +the base distributed-process package. + +-} module Control.Distributed.Process.Platform ( -- * Exported Types diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs index c3288947..91c97898 100644 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ b/src/Control/Distributed/Process/Platform/GenProcess.hs @@ -154,10 +154,16 @@ -- -- That code is, of course, very silly. Under some circumstances, handling -- exit signals is perfectly legitimate. Handling of /other/ forms of --- asynchronous exception is not supported. +-- asynchronous exception is not supported by this API. -- -- If any asynchronous exception goes unhandled, the process will immediately --- exit without running the @terminateHandler@. +-- exit without running the @terminateHandler@. It is very important to note +-- that in Cloud Haskell, link failures generate asynchronous exceptions in +-- the target and these will NOT be caught by the API and will therefore +-- cause the process to exit /without running the termination handler/ +-- callback. If your termination handler is set up to do important work +-- (such as resource cleanup) then you should avoid linking you process +-- and use monitors instead. ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.GenProcess diff --git a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs index cf967d89..5eef2f8f 100644 --- a/src/Control/Distributed/Process/Platform/Internal/Primitives.hs +++ b/src/Control/Distributed/Process/Platform/Internal/Primitives.hs @@ -18,25 +18,26 @@ ----------------------------------------------------------------------------- module Control.Distributed.Process.Platform.Internal.Primitives - ( -- * Exported Types + ( -- * General Purpose Process Addressing Addressable(..) - -- spawning/linking + + -- * Spawning and Linking , spawnLinkLocal , spawnMonitorLocal , linkOnFailure - -- registration/start + -- * Registered Processes , whereisRemote , whereisOrStart , whereisOrStartRemote - -- matching + -- * Selective Receive/Matching , matchCond - -- utility + -- * General Utilities , times - -- remote table + -- * Remote Table , __remoteTable ) where @@ -56,6 +57,7 @@ import Data.Maybe (isJust, fromJust) -- utility +-- | Apply the supplied expression /n/ times times :: Int -> Process () -> Process () n `times` proc = runP proc n where runP :: Process () -> Int -> Process () @@ -103,8 +105,8 @@ spawnMonitorLocal p = do return (pid, ref) -- | CH's 'link' primitive, unlike Erlang's, will trigger when the target --- process dies for any reason. linkOnFailure has semantics like Erlang's: --- it will trigger only when the target dies abnormally. +-- process dies for any reason. This function has semantics like Erlang's: +-- it will trigger 'ProcessLinkException' only when the target dies abnormally. linkOnFailure :: ProcessId -> Process () linkOnFailure them = do us <- getSelfPid From 89ff0453c6ddcf6b8526f8cd4481bb94c0400881 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 15:36:01 +0000 Subject: [PATCH 0887/2357] Bump simplelocalnet version to 0.2.0.9 --- distributed-process-simplelocalnet.cabal | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index 6dd84fec..beaec836 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -1,15 +1,15 @@ Name: distributed-process-simplelocalnet -Version: 0.2.0.8 +Version: 0.2.0.9 Cabal-Version: >=1.8 Build-Type: Simple License: BSD3 License-File: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, duncan@well-typed.com +Maintainer: watson.timothy@gmail.com, edsko@well-typed.com, duncan@well-typed.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process -Bug-Reports: mailto:edsko@well-typed.com +Bug-Reports: http://github.com/haskell-distributed/distributed-process/issues Synopsis: Simple zero-configuration backend for Cloud Haskell Description: Simple backend based on the TCP transport which offers node discovery based on UDP multicast. This is a zero-configuration @@ -38,7 +38,7 @@ Library transformers >= 0.2 && < 0.4, network-transport >= 0.3 && < 0.4, network-transport-tcp >= 0.3 && < 0.4, - distributed-process >= 0.4.1 && < 0.5 + distributed-process >= 0.4.2 && < 0.5 Exposed-modules: Control.Distributed.Process.Backend.SimpleLocalnet, Control.Distributed.Process.Backend.SimpleLocalnet.Internal.Multicast Extensions: RankNTypes, From b85a9f0b72d6070882e84fae84b787cb10b2f515 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 16:54:36 +0000 Subject: [PATCH 0888/2357] bump d-p dependency to include the latest release --- distributed-process-platform.cabal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e570e449..d0a433f8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,12 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -Copyright: Tim Watson +Copyright: Tim Watson 2012 - 2013 Author: Tim Watson Maintainer: watson.timothy@gmail.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform -Bug-Reports: mailto:watson.timothy@gmail.com +Bug-Reports: http://github.com/haskell-distributed/distributed-process-platform/issues synopsis: The Cloud Haskell Application Platform description: Modelled after Erlang's OTP, this framework provides similar facilities for Cloud Haskell, grouping essential practices @@ -27,7 +27,7 @@ source-repository head library build-depends: base >= 4, - distributed-process, + distributed-process >= 0.4.2, derive, binary, mtl, From d239da56b8bc62f165fde8bec425edf02f0d7614 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 16:54:36 +0000 Subject: [PATCH 0889/2357] bump d-p dependency to include the latest release --- distributed-process-platform.cabal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e570e449..d0a433f8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,12 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -Copyright: Tim Watson +Copyright: Tim Watson 2012 - 2013 Author: Tim Watson Maintainer: watson.timothy@gmail.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform -Bug-Reports: mailto:watson.timothy@gmail.com +Bug-Reports: http://github.com/haskell-distributed/distributed-process-platform/issues synopsis: The Cloud Haskell Application Platform description: Modelled after Erlang's OTP, this framework provides similar facilities for Cloud Haskell, grouping essential practices @@ -27,7 +27,7 @@ source-repository head library build-depends: base >= 4, - distributed-process, + distributed-process >= 0.4.2, derive, binary, mtl, From 9769c99d4f68f723119e5e317d7db5fd68e2cb9b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 16:54:36 +0000 Subject: [PATCH 0890/2357] bump d-p dependency to include the latest release --- distributed-process-platform.cabal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e570e449..d0a433f8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,12 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -Copyright: Tim Watson +Copyright: Tim Watson 2012 - 2013 Author: Tim Watson Maintainer: watson.timothy@gmail.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform -Bug-Reports: mailto:watson.timothy@gmail.com +Bug-Reports: http://github.com/haskell-distributed/distributed-process-platform/issues synopsis: The Cloud Haskell Application Platform description: Modelled after Erlang's OTP, this framework provides similar facilities for Cloud Haskell, grouping essential practices @@ -27,7 +27,7 @@ source-repository head library build-depends: base >= 4, - distributed-process, + distributed-process >= 0.4.2, derive, binary, mtl, From a32fe965f9d152b3698a69b90bf565342f498265 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 16:54:36 +0000 Subject: [PATCH 0891/2357] bump d-p dependency to include the latest release --- distributed-process-platform.cabal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index e570e449..d0a433f8 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -5,12 +5,12 @@ build-type: Simple license: BSD3 license-file: LICENCE stability: experimental -Copyright: Tim Watson +Copyright: Tim Watson 2012 - 2013 Author: Tim Watson Maintainer: watson.timothy@gmail.com Stability: experimental Homepage: http://github.com/haskell-distributed/distributed-process-platform -Bug-Reports: mailto:watson.timothy@gmail.com +Bug-Reports: http://github.com/haskell-distributed/distributed-process-platform/issues synopsis: The Cloud Haskell Application Platform description: Modelled after Erlang's OTP, this framework provides similar facilities for Cloud Haskell, grouping essential practices @@ -27,7 +27,7 @@ source-repository head library build-depends: base >= 4, - distributed-process, + distributed-process >= 0.4.2, derive, binary, mtl, From 4d5837bb3532b5f0a0efeb01c154e16ac1d4f796 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 21:05:45 +0000 Subject: [PATCH 0892/2357] simplelocalnet changelog update --- ChangeLog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog b/ChangeLog index 031ad2d3..e86696b1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2013-01-27 Tim Watson 0.2.0.9 + +* Shut down the logger process before exiting +* Improvements to the redirection of logging +* Fix restarting of master + 2012-11-22 Edsko de Vries 0.2.0.8 * Use the new 'register' semantics (depends on distributed-process-0.4.1). From a2293b1551194976a78b1a915fb84231544e47f6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 18:48:53 +0000 Subject: [PATCH 0893/2357] Split up GenProcess into a set of specific APIs --- distributed-process-platform.cabal | 47 +++++++++++++++++------- src/Control/Distributed/Process/Async.hs | 10 ++--- tests/SimplePool.hs | 2 +- tests/TestGenServer.hs | 2 +- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d0a433f8..38e7dfb9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -38,20 +38,24 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform.Async.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite TimerTests type: exitcode-stdio-1.0 @@ -78,6 +82,11 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs + other-modules: + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -104,7 +113,11 @@ test-suite PrimitivesTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -132,7 +145,11 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -160,11 +177,10 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, @@ -176,7 +192,12 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, - SimplePool + SimplePool, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index a34aef15..e7656c78 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -113,16 +113,14 @@ asyncLinkedSTM :: (Serializable a) => AsyncTask a -> Process (Async a) asyncLinkedSTM = AsyncSTM.newAsync AsyncSTM.asyncLinked -- | Spawn an 'AsyncTask' and return the 'Async' handle to it. --- Uses the channel based implementation, whose handles can be read by other --- processes, though they're not @Serializable@. +-- Uses a channel based implementation, whose handles can only be read once, +-- and only by the calling process. -- --- See 'Control.Distributed.Process.Platform.Async.AsyncSTM'. +-- See 'Control.Distributed.Process.Platform.Async.AsyncChan'. asyncChan :: (Serializable a) => AsyncTask a -> Process (Async a) asyncChan = AsyncChan.newAsync AsyncChan.async --- | Spawn an 'AsyncTask' (linked to the calling process) and return the --- 'Async' handle to it. Uses the channel based implementation, whose handles --- can be read by other processes, though they're not @Serializable@. +-- | Linked version of 'asyncChan'. -- -- See 'Control.Distributed.Process.Platform.Async.AsyncChan'. asyncLinkedChan :: (Serializable a) => AsyncTask a -> Process (Async a) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 32453238..1c9f4cbb 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -14,7 +14,7 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable import Control.Exception hiding (catch) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 774e8de3..912e744a 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -15,7 +15,7 @@ import Control.Distributed.Process.Closure import Control.Distributed.Process.Node import Control.Distributed.Process.Platform hiding (__remoteTable) import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer From 57795272ef15773fa009184a649e45181bf58bf5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 18:48:53 +0000 Subject: [PATCH 0894/2357] Split up GenProcess into a set of specific APIs --- distributed-process-platform.cabal | 47 +++++++++++++++++++++--------- tests/Counter.hs | 2 +- tests/MathsDemo.hs | 2 +- tests/SimplePool.hs | 2 +- tests/TestGenServer.hs | 2 +- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d0a433f8..38e7dfb9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -38,20 +38,24 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform.Async.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite TimerTests type: exitcode-stdio-1.0 @@ -78,6 +82,11 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs + other-modules: + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -104,7 +113,11 @@ test-suite PrimitivesTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -132,7 +145,11 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -160,11 +177,10 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, @@ -176,7 +192,12 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, - SimplePool + SimplePool, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs diff --git a/tests/Counter.hs b/tests/Counter.hs index 56e573ad..51a3f9d3 100644 --- a/tests/Counter.hs +++ b/tests/Counter.hs @@ -16,7 +16,7 @@ module Counter import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Data.Binary import Data.DeriveTH diff --git a/tests/MathsDemo.hs b/tests/MathsDemo.hs index ef90d869..9949dd20 100644 --- a/tests/MathsDemo.hs +++ b/tests/MathsDemo.hs @@ -10,7 +10,7 @@ module MathsDemo import Control.Applicative import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Platform -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Data.Binary (Binary(..)) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 32453238..1c9f4cbb 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -14,7 +14,7 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable import Control.Exception hiding (catch) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 774e8de3..912e744a 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -15,7 +15,7 @@ import Control.Distributed.Process.Closure import Control.Distributed.Process.Node import Control.Distributed.Process.Platform hiding (__remoteTable) import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer From d89b7a140a0bd00b741f39f83ba092e17a2aabaa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 18:48:53 +0000 Subject: [PATCH 0895/2357] Split up GenProcess into a set of specific APIs --- distributed-process-platform.cabal | 47 +- .../Process/Platform/GenProcess.hs | 998 ------------------ .../Distributed/Process/Platform/GenServer.hs | 351 ------ tests/SimplePool.hs | 2 +- tests/TestGenServer.hs | 2 +- 5 files changed, 36 insertions(+), 1364 deletions(-) delete mode 100644 src/Control/Distributed/Process/Platform/GenProcess.hs delete mode 100644 src/Control/Distributed/Process/Platform/GenServer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d0a433f8..38e7dfb9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -38,20 +38,24 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform.Async.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite TimerTests type: exitcode-stdio-1.0 @@ -78,6 +82,11 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs + other-modules: + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -104,7 +113,11 @@ test-suite PrimitivesTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -132,7 +145,11 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -160,11 +177,10 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, @@ -176,7 +192,12 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, - SimplePool + SimplePool, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs deleted file mode 100644 index 91c97898..00000000 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ /dev/null @@ -1,998 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} - ------------------------------------------------------------------------------ --- | --- Module : Control.Distributed.Process.Platform.GenProcess --- Copyright : (c) Tim Watson 2012 --- License : BSD3 (see the file LICENSE) --- --- Maintainer : Tim Watson --- Stability : experimental --- Portability : non-portable (requires concurrency) --- --- This module provides a high(er) level API for building complex 'Process' --- implementations by abstracting out the management of the process' mailbox, --- reply/response handling, timeouts, process hiberation, error handling --- and shutdown/stop procedures. It is modelled along similar lines to OTP's --- gen_server API - . --- --- [API Overview] --- --- Once started, a generic process will consume messages from its mailbox and --- pass them on to user defined /handlers/ based on the types received (mapped --- to those accepted by the handlers) and optionally by also evaluating user --- supplied predicates to determine which handlers are valid. --- Each handler returns a 'ProcessAction' which specifies how we should proceed. --- If none of the handlers is able to process a message (because their types are --- incompatible) then the process 'unhandledMessagePolicy' will be applied. --- --- The 'ProcessAction' type defines the ways in which a process can respond --- to its inputs, either by continuing to read incoming messages, setting an --- optional timeout, sleeping for a while or by stopping. The optional timeout --- behaves a little differently to the other process actions. If no messages --- are received within the specified time span, the process 'timeoutHandler' --- will be called in order to determine the next action. --- --- Generic processes are defined by the 'ProcessDefinition' type, using record --- syntax. The 'ProcessDefinition' fields contain handlers (or lists of them) --- for specific tasks. In addtion to the @timeoutHandler@, a 'ProcessDefinition' --- may also define a @terminateHandler@ which is called just before the process --- exits. This handler will be called /whenever/ the process is stopping, i.e., --- when a callback returns 'stop' as the next action /or/ if an unhandled exit --- signal or similar asynchronous exception is thrown in (or to) the process --- itself. --- --- The other handlers are split into two groups: /apiHandlers/ and /infoHandlers/. --- The former contains handlers for the 'cast' and 'call' protocols, whilst the --- latter contains handlers that deal with input messages which are not sent --- via these API calls (i.e., messages sent using bare 'send' or signals put --- into the process mailbox by the node controller, such as --- 'ProcessMonitorNotification' and the like). --- --- [The Cast/Call Protocol] --- --- Deliberate interactions with the process will usually fall into one of two --- categories. A 'cast' interaction involves a client sending a message --- asynchronously and the server handling this input. No reply is sent to --- the client. On the other hand, a 'call' interaction is a kind of /rpc/ --- where the client sends a message and waits for a reply. --- --- The expressions given to @apiHandlers@ have to conform to the /cast|call/ --- protocol. The details of this are, however, hidden from the user. A set --- of API functions for creating @apiHandlers@ are given instead, which --- take expressions (i.e., a function or lambda expression) and create the --- appropriate @Dispatcher@ for handling the cast (or call). --- --- The cast/call protocol handlers deal with /expected/ inputs. These form --- the explicit public API for the process, and will usually be exposed by --- providing module level functions that defer to the cast/call API. For --- example: --- --- @ --- add :: ProcessId -> Double -> Double -> Double --- add pid x y = call pid (Add x y) --- @ --- --- [Handling Info Messages] --- --- An explicit protocol for communicating with the process can be --- configured using 'cast' and 'call', but it is not possible to prevent --- other kinds of messages from being sent to the process mailbox. When --- any message arrives for which there are no handlers able to process --- its content, the 'UnhandledMessagePolicy' will be applied. Sometimes --- it is desireable to process incoming messages which aren't part of the --- protocol, rather than let the policy deal with them. This is particularly --- true when incoming messages are important to the process, but their point --- of origin is outside the developer's control. Handling /signals/ such as --- 'ProcessMonitorNotification' is a typical example of this: --- --- > handleInfo_ (\(ProcessMonitorNotification _ _ r) -> say $ show r >> continue_) --- --- [Handling Process State] --- --- The 'ProcessDefinition' is parameterised by the type of state it maintains. --- A process that has no state will have the type @ProcessDefinition ()@ and can --- be bootstrapped by evaluating 'statelessProcess'. --- --- All call/cast handlers come in two flavours, those which take the process --- state as an input and those which do not. Handlers that ignore the process --- state have to return a function that takes the state and returns the required --- action. Versions of the various action generating functions ending in an --- underscore are provided to simplify this: --- --- @ --- statelessProcess { --- apiHandlers = [ --- handleCall_ (\\(n :: Int) -> return (n * 2)) --- , handleCastIf_ (\\(c :: String, _ :: Delay) -> c == \"timeout\") --- (\\(\"timeout\", Delay d) -> timeoutAfter_ d) --- ] --- , timeoutHandler = \\_ _ -> stop $ TerminateOther \"timeout\" --- } --- @ --- --- [Handling Errors] --- --- Error handling appears in several contexts and process definitions can --- hook into these with relative ease. Only process failures as a result of --- asynchronous exceptions are supported by the API, which provides several --- scopes for error handling. --- --- Catching exceptions inside handler functions is no different to ordinary --- exception handling in monadic code. --- --- @ --- handleCall (\\x y -> --- catch (hereBeDragons x y) --- (\\(e :: SmaugTheTerribleException) -> --- return (Left (show e)))) --- @ --- --- The caveats mentioned in "Control.Distributed.Process.Platform" about --- exit signal handling obviously apply here as well. --- --- [Structured Exit Signal Handling] --- --- Because "Control.Distributed.Process.ProcessExitException" is a ubiquitous --- /signalling mechanism/ in Cloud Haskell, it is treated unlike other --- asynchronous exceptions. The 'ProcessDefinition' 'exitHandlers' field --- accepts a list of handlers that, for a specific exit reason, can decide --- how the process should respond. If none of these handlers matches the --- type of @reason@ then the process will exit with @DiedException why@. In --- addition, a default /exit handler/ is installed for exit signals where the --- @reason == Shutdown@, because this is an /exit signal/ used explicitly and --- extensively throughout the platform. The default behaviour is to gracefully --- shut down the process, calling the @terminateHandler@ as usual, before --- stopping with @TerminateShutdown@ given as the final outcome. --- --- /Example: How to annoy your supervisor and end up force-killed:/ --- --- > handleExit (\state from (sigExit :: Shutdown) -> continue s) --- --- That code is, of course, very silly. Under some circumstances, handling --- exit signals is perfectly legitimate. Handling of /other/ forms of --- asynchronous exception is not supported by this API. --- --- If any asynchronous exception goes unhandled, the process will immediately --- exit without running the @terminateHandler@. It is very important to note --- that in Cloud Haskell, link failures generate asynchronous exceptions in --- the target and these will NOT be caught by the API and will therefore --- cause the process to exit /without running the termination handler/ --- callback. If your termination handler is set up to do important work --- (such as resource cleanup) then you should avoid linking you process --- and use monitors instead. ------------------------------------------------------------------------------ - -module Control.Distributed.Process.Platform.GenProcess - ( -- * Exported data types - InitResult(..) - , ProcessAction(..) - , ProcessReply - , CallHandler - , CastHandler - , InitHandler - , TerminateHandler - , TimeoutHandler - , UnhandledMessagePolicy(..) - , ProcessDefinition(..) - -- * Client interaction with the process - , start - , runProcess - , shutdown - , defaultProcess - , statelessProcess - , statelessInit - , call - , safeCall - , tryCall - , callAsync - , callTimeout - , cast - -- * Handler interaction inside the process - , condition - , state - , input - , reply - , replyWith - , noReply - , noReply_ - , haltNoReply_ - , continue - , continue_ - , timeoutAfter - , timeoutAfter_ - , hibernate - , hibernate_ - , stop - , stop_ - , replyTo - -- * Handler callback creation - , handleCall - , handleCallIf - , handleCallFrom - , handleCallFromIf - , handleCast - , handleCastIf - , handleInfo - , handleDispatch - , handleExit - -- * Stateless handlers - , action - , handleCall_ - , handleCallIf_ - , handleCast_ - , handleCastIf_ - ) where - -import Control.Concurrent (threadDelay) -import Control.Distributed.Process hiding (call) -import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Async hiding (check) -import Control.Distributed.Process.Platform.Internal.Primitives -import Control.Distributed.Process.Platform.Internal.Types - ( Recipient(..) - , TerminateReason(..) - , Shutdown(..) - ) -import Control.Distributed.Process.Platform.Internal.Common -import Control.Distributed.Process.Platform.Time - -import Data.Binary hiding (decode) -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - -data Message a = - CastMessage a - | CallMessage a Recipient - deriving (Typeable) -$(derive makeBinary ''Message) - -data CallResponse a = CallResponse a - deriving (Typeable) -$(derive makeBinary ''CallResponse) - --- | Return type for and 'InitHandler' expression. -data InitResult s = - InitOk s Delay {- - ^ denotes successful initialisation, initial state and timeout -} - | forall r. (Serializable r) - => InitFail r -- ^ denotes failed initialisation and the reason - --- | The action taken by a process after a handler has run and its updated state. --- See 'continue' --- 'timeoutAfter' --- 'hibernate' --- 'stop' --- -data ProcessAction s = - ProcessContinue s -- ^ continue with (possibly new) state - | ProcessTimeout TimeInterval s -- ^ timeout if no messages are received - | ProcessHibernate TimeInterval s -- ^ hibernate for /delay/ - | ProcessStop TerminateReason -- ^ stop the process, giving @TerminateReason@ - --- | Returned from handlers for the synchronous 'call' protocol, encapsulates --- the reply data /and/ the action to take after sending the reply. A handler --- can return @NoReply@ if they wish to ignore the call. -data ProcessReply s a = - ProcessReply a (ProcessAction s) - | NoReply (ProcessAction s) - -type CallHandler a s = s -> a -> Process (ProcessReply s a) - -type CastHandler s = s -> Process () - --- type InfoHandler a = forall a b. (Serializable a, Serializable b) => a -> Process b - --- | Wraps a predicate that is used to determine whether or not a handler --- is valid based on some combination of the current process state, the --- type and/or value of the input message or both. -data Condition s m = - Condition (s -> m -> Bool) -- ^ predicated on the process state /and/ the message - | State (s -> Bool) -- ^ predicated on the process state only - | Input (m -> Bool) -- ^ predicated on the input message only - --- | An expression used to initialise a process with its state. -type InitHandler a s = a -> Process (InitResult s) - --- | An expression used to handle process termination. -type TerminateHandler s = s -> TerminateReason -> Process () - --- | An expression used to handle process timeouts. -type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) - --- dispatching to implementation callbacks - --- | Provides dispatch from cast and call messages to a typed handler. -data Dispatcher s = - forall a . (Serializable a) => Dispatch { - dispatch :: s -> Message a -> Process (ProcessAction s) - } - | forall a . (Serializable a) => DispatchIf { - dispatch :: s -> Message a -> Process (ProcessAction s) - , dispatchIf :: s -> Message a -> Bool - } - --- | Provides dispatch for any input, returns 'Nothing' for unhandled messages. -data DeferredDispatcher s = DeferredDispatcher { - dispatchInfo :: s - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) - } - --- | Provides dispatch for any exit signal - returns 'Nothing' for unhandled exceptions -data ExitSignalDispatcher s = ExitSignalDispatcher { - dispatchExit :: s - -> ProcessId - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) - } - -class MessageMatcher d where - matchMessage :: UnhandledMessagePolicy -> s -> d s -> Match (ProcessAction s) - -instance MessageMatcher Dispatcher where - matchMessage _ s (Dispatch d) = match (d s) - matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) - --- | Policy for handling unexpected messages, i.e., messages which are not --- sent using the 'call' or 'cast' APIs, and which are not handled by any of the --- 'handleInfo' handlers. -data UnhandledMessagePolicy = - Terminate -- ^ stop immediately, giving @TerminateOther "UnhandledInput"@ as the reason - | DeadLetter ProcessId -- ^ forward the message to the given recipient - | Drop -- ^ dequeue and then drop/ignore the message - --- | Stores the functions that determine runtime behaviour in response to --- incoming messages and a policy for responding to unhandled messages. -data ProcessDefinition s = ProcessDefinition { - apiHandlers :: [Dispatcher s] -- ^ functions that handle call/cast messages - , infoHandlers :: [DeferredDispatcher s] -- ^ functions that handle non call/cast messages - , exitHandlers :: [ExitSignalDispatcher s] -- ^ functions that handle exit signals - , timeoutHandler :: TimeoutHandler s -- ^ a function that handles timeouts - , terminateHandler :: TerminateHandler s -- ^ a function that is run just before the process exits - , unhandledMessagePolicy :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages - } - --------------------------------------------------------------------------------- --- Client API -- --------------------------------------------------------------------------------- - --- TODO: automatic registration - --- | Starts a gen-process configured with the supplied process definition, --- using an init handler and its initial arguments. This code will run the --- 'Process' until completion and return @Right TerminateReason@ *or*, --- if initialisation fails, return @Left InitResult@ which will be --- @InitFail why@. -start :: a - -> InitHandler a s - -> ProcessDefinition s - -> Process (Either (InitResult s) TerminateReason) -start = runProcess recvLoop - -runProcess :: (ProcessDefinition s -> s -> Delay -> Process TerminateReason) - -> a - -> InitHandler a s - -> ProcessDefinition s - -> Process (Either (InitResult s) TerminateReason) -runProcess loop args init def = do - ir <- init args - case ir of - InitOk s d -> loop def s d >>= return . Right - f@(InitFail _) -> return $ Left f - --- | Send a signal instructing the process to terminate. The /receive loop/ which --- manages the process mailbox will prioritise @Shutdown@ signals higher than --- any other incoming messages, but the server might be busy (i.e., still in the --- process of excuting a handler) at the time of sending however, so the caller --- should not make any assumptions about the timeliness with which the shutdown --- signal will be handled. If responsiveness is important, a better approach --- might be to send an /exit signal/ with 'Shutdown' as the reason. An exit --- signal will interrupt any operation currently underway and force the running --- process to clean up and terminate. -shutdown :: ProcessId -> Process () -shutdown pid = cast pid Shutdown - -defaultProcess :: ProcessDefinition s -defaultProcess = ProcessDefinition { - apiHandlers = [] - , infoHandlers = [] - , exitHandlers = [] - , timeoutHandler = \s _ -> continue s - , terminateHandler = \_ _ -> return () - , unhandledMessagePolicy = Terminate - } :: ProcessDefinition s - --- | A basic, stateless process definition, where the unhandled message policy --- is set to 'Terminate', the default timeout handlers does nothing (i.e., the --- same as calling @continue ()@ and the terminate handler is a no-op. -statelessProcess :: ProcessDefinition () -statelessProcess = ProcessDefinition { - apiHandlers = [] - , infoHandlers = [] - , exitHandlers = [] - , timeoutHandler = \s _ -> continue s - , terminateHandler = \_ _ -> return () - , unhandledMessagePolicy = Terminate - } - --- | A basic, state /unaware/ 'InitHandler' that can be used with --- 'statelessProcess'. -statelessInit :: Delay -> InitHandler () () -statelessInit d () = return $ InitOk () d - --- | Make a synchronous call - will block until a reply is received. --- The calling process will exit with 'TerminateReason' if the calls fails. -call :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process b -call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] - where unpack :: AsyncResult b -> Process b - unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ explain "CallFailed" r - unpack (AsyncLinkFailed r) = die $ explain "LinkFailed" r - unpack AsyncCancelled = die $ TerminateOther $ "Cancelled" - unpack AsyncPending = terminate -- as this *cannot* happen - --- | Safe version of 'call' that returns information about the error --- if the operation fails. If an error occurs then the explanation will be --- will be stashed away as @(TerminateOther String)@. -safeCall :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Either TerminateReason b) -safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] - where unpack (AsyncDone r) = return $ Right r - unpack (AsyncFailed r) = return $ Left $ explain "CallFailed" r - unpack (AsyncLinkFailed r) = return $ Left $ explain "LinkFailed" r - unpack AsyncCancelled = return $ Left $ TerminateOther $ "Cancelled" - unpack AsyncPending = return $ Left $ TerminateOther $ "Pending" - --- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If --- you need information about *why* a call has failed then you should use --- 'safeCall' or combine @catchExit@ and @call@ instead. -tryCall :: forall s a b . (Addressable s, Serializable a, Serializable b) - => s -> a -> Process (Maybe b) -tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] - where unpack (AsyncDone r) = return $ Just r - unpack _ = return Nothing - --- | Make a synchronous call, but timeout and return @Nothing@ if the reply --- is not received within the specified time interval. --- --- If the result of the call is a failure (or the call was cancelled) then --- the calling process will exit, with the 'AsyncResult' given as the reason. --- -callTimeout :: forall s a b . (Addressable s, Serializable a, Serializable b) - => s -> a -> TimeInterval -> Process (Maybe b) -callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack - where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) - unpack Nothing = return Nothing - unpack (Just (AsyncDone r)) = return $ Just r - unpack (Just other) = die other - --- | Performs a synchronous 'call' to the the given server address, however the --- call is made /out of band/ and an async handle is returned immediately. This --- can be passed to functions in the /Async/ API in order to obtain the result. --- --- See "Control.Distributed.Process.Platform.Async" --- -callAsync :: forall s a b . (Addressable s, Serializable a, Serializable b) - => s -> a -> Process (Async b) -callAsync = callAsyncUsing async - --- | As 'callAsync' but takes a function that can be used to generate an async --- task and return an async handle to it. This can be used to switch between --- async implementations, by e.g., using an async channel instead of the default --- STM based handle. --- --- See "Control.Distributed.Process.Platform.Async" --- -callAsyncUsing :: forall s a b . (Addressable s, Serializable a, Serializable b) - => (Process b -> Process (Async b)) - -> s -> a -> Process (Async b) -callAsyncUsing asyncStart sid msg = do - asyncStart $ do -- note [call using async] - (Just pid) <- resolve sid - mRef <- monitor pid - wpid <- getSelfPid - sendTo sid (CallMessage msg (Pid wpid)) - r <- receiveWait [ - match (\((CallResponse m) :: CallResponse b) -> return (Right m)) - , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) - (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) - ] - -- TODO: better failure API - unmonitor mRef - case r of - Right m -> return m - Left err -> die $ TerminateOther ("ServerExit (" ++ (show err) ++ ")") - --- note [call using async] --- One problem with using plain expect/receive primitives to perform a --- synchronous (round trip) call is that a reply matching the expected type --- could come from anywhere! The Call.hs module uses a unique integer tag to --- distinguish between inputs but this is easy to forge, as is tagging the --- response with the sender's pid. --- --- The approach we take here is to rely on AsyncSTM (by default) to insulate us --- from erroneous incoming messages without the need for tagging. The /handle/ --- returned uses an @STM (AsyncResult a)@ field to handle the response /and/ --- the implementation spawns a new process to perform the actual call and --- await the reply before atomically updating the result. Whilst in theory, --- given a hypothetical 'listAllProcesses' primitive, it might be possible for --- malacious code to obtain the ProcessId of the worker and send a false reply, --- the likelihood of this is small enough that it seems reasonable to assume --- we've solved the problem without the need for tags or globally unique --- identifiers. - --- | Sends a /cast/ message to the server identified by 'ServerId'. The server --- will not send a response. Like Cloud Haskell's 'send' primitive, cast is --- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent --- (e.g., dead) server process will not generate an error. -cast :: forall a m . (Addressable a, Serializable m) - => a -> m -> Process () -cast sid msg = sendTo sid (CastMessage msg) - --------------------------------------------------------------------------------- --- Producing ProcessAction and ProcessReply from inside handler expressions -- --------------------------------------------------------------------------------- - --- | Creates a 'Conditon' from a function that takes a process state @a@ and --- an input message @b@ and returns a 'Bool' indicating whether the associated --- handler should run. --- -condition :: forall a b. (Serializable a, Serializable b) - => (a -> b -> Bool) - -> Condition a b -condition = Condition - --- | Create a 'Condition' from a function that takes a process state @a@ and --- returns a 'Bool' indicating whether the associated handler should run. --- -state :: forall s m. (Serializable m) => (s -> Bool) -> Condition s m -state = State - --- | Creates a 'Condition' from a function that takes an input message @m@ and --- returns a 'Bool' indicating whether the associated handler should run. --- -input :: forall s m. (Serializable m) => (m -> Bool) -> Condition s m -input = Input - --- | Instructs the process to send a reply and continue running. -reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) -reply r s = continue s >>= replyWith r - --- | Instructs the process to send a reply /and/ evaluate the 'ProcessAction'. -replyWith :: (Serializable m) - => m - -> ProcessAction s - -> Process (ProcessReply s m) -replyWith msg st = return $ ProcessReply msg st - --- | Instructs the process to skip sending a reply /and/ evaluate a 'ProcessAction' -noReply :: (Serializable r) => ProcessAction s -> Process (ProcessReply s r) -noReply = return . NoReply - --- | Continue without giving a reply to the caller - equivalent to 'continue', --- but usable in a callback passed to the 'handleCall' family of functions. -noReply_ :: forall s r . (Serializable r) => s -> Process (ProcessReply s r) -noReply_ s = continue s >>= noReply - --- | Halt process execution during a call handler, without paying any attention --- to the expected return type. -haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) -haltNoReply_ r = stop r >>= noReply - --- | Instructs the process to continue running and receiving messages. -continue :: s -> Process (ProcessAction s) -continue = return . ProcessContinue - --- | Version of 'continue' that can be used in handlers that ignore process state. --- -continue_ :: (s -> Process (ProcessAction s)) -continue_ = return . ProcessContinue - --- | Instructs the process to wait for incoming messages until 'TimeInterval' --- is exceeded. If no messages are handled during this period, the /timeout/ --- handler will be called. Note that this alters the process timeout permanently --- such that the given @TimeInterval@ will remain in use until changed. -timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) -timeoutAfter d s = return $ ProcessTimeout d s - --- | Version of 'timeoutAfter' that can be used in handlers that ignore process state. --- --- > action (\(TimeoutPlease duration) -> timeoutAfter_ duration) --- -timeoutAfter_ :: TimeInterval -> (s -> Process (ProcessAction s)) -timeoutAfter_ d = return . ProcessTimeout d - --- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note --- that no messages will be removed from the mailbox until after hibernation has --- ceased. This is equivalent to calling @threadDelay@. --- -hibernate :: TimeInterval -> s -> Process (ProcessAction s) -hibernate d s = return $ ProcessHibernate d s - --- | Version of 'hibernate' that can be used in handlers that ignore process state. --- --- > action (\(HibernatePlease delay) -> hibernate_ delay) --- -hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) -hibernate_ d = return . ProcessHibernate d - --- | Instructs the process to terminate, giving the supplied reason. If a valid --- 'terminateHandler' is installed, it will be called with the 'TerminateReason' --- returned from this call, along with the process state. -stop :: TerminateReason -> Process (ProcessAction s) -stop r = return $ ProcessStop r - --- | Version of 'stop' that can be used in handlers that ignore process state. --- --- > action (\ClientError -> stop_ TerminateNormal) --- -stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) -stop_ r _ = stop r - -replyTo :: (Serializable m) => Recipient -> m -> Process () -replyTo client msg = sendTo client (CallResponse msg) - --------------------------------------------------------------------------------- --- Wrapping handler expressions in Dispatcher and DeferredDispatcher -- --------------------------------------------------------------------------------- - --- | Constructs a 'call' handler from a function in the 'Process' monad. --- The handler expression returns the reply, and the action will be --- set to 'continue'. --- --- > handleCall_ = handleCallIf_ (const True) --- -handleCall_ :: (Serializable a, Serializable b) - => (a -> Process b) - -> Dispatcher s -handleCall_ = handleCallIf_ $ input (const True) - --- | Constructs a 'call' handler from an ordinary function in the 'Process' --- monad. This variant ignores the state argument present in 'handleCall' and --- 'handleCallIf' and is therefore useful in a stateless server. Messges are --- only dispatched to the handler if the supplied condition evaluates to @True@ --- --- See 'handleCall' -handleCallIf_ :: forall s a b . (Serializable a, Serializable b) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (a -> Process b) -- ^ a function from an input message to a reply - -> Dispatcher s -handleCallIf_ cond handler - = DispatchIf { - dispatch = doHandle handler - , dispatchIf = checkCall cond - } - where doHandle :: (Serializable a, Serializable b) - => (a -> Process b) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h p) >>= mkCallReply c s - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - - -- handling 'reply-to' in the main process loop is awkward at best, - -- so we handle it here instead and return the 'action' to the loop - mkCallReply :: (Serializable b) - => Recipient -> s -> b -> Process (ProcessAction s) - mkCallReply c s m = sendTo c (CallResponse m) >> continue s - --- | Constructs a 'call' handler from a function in the 'Process' monad. --- > handleCall = handleCallIf (const True) --- -handleCall :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) - -> Dispatcher s -handleCall = handleCallIf $ state (const True) - --- | Constructs a 'call' handler from an ordinary function in the 'Process' --- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, --- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. Messages are only --- dispatched to the handler if the supplied condition evaluates to @True@ --- -handleCallIf :: forall s a b . (Serializable a, Serializable b) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (s -> a -> Process (ProcessReply s b)) - -- ^ a reply yielding function over the process state and input message - -> Dispatcher s -handleCallIf cond handler - = DispatchIf { - dispatch = doHandle handler - , dispatchIf = checkCall cond - } - where doHandle :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - --- | As 'handleCall' but passes the 'Recipient' to the handler function. --- This can be useful if you wish to /reply later/ to the caller by, e.g., --- spawning a process to do some work and have it @replyTo caller response@ --- out of band. In this case the callback can pass the 'Recipient' to the --- worker (or stash it away itself) and return 'noReply'. --- -handleCallFrom :: forall s a b . (Serializable a, Serializable b) - => (s -> Recipient -> a -> Process (ProcessReply s b)) - -> Dispatcher s -handleCallFrom = handleCallFromIf $ state (const True) - --- | As 'handleCallFrom' but only runs the handler if the supplied 'Condition' --- evaluates to @True@. --- -handleCallFromIf :: forall s a b . (Serializable a, Serializable b) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (s -> Recipient -> a -> Process (ProcessReply s b)) - -- ^ a reply yielding function over the process state, sender and input message - -> Dispatcher s -handleCallFromIf cond handler - = DispatchIf { - dispatch = doHandle handler - , dispatchIf = checkCall cond - } - where doHandle :: (Serializable a, Serializable b) - => (s -> Recipient -> a -> Process (ProcessReply s b)) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h s c p) >>= mkReply c - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - --- | Constructs a 'cast' handler from an ordinary function in the 'Process' --- monad. --- > handleCast = handleCastIf (const True) --- -handleCast :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleCast = handleCastIf $ input (const True) - --- | Constructs a 'cast' handler from an ordinary function in the 'Process' --- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, --- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. --- -handleCastIf :: forall s a . (Serializable a) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (s -> a -> Process (ProcessAction s)) - -- ^ an action yielding function over the process state and input message - -> Dispatcher s -handleCastIf cond h - = DispatchIf { - dispatch = (\s (CastMessage p) -> h s p) - , dispatchIf = checkCast cond - } - --- | Version of 'handleCast' that ignores the server state. --- -handleCast_ :: (Serializable a) - => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -handleCast_ = handleCastIf_ $ input (const True) - --- | Version of 'handleCastIf' that ignores the server state. --- -handleCastIf_ :: forall s a . (Serializable a) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (a -> (s -> Process (ProcessAction s))) - -- ^ a function from the input message to a /stateless action/, cf 'continue_' - -> Dispatcher s -handleCastIf_ cond h - = DispatchIf { - dispatch = (\s (CastMessage p) -> h p $ s) - , dispatchIf = checkCast cond - } - --- | Constructs an /action/ handler. Like 'handleDispatch' this can handle both --- 'cast' and 'call' messages and you won't know which you're dealing with. --- This can be useful where certain inputs require a definite action, such as --- stopping the server, without concern for the state (e.g., when stopping we --- need only decide to stop, as the terminate handler can deal with state --- cleanup etc). For example: --- --- @action (\MyCriticalErrorSignal -> stop_ TerminateNormal)@ --- -action :: forall s a . (Serializable a) - => (a -> (s -> Process (ProcessAction s))) - -- ^ a function from the input message to a /stateless action/, cf 'continue_' - -> Dispatcher s -action h = handleDispatch perform - where perform :: (s -> a -> Process (ProcessAction s)) - perform s a = let f = h a in f s - --- | Constructs a handler for both /call/ and /cast/ messages. --- @handleDispatch = handleDispatchIf (const True)@ --- -handleDispatch :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleDispatch = handleDispatchIf $ input (const True) - --- | Constructs a handler for both /call/ and /cast/ messages. Messages are only --- dispatched to the handler if the supplied condition evaluates to @True@. --- -handleDispatchIf :: forall s a . (Serializable a) - => Condition s a - -> (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleDispatchIf cond handler = DispatchIf { - dispatch = doHandle handler - , dispatchIf = check cond - } - where doHandle :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s msg = - case msg of - (CallMessage p _) -> (h s p) - (CastMessage p) -> (h s p) - --- | Creates a generic input handler (i.e., for recieved messages that are /not/ --- sent using the 'cast' or 'call' APIs) from an ordinary function in the --- 'Process' monad. -handleInfo :: forall s a. (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> DeferredDispatcher s -handleInfo h = DeferredDispatcher { dispatchInfo = doHandleInfo h } - where - doHandleInfo :: forall s2 a2. (Serializable a2) - => (s2 -> a2 -> Process (ProcessAction s2)) - -> s2 - -> AbstractMessage - -> Process (Maybe (ProcessAction s2)) - doHandleInfo h' s msg = maybeHandleMessage msg (h' s) - --- | Creates an /exit handler/ scoped to the execution of any and all the --- registered call, cast and info handlers for the process. -handleExit :: forall s a. (Serializable a) - => (s -> ProcessId -> a -> Process (ProcessAction s)) - -> ExitSignalDispatcher s -handleExit h = ExitSignalDispatcher { dispatchExit = doHandleExit h } - where - doHandleExit :: (s -> ProcessId -> a -> Process (ProcessAction s)) - -> s - -> ProcessId - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) - doHandleExit h' s p msg = maybeHandleMessage msg (h' s p) - --- handling 'reply-to' in the main process loop is awkward at best, --- so we handle it here instead and return the 'action' to the loop -mkReply :: (Serializable b) - => Recipient -> ProcessReply s b -> Process (ProcessAction s) -mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a -mkReply _ (NoReply a) = return a - --- these functions are the inverse of 'condition', 'state' and 'input' - -check :: forall s m . (Serializable m) - => Condition s m - -> s - -> Message m - -> Bool -check (Condition c) st msg = c st $ decode msg -check (State c) st _ = c st -check (Input c) _ msg = c $ decode msg - -checkCall :: forall s m . (Serializable m) - => Condition s m - -> s - -> Message m - -> Bool -checkCall cond st msg@(CallMessage _ _) = check cond st msg -checkCall _ _ _ = False - -checkCast :: forall s m . (Serializable m) - => Condition s m - -> s - -> Message m - -> Bool -checkCast cond st msg@(CastMessage _) = check cond st msg -checkCast _ _ _ = False - -decode :: Message a -> a -decode (CallMessage a _) = a -decode (CastMessage a) = a - --------------------------------------------------------------------------------- --- Internal Process Implementation -- --------------------------------------------------------------------------------- - -recvLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason -recvLoop pDef pState recvDelay = - let p = unhandledMessagePolicy pDef - handleTimeout = timeoutHandler pDef - handleStop = terminateHandler pDef - shutdown' = matchMessage p pState shutdownHandler - matchers = map (matchMessage p pState) (apiHandlers pDef) - ex' = (exitHandlers pDef) ++ [trapExit] - ms' = (shutdown':matchers) ++ matchAux p pState (infoHandlers pDef) - in do - ac <- catchesExit (processReceive ms' handleTimeout pState recvDelay) - (map (\d' -> (dispatchExit d') pState) ex') - case ac of - (ProcessContinue s') -> recvLoop pDef s' recvDelay - (ProcessTimeout t' s') -> recvLoop pDef s' (Delay t') - (ProcessHibernate d' s') -> block d' >> recvLoop pDef s' recvDelay - (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) - --- an explicit 'cast' giving 'Shutdown' will stop the server gracefully -shutdownHandler :: Dispatcher s -shutdownHandler = handleCast (\_ Shutdown -> stop $ TerminateShutdown) - --- @(ProcessExitException from Shutdown)@ will stop the server gracefully -trapExit :: ExitSignalDispatcher s -trapExit = handleExit (\_ (_ :: ProcessId) Shutdown -> stop $ TerminateShutdown) - -block :: TimeInterval -> Process () -block i = liftIO $ threadDelay (asTimeout i) - -applyPolicy :: UnhandledMessagePolicy - -> s - -> AbstractMessage - -> Process (ProcessAction s) -applyPolicy p s m = - case p of - Terminate -> stop $ TerminateOther "UnhandledInput" - DeadLetter pid -> forward m pid >> continue s - Drop -> continue s - -matchAux :: UnhandledMessagePolicy - -> s - -> [DeferredDispatcher s] - -> [Match (ProcessAction s)] -matchAux p ps ds = [matchAny (auxHandler (applyPolicy p ps) ps ds)] - -auxHandler :: (AbstractMessage -> Process (ProcessAction s)) - -> s - -> [DeferredDispatcher s] - -> AbstractMessage - -> Process (ProcessAction s) -auxHandler policy _ [] msg = policy msg -auxHandler policy st (d:ds :: [DeferredDispatcher s]) msg - | length ds > 0 = let dh = dispatchInfo d in do - -- NB: we *do not* want to terminate/dead-letter messages until - -- we've exhausted all the possible info handlers - m <- dh st msg - case m of - Nothing -> auxHandler policy st ds msg - Just act -> return act - -- but here we *do* let the policy kick in - | otherwise = let dh = dispatchInfo d in do - m <- dh st msg - case m of - Nothing -> policy msg - Just act -> return act - -processReceive :: [Match (ProcessAction s)] - -> TimeoutHandler s - -> s - -> Delay - -> Process (ProcessAction s) -processReceive ms handleTimeout st d = do - next <- recv ms d - case next of - Nothing -> handleTimeout st d - Just pa -> return pa - where - recv :: [Match (ProcessAction s)] - -> Delay - -> Process (Maybe (ProcessAction s)) - recv matches d' = - case d' of - Infinity -> receiveWait matches >>= return . Just - Delay t' -> receiveTimeout (asTimeout t') matches - diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs deleted file mode 100644 index 983a4d4e..00000000 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ /dev/null @@ -1,351 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Second iteration of GenServer -module Control.Distributed.Process.Platform.GenServer ( - ServerId, - initOk, - initStop, - ok, - forward, - stop, - InitHandler, - Handler, - TerminateHandler, - MessageDispatcher(), - handle, - handleIf, - handleAny, - putState, - getState, - modifyState, - LocalServer(..), - defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - call, - callTimeout, - wait, - waitTimeout, - Process, - trace - ) where - -import qualified Control.Distributed.Process as P (forward, catch) -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) - -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - monitor, link, - exit, getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal) - -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Process.Platform.Time -import Control.Distributed.Process.Platform.Async.AsyncChan - -import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) -import Data.DeriveTH -import Data.Typeable (Typeable) - --------------------------------------------------------------------------------- --- Data Types -- --------------------------------------------------------------------------------- - --- | ServerId -type ServerId = ProcessId - --- | Server monad -newtype Server s a = Server { - unServer :: ST.StateT s Process a - } - deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) - --- | Initialize handler result -data InitResult - = InitOk Delay - | InitStop String - -initOk :: Delay -> Server s InitResult -initOk t = return (InitOk t) - -initStop :: String -> Server s InitResult -initStop reason = return (InitStop reason) - --- | Terminate reason -data TerminateReason - = TerminateNormal - | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - --- | The result of a call -data Result a - = Ok a - | Forward ServerId - | Stop a String - deriving (Show, Typeable) - -ok :: (Serializable a, Show a) => a -> Server s (Result a) -ok resp = return (Ok resp) - -forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) -forward sid = return (Forward sid) - -stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) -stop resp reason = return (Stop resp reason) - --- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type Handler s a b = a -> Server s (Result b) - --- | Adds routing metadata to the actual payload -data Message a = - CallMessage { msgFrom :: ProcessId, msgPayload :: a } - | CastMessage { msgFrom :: ProcessId, msgPayload :: a } - deriving (Show, Typeable) -$(derive makeBinary ''Message) - --- | Dispatcher that knows how to dispatch messages to a handler --- s The server state -data MessageDispatcher s = - forall a . (Serializable a) => MessageDispatcher { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) - } - | forall a . (Serializable a) => MessageDispatcherIf { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason), - dispatchIf :: s -> Message a -> Bool - } - | MessageDispatcherAny { - dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) - } - --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - --- | Matches messages to a MessageDispatcher -instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher d) = match (d s) - matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) - matchMessage s (MessageDispatcherAny d) = matchAny (d s) - --- | Constructs a call message dispatcher --- -handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s -handle = handleIf (const True) - -handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s -handleIf cond handler = MessageDispatcherIf { - dispatcher = (\s msg -> case msg of - CallMessage cid payload -> do - --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Ok resp -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Nothing) - Forward sid -> do - --say $ "Server FORWARD to: " ++ show sid - send sid msg - return (s', Nothing) - Stop resp reason -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do - --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - send sid msg - return (s', Nothing) - ), - dispatchIf = \_ msg -> cond (msgPayload msg) -} - --- | Constructs a dispatcher for any message --- Note that since we don't know the type of this message it assumes the protocol of a cast --- i.e. no reply's -handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s -handleAny handler = MessageDispatcherAny { - dispatcherAny = (\s m -> do - (r, s') <- runServer (handler m) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - (P.forward m) sid - return (s', Nothing) - ) -} - --- | The server callbacks -data LocalServer s = LocalServer { - initHandler :: InitHandler s, -- ^ initialization handler - handlers :: [MessageDispatcher s], - terminateHandler :: TerminateHandler s -- ^ termination handler - } - ----- | Default record ----- Starting point for creating new servers -defaultServer :: LocalServer s -defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - handlers = [], - terminateHandler = \_ -> return () -} - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc - where - proc = processServer initH terminateH hs s - initH = initHandler ls - terminateH = terminateHandler ls - hs = handlers ls - --- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls - link pid - return pid - --- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls - ref <- monitor pid - return (pid, ref) - --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Delay -> rq -> Process (Maybe rs) -callTimeout sid t rq = undefined - --- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do - cid <- getSelfPid - --say $ "Casting server " ++ show cid - send sid (CastMessage cid msg) - --- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do - --say $ "Stop server " ++ show sid - exit sid reason - --- | Get the server state -getState :: Server s s -getState = ST.get - --- | Put the server state -putState :: s -> Server s () -putState = ST.put - --- | Modify the server state -modifyState :: (s -> s) -> Server s () -modifyState = ST.modify - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- | server process -processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () -processServer initH terminateH dispatchers s = do - (ir, s') <- runServer initH s - P.catch (proc ir s') (exitHandler s') - where - proc ir s' = do - (tr, s'') <- runServer (processLoop dispatchers ir) s' - _ <- runServer (terminateH tr) s'' - return () - exitHandler s' e = do - let tr = TerminateReason $ show (e :: SomeException) - _ <- runServer (terminateH tr) s' - return () - --- | server loop -processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason -processLoop dispatchers ir = do - case ir of - InitOk t -> loop dispatchers t - InitStop r -> return $ TerminateReason r - where - loop ds t = do - msgM <- processReceive ds t - case msgM of - Nothing -> loop ds t - Just r -> return r - --- | -processReceive :: [MessageDispatcher s] -> Delay -> Server s (Maybe TerminateReason) -processReceive ds t = do - s <- getState - let ms = map (matchMessage s) ds - case t of - Infinity -> do - (s', r) <- lift $ receiveWait ms - putState s' - return r - Delay t' -> do - mayResult <- lift $ receiveTimeout (asTimeout t') ms - case mayResult of - Just (s', r) -> do - putState s' - return r - Nothing -> do - --trace "Receive timed out ..." - return $ Just (TerminateReason "Receive timed out") - --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = lift . say $ msg - --- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a -lift :: Process a -> Server s a -lift p = Server $ ST.lift p - --- | -runServer :: Server s a -> s -> Process (a, s) -runServer server state = ST.runStateT (unServer server) state diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 32453238..1c9f4cbb 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -14,7 +14,7 @@ import Control.Distributed.Process hiding (call) import Control.Distributed.Process.Closure() import Control.Distributed.Process.Platform import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable import Control.Exception hiding (catch) diff --git a/tests/TestGenServer.hs b/tests/TestGenServer.hs index 774e8de3..912e744a 100644 --- a/tests/TestGenServer.hs +++ b/tests/TestGenServer.hs @@ -15,7 +15,7 @@ import Control.Distributed.Process.Closure import Control.Distributed.Process.Node import Control.Distributed.Process.Platform hiding (__remoteTable) import Control.Distributed.Process.Platform.Async -import Control.Distributed.Process.Platform.GenProcess +import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Test import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Platform.Timer From 04ef4e2bb1943ca26bca5ce60d509d26a95d2c95 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 27 Jan 2013 18:48:53 +0000 Subject: [PATCH 0896/2357] Split up GenProcess into a set of specific APIs --- distributed-process-platform.cabal | 47 +- .../Process/Platform/GenProcess.hs | 998 ------------------ .../Distributed/Process/Platform/GenServer.hs | 351 ------ 3 files changed, 34 insertions(+), 1362 deletions(-) delete mode 100644 src/Control/Distributed/Process/Platform/GenProcess.hs delete mode 100644 src/Control/Distributed/Process/Platform/GenServer.hs diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index d0a433f8..38e7dfb9 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -38,20 +38,24 @@ library ghc-options: -Wall exposed-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer + Control.Distributed.Process.Platform.Timer, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server other-modules: Control.Distributed.Process.Platform.Internal.Primitives, Control.Distributed.Process.Platform.Internal.Types, Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types + Control.Distributed.Process.Platform.Async.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite TimerTests type: exitcode-stdio-1.0 @@ -78,6 +82,11 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs + other-modules: + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -104,7 +113,11 @@ test-suite PrimitivesTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -132,7 +145,11 @@ test-suite AsyncTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: TestUtils, - TestGenServer + TestGenServer, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -160,11 +177,10 @@ test-suite GenServerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind other-modules: Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async - Control.Distributed.Process.Platform.Async.AsyncChan - Control.Distributed.Process.Platform.Async.AsyncSTM + Control.Distributed.Process.Platform.Async, + Control.Distributed.Process.Platform.Async.AsyncChan, + Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenProcess, Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, @@ -176,7 +192,12 @@ test-suite GenServerTests TestUtils, MathsDemo, Counter, - SimplePool + SimplePool, + Control.Distributed.Process.Platform.ManagedProcess, + Control.Distributed.Process.Platform.ManagedProcess.Client, + Control.Distributed.Process.Platform.ManagedProcess.Server, + Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, + Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs diff --git a/src/Control/Distributed/Process/Platform/GenProcess.hs b/src/Control/Distributed/Process/Platform/GenProcess.hs deleted file mode 100644 index 91c97898..00000000 --- a/src/Control/Distributed/Process/Platform/GenProcess.hs +++ /dev/null @@ -1,998 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} - ------------------------------------------------------------------------------ --- | --- Module : Control.Distributed.Process.Platform.GenProcess --- Copyright : (c) Tim Watson 2012 --- License : BSD3 (see the file LICENSE) --- --- Maintainer : Tim Watson --- Stability : experimental --- Portability : non-portable (requires concurrency) --- --- This module provides a high(er) level API for building complex 'Process' --- implementations by abstracting out the management of the process' mailbox, --- reply/response handling, timeouts, process hiberation, error handling --- and shutdown/stop procedures. It is modelled along similar lines to OTP's --- gen_server API - . --- --- [API Overview] --- --- Once started, a generic process will consume messages from its mailbox and --- pass them on to user defined /handlers/ based on the types received (mapped --- to those accepted by the handlers) and optionally by also evaluating user --- supplied predicates to determine which handlers are valid. --- Each handler returns a 'ProcessAction' which specifies how we should proceed. --- If none of the handlers is able to process a message (because their types are --- incompatible) then the process 'unhandledMessagePolicy' will be applied. --- --- The 'ProcessAction' type defines the ways in which a process can respond --- to its inputs, either by continuing to read incoming messages, setting an --- optional timeout, sleeping for a while or by stopping. The optional timeout --- behaves a little differently to the other process actions. If no messages --- are received within the specified time span, the process 'timeoutHandler' --- will be called in order to determine the next action. --- --- Generic processes are defined by the 'ProcessDefinition' type, using record --- syntax. The 'ProcessDefinition' fields contain handlers (or lists of them) --- for specific tasks. In addtion to the @timeoutHandler@, a 'ProcessDefinition' --- may also define a @terminateHandler@ which is called just before the process --- exits. This handler will be called /whenever/ the process is stopping, i.e., --- when a callback returns 'stop' as the next action /or/ if an unhandled exit --- signal or similar asynchronous exception is thrown in (or to) the process --- itself. --- --- The other handlers are split into two groups: /apiHandlers/ and /infoHandlers/. --- The former contains handlers for the 'cast' and 'call' protocols, whilst the --- latter contains handlers that deal with input messages which are not sent --- via these API calls (i.e., messages sent using bare 'send' or signals put --- into the process mailbox by the node controller, such as --- 'ProcessMonitorNotification' and the like). --- --- [The Cast/Call Protocol] --- --- Deliberate interactions with the process will usually fall into one of two --- categories. A 'cast' interaction involves a client sending a message --- asynchronously and the server handling this input. No reply is sent to --- the client. On the other hand, a 'call' interaction is a kind of /rpc/ --- where the client sends a message and waits for a reply. --- --- The expressions given to @apiHandlers@ have to conform to the /cast|call/ --- protocol. The details of this are, however, hidden from the user. A set --- of API functions for creating @apiHandlers@ are given instead, which --- take expressions (i.e., a function or lambda expression) and create the --- appropriate @Dispatcher@ for handling the cast (or call). --- --- The cast/call protocol handlers deal with /expected/ inputs. These form --- the explicit public API for the process, and will usually be exposed by --- providing module level functions that defer to the cast/call API. For --- example: --- --- @ --- add :: ProcessId -> Double -> Double -> Double --- add pid x y = call pid (Add x y) --- @ --- --- [Handling Info Messages] --- --- An explicit protocol for communicating with the process can be --- configured using 'cast' and 'call', but it is not possible to prevent --- other kinds of messages from being sent to the process mailbox. When --- any message arrives for which there are no handlers able to process --- its content, the 'UnhandledMessagePolicy' will be applied. Sometimes --- it is desireable to process incoming messages which aren't part of the --- protocol, rather than let the policy deal with them. This is particularly --- true when incoming messages are important to the process, but their point --- of origin is outside the developer's control. Handling /signals/ such as --- 'ProcessMonitorNotification' is a typical example of this: --- --- > handleInfo_ (\(ProcessMonitorNotification _ _ r) -> say $ show r >> continue_) --- --- [Handling Process State] --- --- The 'ProcessDefinition' is parameterised by the type of state it maintains. --- A process that has no state will have the type @ProcessDefinition ()@ and can --- be bootstrapped by evaluating 'statelessProcess'. --- --- All call/cast handlers come in two flavours, those which take the process --- state as an input and those which do not. Handlers that ignore the process --- state have to return a function that takes the state and returns the required --- action. Versions of the various action generating functions ending in an --- underscore are provided to simplify this: --- --- @ --- statelessProcess { --- apiHandlers = [ --- handleCall_ (\\(n :: Int) -> return (n * 2)) --- , handleCastIf_ (\\(c :: String, _ :: Delay) -> c == \"timeout\") --- (\\(\"timeout\", Delay d) -> timeoutAfter_ d) --- ] --- , timeoutHandler = \\_ _ -> stop $ TerminateOther \"timeout\" --- } --- @ --- --- [Handling Errors] --- --- Error handling appears in several contexts and process definitions can --- hook into these with relative ease. Only process failures as a result of --- asynchronous exceptions are supported by the API, which provides several --- scopes for error handling. --- --- Catching exceptions inside handler functions is no different to ordinary --- exception handling in monadic code. --- --- @ --- handleCall (\\x y -> --- catch (hereBeDragons x y) --- (\\(e :: SmaugTheTerribleException) -> --- return (Left (show e)))) --- @ --- --- The caveats mentioned in "Control.Distributed.Process.Platform" about --- exit signal handling obviously apply here as well. --- --- [Structured Exit Signal Handling] --- --- Because "Control.Distributed.Process.ProcessExitException" is a ubiquitous --- /signalling mechanism/ in Cloud Haskell, it is treated unlike other --- asynchronous exceptions. The 'ProcessDefinition' 'exitHandlers' field --- accepts a list of handlers that, for a specific exit reason, can decide --- how the process should respond. If none of these handlers matches the --- type of @reason@ then the process will exit with @DiedException why@. In --- addition, a default /exit handler/ is installed for exit signals where the --- @reason == Shutdown@, because this is an /exit signal/ used explicitly and --- extensively throughout the platform. The default behaviour is to gracefully --- shut down the process, calling the @terminateHandler@ as usual, before --- stopping with @TerminateShutdown@ given as the final outcome. --- --- /Example: How to annoy your supervisor and end up force-killed:/ --- --- > handleExit (\state from (sigExit :: Shutdown) -> continue s) --- --- That code is, of course, very silly. Under some circumstances, handling --- exit signals is perfectly legitimate. Handling of /other/ forms of --- asynchronous exception is not supported by this API. --- --- If any asynchronous exception goes unhandled, the process will immediately --- exit without running the @terminateHandler@. It is very important to note --- that in Cloud Haskell, link failures generate asynchronous exceptions in --- the target and these will NOT be caught by the API and will therefore --- cause the process to exit /without running the termination handler/ --- callback. If your termination handler is set up to do important work --- (such as resource cleanup) then you should avoid linking you process --- and use monitors instead. ------------------------------------------------------------------------------ - -module Control.Distributed.Process.Platform.GenProcess - ( -- * Exported data types - InitResult(..) - , ProcessAction(..) - , ProcessReply - , CallHandler - , CastHandler - , InitHandler - , TerminateHandler - , TimeoutHandler - , UnhandledMessagePolicy(..) - , ProcessDefinition(..) - -- * Client interaction with the process - , start - , runProcess - , shutdown - , defaultProcess - , statelessProcess - , statelessInit - , call - , safeCall - , tryCall - , callAsync - , callTimeout - , cast - -- * Handler interaction inside the process - , condition - , state - , input - , reply - , replyWith - , noReply - , noReply_ - , haltNoReply_ - , continue - , continue_ - , timeoutAfter - , timeoutAfter_ - , hibernate - , hibernate_ - , stop - , stop_ - , replyTo - -- * Handler callback creation - , handleCall - , handleCallIf - , handleCallFrom - , handleCallFromIf - , handleCast - , handleCastIf - , handleInfo - , handleDispatch - , handleExit - -- * Stateless handlers - , action - , handleCall_ - , handleCallIf_ - , handleCast_ - , handleCastIf_ - ) where - -import Control.Concurrent (threadDelay) -import Control.Distributed.Process hiding (call) -import Control.Distributed.Process.Serializable -import Control.Distributed.Process.Platform.Async hiding (check) -import Control.Distributed.Process.Platform.Internal.Primitives -import Control.Distributed.Process.Platform.Internal.Types - ( Recipient(..) - , TerminateReason(..) - , Shutdown(..) - ) -import Control.Distributed.Process.Platform.Internal.Common -import Control.Distributed.Process.Platform.Time - -import Data.Binary hiding (decode) -import Data.DeriveTH -import Data.Typeable (Typeable) -import Prelude hiding (init) - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - -data Message a = - CastMessage a - | CallMessage a Recipient - deriving (Typeable) -$(derive makeBinary ''Message) - -data CallResponse a = CallResponse a - deriving (Typeable) -$(derive makeBinary ''CallResponse) - --- | Return type for and 'InitHandler' expression. -data InitResult s = - InitOk s Delay {- - ^ denotes successful initialisation, initial state and timeout -} - | forall r. (Serializable r) - => InitFail r -- ^ denotes failed initialisation and the reason - --- | The action taken by a process after a handler has run and its updated state. --- See 'continue' --- 'timeoutAfter' --- 'hibernate' --- 'stop' --- -data ProcessAction s = - ProcessContinue s -- ^ continue with (possibly new) state - | ProcessTimeout TimeInterval s -- ^ timeout if no messages are received - | ProcessHibernate TimeInterval s -- ^ hibernate for /delay/ - | ProcessStop TerminateReason -- ^ stop the process, giving @TerminateReason@ - --- | Returned from handlers for the synchronous 'call' protocol, encapsulates --- the reply data /and/ the action to take after sending the reply. A handler --- can return @NoReply@ if they wish to ignore the call. -data ProcessReply s a = - ProcessReply a (ProcessAction s) - | NoReply (ProcessAction s) - -type CallHandler a s = s -> a -> Process (ProcessReply s a) - -type CastHandler s = s -> Process () - --- type InfoHandler a = forall a b. (Serializable a, Serializable b) => a -> Process b - --- | Wraps a predicate that is used to determine whether or not a handler --- is valid based on some combination of the current process state, the --- type and/or value of the input message or both. -data Condition s m = - Condition (s -> m -> Bool) -- ^ predicated on the process state /and/ the message - | State (s -> Bool) -- ^ predicated on the process state only - | Input (m -> Bool) -- ^ predicated on the input message only - --- | An expression used to initialise a process with its state. -type InitHandler a s = a -> Process (InitResult s) - --- | An expression used to handle process termination. -type TerminateHandler s = s -> TerminateReason -> Process () - --- | An expression used to handle process timeouts. -type TimeoutHandler s = s -> Delay -> Process (ProcessAction s) - --- dispatching to implementation callbacks - --- | Provides dispatch from cast and call messages to a typed handler. -data Dispatcher s = - forall a . (Serializable a) => Dispatch { - dispatch :: s -> Message a -> Process (ProcessAction s) - } - | forall a . (Serializable a) => DispatchIf { - dispatch :: s -> Message a -> Process (ProcessAction s) - , dispatchIf :: s -> Message a -> Bool - } - --- | Provides dispatch for any input, returns 'Nothing' for unhandled messages. -data DeferredDispatcher s = DeferredDispatcher { - dispatchInfo :: s - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) - } - --- | Provides dispatch for any exit signal - returns 'Nothing' for unhandled exceptions -data ExitSignalDispatcher s = ExitSignalDispatcher { - dispatchExit :: s - -> ProcessId - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) - } - -class MessageMatcher d where - matchMessage :: UnhandledMessagePolicy -> s -> d s -> Match (ProcessAction s) - -instance MessageMatcher Dispatcher where - matchMessage _ s (Dispatch d) = match (d s) - matchMessage _ s (DispatchIf d cond) = matchIf (cond s) (d s) - --- | Policy for handling unexpected messages, i.e., messages which are not --- sent using the 'call' or 'cast' APIs, and which are not handled by any of the --- 'handleInfo' handlers. -data UnhandledMessagePolicy = - Terminate -- ^ stop immediately, giving @TerminateOther "UnhandledInput"@ as the reason - | DeadLetter ProcessId -- ^ forward the message to the given recipient - | Drop -- ^ dequeue and then drop/ignore the message - --- | Stores the functions that determine runtime behaviour in response to --- incoming messages and a policy for responding to unhandled messages. -data ProcessDefinition s = ProcessDefinition { - apiHandlers :: [Dispatcher s] -- ^ functions that handle call/cast messages - , infoHandlers :: [DeferredDispatcher s] -- ^ functions that handle non call/cast messages - , exitHandlers :: [ExitSignalDispatcher s] -- ^ functions that handle exit signals - , timeoutHandler :: TimeoutHandler s -- ^ a function that handles timeouts - , terminateHandler :: TerminateHandler s -- ^ a function that is run just before the process exits - , unhandledMessagePolicy :: UnhandledMessagePolicy -- ^ how to deal with unhandled messages - } - --------------------------------------------------------------------------------- --- Client API -- --------------------------------------------------------------------------------- - --- TODO: automatic registration - --- | Starts a gen-process configured with the supplied process definition, --- using an init handler and its initial arguments. This code will run the --- 'Process' until completion and return @Right TerminateReason@ *or*, --- if initialisation fails, return @Left InitResult@ which will be --- @InitFail why@. -start :: a - -> InitHandler a s - -> ProcessDefinition s - -> Process (Either (InitResult s) TerminateReason) -start = runProcess recvLoop - -runProcess :: (ProcessDefinition s -> s -> Delay -> Process TerminateReason) - -> a - -> InitHandler a s - -> ProcessDefinition s - -> Process (Either (InitResult s) TerminateReason) -runProcess loop args init def = do - ir <- init args - case ir of - InitOk s d -> loop def s d >>= return . Right - f@(InitFail _) -> return $ Left f - --- | Send a signal instructing the process to terminate. The /receive loop/ which --- manages the process mailbox will prioritise @Shutdown@ signals higher than --- any other incoming messages, but the server might be busy (i.e., still in the --- process of excuting a handler) at the time of sending however, so the caller --- should not make any assumptions about the timeliness with which the shutdown --- signal will be handled. If responsiveness is important, a better approach --- might be to send an /exit signal/ with 'Shutdown' as the reason. An exit --- signal will interrupt any operation currently underway and force the running --- process to clean up and terminate. -shutdown :: ProcessId -> Process () -shutdown pid = cast pid Shutdown - -defaultProcess :: ProcessDefinition s -defaultProcess = ProcessDefinition { - apiHandlers = [] - , infoHandlers = [] - , exitHandlers = [] - , timeoutHandler = \s _ -> continue s - , terminateHandler = \_ _ -> return () - , unhandledMessagePolicy = Terminate - } :: ProcessDefinition s - --- | A basic, stateless process definition, where the unhandled message policy --- is set to 'Terminate', the default timeout handlers does nothing (i.e., the --- same as calling @continue ()@ and the terminate handler is a no-op. -statelessProcess :: ProcessDefinition () -statelessProcess = ProcessDefinition { - apiHandlers = [] - , infoHandlers = [] - , exitHandlers = [] - , timeoutHandler = \s _ -> continue s - , terminateHandler = \_ _ -> return () - , unhandledMessagePolicy = Terminate - } - --- | A basic, state /unaware/ 'InitHandler' that can be used with --- 'statelessProcess'. -statelessInit :: Delay -> InitHandler () () -statelessInit d () = return $ InitOk () d - --- | Make a synchronous call - will block until a reply is received. --- The calling process will exit with 'TerminateReason' if the calls fails. -call :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process b -call sid msg = callAsync sid msg >>= wait >>= unpack -- note [call using async] - where unpack :: AsyncResult b -> Process b - unpack (AsyncDone r) = return r - unpack (AsyncFailed r) = die $ explain "CallFailed" r - unpack (AsyncLinkFailed r) = die $ explain "LinkFailed" r - unpack AsyncCancelled = die $ TerminateOther $ "Cancelled" - unpack AsyncPending = terminate -- as this *cannot* happen - --- | Safe version of 'call' that returns information about the error --- if the operation fails. If an error occurs then the explanation will be --- will be stashed away as @(TerminateOther String)@. -safeCall :: forall a b . (Serializable a, Serializable b) - => ProcessId -> a -> Process (Either TerminateReason b) -safeCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] - where unpack (AsyncDone r) = return $ Right r - unpack (AsyncFailed r) = return $ Left $ explain "CallFailed" r - unpack (AsyncLinkFailed r) = return $ Left $ explain "LinkFailed" r - unpack AsyncCancelled = return $ Left $ TerminateOther $ "Cancelled" - unpack AsyncPending = return $ Left $ TerminateOther $ "Pending" - --- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If --- you need information about *why* a call has failed then you should use --- 'safeCall' or combine @catchExit@ and @call@ instead. -tryCall :: forall s a b . (Addressable s, Serializable a, Serializable b) - => s -> a -> Process (Maybe b) -tryCall s m = callAsync s m >>= wait >>= unpack -- note [call using async] - where unpack (AsyncDone r) = return $ Just r - unpack _ = return Nothing - --- | Make a synchronous call, but timeout and return @Nothing@ if the reply --- is not received within the specified time interval. --- --- If the result of the call is a failure (or the call was cancelled) then --- the calling process will exit, with the 'AsyncResult' given as the reason. --- -callTimeout :: forall s a b . (Addressable s, Serializable a, Serializable b) - => s -> a -> TimeInterval -> Process (Maybe b) -callTimeout s m d = callAsync s m >>= waitTimeout d >>= unpack - where unpack :: (Serializable b) => Maybe (AsyncResult b) -> Process (Maybe b) - unpack Nothing = return Nothing - unpack (Just (AsyncDone r)) = return $ Just r - unpack (Just other) = die other - --- | Performs a synchronous 'call' to the the given server address, however the --- call is made /out of band/ and an async handle is returned immediately. This --- can be passed to functions in the /Async/ API in order to obtain the result. --- --- See "Control.Distributed.Process.Platform.Async" --- -callAsync :: forall s a b . (Addressable s, Serializable a, Serializable b) - => s -> a -> Process (Async b) -callAsync = callAsyncUsing async - --- | As 'callAsync' but takes a function that can be used to generate an async --- task and return an async handle to it. This can be used to switch between --- async implementations, by e.g., using an async channel instead of the default --- STM based handle. --- --- See "Control.Distributed.Process.Platform.Async" --- -callAsyncUsing :: forall s a b . (Addressable s, Serializable a, Serializable b) - => (Process b -> Process (Async b)) - -> s -> a -> Process (Async b) -callAsyncUsing asyncStart sid msg = do - asyncStart $ do -- note [call using async] - (Just pid) <- resolve sid - mRef <- monitor pid - wpid <- getSelfPid - sendTo sid (CallMessage msg (Pid wpid)) - r <- receiveWait [ - match (\((CallResponse m) :: CallResponse b) -> return (Right m)) - , matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef) - (\(ProcessMonitorNotification _ _ reason) -> return (Left reason)) - ] - -- TODO: better failure API - unmonitor mRef - case r of - Right m -> return m - Left err -> die $ TerminateOther ("ServerExit (" ++ (show err) ++ ")") - --- note [call using async] --- One problem with using plain expect/receive primitives to perform a --- synchronous (round trip) call is that a reply matching the expected type --- could come from anywhere! The Call.hs module uses a unique integer tag to --- distinguish between inputs but this is easy to forge, as is tagging the --- response with the sender's pid. --- --- The approach we take here is to rely on AsyncSTM (by default) to insulate us --- from erroneous incoming messages without the need for tagging. The /handle/ --- returned uses an @STM (AsyncResult a)@ field to handle the response /and/ --- the implementation spawns a new process to perform the actual call and --- await the reply before atomically updating the result. Whilst in theory, --- given a hypothetical 'listAllProcesses' primitive, it might be possible for --- malacious code to obtain the ProcessId of the worker and send a false reply, --- the likelihood of this is small enough that it seems reasonable to assume --- we've solved the problem without the need for tags or globally unique --- identifiers. - --- | Sends a /cast/ message to the server identified by 'ServerId'. The server --- will not send a response. Like Cloud Haskell's 'send' primitive, cast is --- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent --- (e.g., dead) server process will not generate an error. -cast :: forall a m . (Addressable a, Serializable m) - => a -> m -> Process () -cast sid msg = sendTo sid (CastMessage msg) - --------------------------------------------------------------------------------- --- Producing ProcessAction and ProcessReply from inside handler expressions -- --------------------------------------------------------------------------------- - --- | Creates a 'Conditon' from a function that takes a process state @a@ and --- an input message @b@ and returns a 'Bool' indicating whether the associated --- handler should run. --- -condition :: forall a b. (Serializable a, Serializable b) - => (a -> b -> Bool) - -> Condition a b -condition = Condition - --- | Create a 'Condition' from a function that takes a process state @a@ and --- returns a 'Bool' indicating whether the associated handler should run. --- -state :: forall s m. (Serializable m) => (s -> Bool) -> Condition s m -state = State - --- | Creates a 'Condition' from a function that takes an input message @m@ and --- returns a 'Bool' indicating whether the associated handler should run. --- -input :: forall s m. (Serializable m) => (m -> Bool) -> Condition s m -input = Input - --- | Instructs the process to send a reply and continue running. -reply :: (Serializable r) => r -> s -> Process (ProcessReply s r) -reply r s = continue s >>= replyWith r - --- | Instructs the process to send a reply /and/ evaluate the 'ProcessAction'. -replyWith :: (Serializable m) - => m - -> ProcessAction s - -> Process (ProcessReply s m) -replyWith msg st = return $ ProcessReply msg st - --- | Instructs the process to skip sending a reply /and/ evaluate a 'ProcessAction' -noReply :: (Serializable r) => ProcessAction s -> Process (ProcessReply s r) -noReply = return . NoReply - --- | Continue without giving a reply to the caller - equivalent to 'continue', --- but usable in a callback passed to the 'handleCall' family of functions. -noReply_ :: forall s r . (Serializable r) => s -> Process (ProcessReply s r) -noReply_ s = continue s >>= noReply - --- | Halt process execution during a call handler, without paying any attention --- to the expected return type. -haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason) -haltNoReply_ r = stop r >>= noReply - --- | Instructs the process to continue running and receiving messages. -continue :: s -> Process (ProcessAction s) -continue = return . ProcessContinue - --- | Version of 'continue' that can be used in handlers that ignore process state. --- -continue_ :: (s -> Process (ProcessAction s)) -continue_ = return . ProcessContinue - --- | Instructs the process to wait for incoming messages until 'TimeInterval' --- is exceeded. If no messages are handled during this period, the /timeout/ --- handler will be called. Note that this alters the process timeout permanently --- such that the given @TimeInterval@ will remain in use until changed. -timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s) -timeoutAfter d s = return $ ProcessTimeout d s - --- | Version of 'timeoutAfter' that can be used in handlers that ignore process state. --- --- > action (\(TimeoutPlease duration) -> timeoutAfter_ duration) --- -timeoutAfter_ :: TimeInterval -> (s -> Process (ProcessAction s)) -timeoutAfter_ d = return . ProcessTimeout d - --- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note --- that no messages will be removed from the mailbox until after hibernation has --- ceased. This is equivalent to calling @threadDelay@. --- -hibernate :: TimeInterval -> s -> Process (ProcessAction s) -hibernate d s = return $ ProcessHibernate d s - --- | Version of 'hibernate' that can be used in handlers that ignore process state. --- --- > action (\(HibernatePlease delay) -> hibernate_ delay) --- -hibernate_ :: TimeInterval -> (s -> Process (ProcessAction s)) -hibernate_ d = return . ProcessHibernate d - --- | Instructs the process to terminate, giving the supplied reason. If a valid --- 'terminateHandler' is installed, it will be called with the 'TerminateReason' --- returned from this call, along with the process state. -stop :: TerminateReason -> Process (ProcessAction s) -stop r = return $ ProcessStop r - --- | Version of 'stop' that can be used in handlers that ignore process state. --- --- > action (\ClientError -> stop_ TerminateNormal) --- -stop_ :: TerminateReason -> (s -> Process (ProcessAction s)) -stop_ r _ = stop r - -replyTo :: (Serializable m) => Recipient -> m -> Process () -replyTo client msg = sendTo client (CallResponse msg) - --------------------------------------------------------------------------------- --- Wrapping handler expressions in Dispatcher and DeferredDispatcher -- --------------------------------------------------------------------------------- - --- | Constructs a 'call' handler from a function in the 'Process' monad. --- The handler expression returns the reply, and the action will be --- set to 'continue'. --- --- > handleCall_ = handleCallIf_ (const True) --- -handleCall_ :: (Serializable a, Serializable b) - => (a -> Process b) - -> Dispatcher s -handleCall_ = handleCallIf_ $ input (const True) - --- | Constructs a 'call' handler from an ordinary function in the 'Process' --- monad. This variant ignores the state argument present in 'handleCall' and --- 'handleCallIf' and is therefore useful in a stateless server. Messges are --- only dispatched to the handler if the supplied condition evaluates to @True@ --- --- See 'handleCall' -handleCallIf_ :: forall s a b . (Serializable a, Serializable b) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (a -> Process b) -- ^ a function from an input message to a reply - -> Dispatcher s -handleCallIf_ cond handler - = DispatchIf { - dispatch = doHandle handler - , dispatchIf = checkCall cond - } - where doHandle :: (Serializable a, Serializable b) - => (a -> Process b) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h p) >>= mkCallReply c s - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - - -- handling 'reply-to' in the main process loop is awkward at best, - -- so we handle it here instead and return the 'action' to the loop - mkCallReply :: (Serializable b) - => Recipient -> s -> b -> Process (ProcessAction s) - mkCallReply c s m = sendTo c (CallResponse m) >> continue s - --- | Constructs a 'call' handler from a function in the 'Process' monad. --- > handleCall = handleCallIf (const True) --- -handleCall :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) - -> Dispatcher s -handleCall = handleCallIf $ state (const True) - --- | Constructs a 'call' handler from an ordinary function in the 'Process' --- monad. Given a function @f :: (s -> a -> Process (ProcessReply s b))@, --- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. Messages are only --- dispatched to the handler if the supplied condition evaluates to @True@ --- -handleCallIf :: forall s a b . (Serializable a, Serializable b) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (s -> a -> Process (ProcessReply s b)) - -- ^ a reply yielding function over the process state and input message - -> Dispatcher s -handleCallIf cond handler - = DispatchIf { - dispatch = doHandle handler - , dispatchIf = checkCall cond - } - where doHandle :: (Serializable a, Serializable b) - => (s -> a -> Process (ProcessReply s b)) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h s p) >>= mkReply c - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - --- | As 'handleCall' but passes the 'Recipient' to the handler function. --- This can be useful if you wish to /reply later/ to the caller by, e.g., --- spawning a process to do some work and have it @replyTo caller response@ --- out of band. In this case the callback can pass the 'Recipient' to the --- worker (or stash it away itself) and return 'noReply'. --- -handleCallFrom :: forall s a b . (Serializable a, Serializable b) - => (s -> Recipient -> a -> Process (ProcessReply s b)) - -> Dispatcher s -handleCallFrom = handleCallFromIf $ state (const True) - --- | As 'handleCallFrom' but only runs the handler if the supplied 'Condition' --- evaluates to @True@. --- -handleCallFromIf :: forall s a b . (Serializable a, Serializable b) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (s -> Recipient -> a -> Process (ProcessReply s b)) - -- ^ a reply yielding function over the process state, sender and input message - -> Dispatcher s -handleCallFromIf cond handler - = DispatchIf { - dispatch = doHandle handler - , dispatchIf = checkCall cond - } - where doHandle :: (Serializable a, Serializable b) - => (s -> Recipient -> a -> Process (ProcessReply s b)) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s (CallMessage p c) = (h s c p) >>= mkReply c - doHandle _ _ _ = die "CALL_HANDLER_TYPE_MISMATCH" -- cannot happen! - --- | Constructs a 'cast' handler from an ordinary function in the 'Process' --- monad. --- > handleCast = handleCastIf (const True) --- -handleCast :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleCast = handleCastIf $ input (const True) - --- | Constructs a 'cast' handler from an ordinary function in the 'Process' --- monad. Given a function @f :: (s -> a -> Process (ProcessAction s))@, --- the expression @handleCall f@ will yield a 'Dispatcher' for inclusion --- in a 'Behaviour' specification for the /GenProcess/. --- -handleCastIf :: forall s a . (Serializable a) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (s -> a -> Process (ProcessAction s)) - -- ^ an action yielding function over the process state and input message - -> Dispatcher s -handleCastIf cond h - = DispatchIf { - dispatch = (\s (CastMessage p) -> h s p) - , dispatchIf = checkCast cond - } - --- | Version of 'handleCast' that ignores the server state. --- -handleCast_ :: (Serializable a) - => (a -> (s -> Process (ProcessAction s))) -> Dispatcher s -handleCast_ = handleCastIf_ $ input (const True) - --- | Version of 'handleCastIf' that ignores the server state. --- -handleCastIf_ :: forall s a . (Serializable a) - => Condition s a -- ^ predicate that must be satisfied for the handler to run - -> (a -> (s -> Process (ProcessAction s))) - -- ^ a function from the input message to a /stateless action/, cf 'continue_' - -> Dispatcher s -handleCastIf_ cond h - = DispatchIf { - dispatch = (\s (CastMessage p) -> h p $ s) - , dispatchIf = checkCast cond - } - --- | Constructs an /action/ handler. Like 'handleDispatch' this can handle both --- 'cast' and 'call' messages and you won't know which you're dealing with. --- This can be useful where certain inputs require a definite action, such as --- stopping the server, without concern for the state (e.g., when stopping we --- need only decide to stop, as the terminate handler can deal with state --- cleanup etc). For example: --- --- @action (\MyCriticalErrorSignal -> stop_ TerminateNormal)@ --- -action :: forall s a . (Serializable a) - => (a -> (s -> Process (ProcessAction s))) - -- ^ a function from the input message to a /stateless action/, cf 'continue_' - -> Dispatcher s -action h = handleDispatch perform - where perform :: (s -> a -> Process (ProcessAction s)) - perform s a = let f = h a in f s - --- | Constructs a handler for both /call/ and /cast/ messages. --- @handleDispatch = handleDispatchIf (const True)@ --- -handleDispatch :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleDispatch = handleDispatchIf $ input (const True) - --- | Constructs a handler for both /call/ and /cast/ messages. Messages are only --- dispatched to the handler if the supplied condition evaluates to @True@. --- -handleDispatchIf :: forall s a . (Serializable a) - => Condition s a - -> (s -> a -> Process (ProcessAction s)) - -> Dispatcher s -handleDispatchIf cond handler = DispatchIf { - dispatch = doHandle handler - , dispatchIf = check cond - } - where doHandle :: (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> s - -> Message a - -> Process (ProcessAction s) - doHandle h s msg = - case msg of - (CallMessage p _) -> (h s p) - (CastMessage p) -> (h s p) - --- | Creates a generic input handler (i.e., for recieved messages that are /not/ --- sent using the 'cast' or 'call' APIs) from an ordinary function in the --- 'Process' monad. -handleInfo :: forall s a. (Serializable a) - => (s -> a -> Process (ProcessAction s)) - -> DeferredDispatcher s -handleInfo h = DeferredDispatcher { dispatchInfo = doHandleInfo h } - where - doHandleInfo :: forall s2 a2. (Serializable a2) - => (s2 -> a2 -> Process (ProcessAction s2)) - -> s2 - -> AbstractMessage - -> Process (Maybe (ProcessAction s2)) - doHandleInfo h' s msg = maybeHandleMessage msg (h' s) - --- | Creates an /exit handler/ scoped to the execution of any and all the --- registered call, cast and info handlers for the process. -handleExit :: forall s a. (Serializable a) - => (s -> ProcessId -> a -> Process (ProcessAction s)) - -> ExitSignalDispatcher s -handleExit h = ExitSignalDispatcher { dispatchExit = doHandleExit h } - where - doHandleExit :: (s -> ProcessId -> a -> Process (ProcessAction s)) - -> s - -> ProcessId - -> AbstractMessage - -> Process (Maybe (ProcessAction s)) - doHandleExit h' s p msg = maybeHandleMessage msg (h' s p) - --- handling 'reply-to' in the main process loop is awkward at best, --- so we handle it here instead and return the 'action' to the loop -mkReply :: (Serializable b) - => Recipient -> ProcessReply s b -> Process (ProcessAction s) -mkReply c (ProcessReply r' a) = sendTo c (CallResponse r') >> return a -mkReply _ (NoReply a) = return a - --- these functions are the inverse of 'condition', 'state' and 'input' - -check :: forall s m . (Serializable m) - => Condition s m - -> s - -> Message m - -> Bool -check (Condition c) st msg = c st $ decode msg -check (State c) st _ = c st -check (Input c) _ msg = c $ decode msg - -checkCall :: forall s m . (Serializable m) - => Condition s m - -> s - -> Message m - -> Bool -checkCall cond st msg@(CallMessage _ _) = check cond st msg -checkCall _ _ _ = False - -checkCast :: forall s m . (Serializable m) - => Condition s m - -> s - -> Message m - -> Bool -checkCast cond st msg@(CastMessage _) = check cond st msg -checkCast _ _ _ = False - -decode :: Message a -> a -decode (CallMessage a _) = a -decode (CastMessage a) = a - --------------------------------------------------------------------------------- --- Internal Process Implementation -- --------------------------------------------------------------------------------- - -recvLoop :: ProcessDefinition s -> s -> Delay -> Process TerminateReason -recvLoop pDef pState recvDelay = - let p = unhandledMessagePolicy pDef - handleTimeout = timeoutHandler pDef - handleStop = terminateHandler pDef - shutdown' = matchMessage p pState shutdownHandler - matchers = map (matchMessage p pState) (apiHandlers pDef) - ex' = (exitHandlers pDef) ++ [trapExit] - ms' = (shutdown':matchers) ++ matchAux p pState (infoHandlers pDef) - in do - ac <- catchesExit (processReceive ms' handleTimeout pState recvDelay) - (map (\d' -> (dispatchExit d') pState) ex') - case ac of - (ProcessContinue s') -> recvLoop pDef s' recvDelay - (ProcessTimeout t' s') -> recvLoop pDef s' (Delay t') - (ProcessHibernate d' s') -> block d' >> recvLoop pDef s' recvDelay - (ProcessStop r) -> handleStop pState r >> return (r :: TerminateReason) - --- an explicit 'cast' giving 'Shutdown' will stop the server gracefully -shutdownHandler :: Dispatcher s -shutdownHandler = handleCast (\_ Shutdown -> stop $ TerminateShutdown) - --- @(ProcessExitException from Shutdown)@ will stop the server gracefully -trapExit :: ExitSignalDispatcher s -trapExit = handleExit (\_ (_ :: ProcessId) Shutdown -> stop $ TerminateShutdown) - -block :: TimeInterval -> Process () -block i = liftIO $ threadDelay (asTimeout i) - -applyPolicy :: UnhandledMessagePolicy - -> s - -> AbstractMessage - -> Process (ProcessAction s) -applyPolicy p s m = - case p of - Terminate -> stop $ TerminateOther "UnhandledInput" - DeadLetter pid -> forward m pid >> continue s - Drop -> continue s - -matchAux :: UnhandledMessagePolicy - -> s - -> [DeferredDispatcher s] - -> [Match (ProcessAction s)] -matchAux p ps ds = [matchAny (auxHandler (applyPolicy p ps) ps ds)] - -auxHandler :: (AbstractMessage -> Process (ProcessAction s)) - -> s - -> [DeferredDispatcher s] - -> AbstractMessage - -> Process (ProcessAction s) -auxHandler policy _ [] msg = policy msg -auxHandler policy st (d:ds :: [DeferredDispatcher s]) msg - | length ds > 0 = let dh = dispatchInfo d in do - -- NB: we *do not* want to terminate/dead-letter messages until - -- we've exhausted all the possible info handlers - m <- dh st msg - case m of - Nothing -> auxHandler policy st ds msg - Just act -> return act - -- but here we *do* let the policy kick in - | otherwise = let dh = dispatchInfo d in do - m <- dh st msg - case m of - Nothing -> policy msg - Just act -> return act - -processReceive :: [Match (ProcessAction s)] - -> TimeoutHandler s - -> s - -> Delay - -> Process (ProcessAction s) -processReceive ms handleTimeout st d = do - next <- recv ms d - case next of - Nothing -> handleTimeout st d - Just pa -> return pa - where - recv :: [Match (ProcessAction s)] - -> Delay - -> Process (Maybe (ProcessAction s)) - recv matches d' = - case d' of - Infinity -> receiveWait matches >>= return . Just - Delay t' -> receiveTimeout (asTimeout t') matches - diff --git a/src/Control/Distributed/Process/Platform/GenServer.hs b/src/Control/Distributed/Process/Platform/GenServer.hs deleted file mode 100644 index 983a4d4e..00000000 --- a/src/Control/Distributed/Process/Platform/GenServer.hs +++ /dev/null @@ -1,351 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FunctionalDependencies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - --- | Second iteration of GenServer -module Control.Distributed.Process.Platform.GenServer ( - ServerId, - initOk, - initStop, - ok, - forward, - stop, - InitHandler, - Handler, - TerminateHandler, - MessageDispatcher(), - handle, - handleIf, - handleAny, - putState, - getState, - modifyState, - LocalServer(..), - defaultServer, - start, - startLink, - startMonitor, - terminate, - cast, - call, - callTimeout, - wait, - waitTimeout, - Process, - trace - ) where - -import qualified Control.Distributed.Process as P (forward, catch) -import qualified Control.Monad.State as ST (MonadState, - MonadTrans, - StateT, get, - lift, modify, - put, - runStateT) - -import Control.Applicative (Applicative) -import Control.Exception (SomeException) -import Control.Monad.IO.Class (MonadIO) - -import Control.Distributed.Process (AbstractMessage, - Match, - Process, - ProcessId, - monitor, link, - exit, getSelfPid, match, - matchAny, matchIf, - receiveTimeout, - receiveWait, say, - send, spawnLocal) - -import Control.Distributed.Process.Internal.Types (MonitorRef) -import Control.Distributed.Process.Serializable (Serializable) -import Control.Distributed.Process.Platform.Time -import Control.Distributed.Process.Platform.Async.AsyncChan - -import Data.Binary (Binary (..), getWord8, putWord8) -import Data.Maybe (fromJust) -import Data.DeriveTH -import Data.Typeable (Typeable) - --------------------------------------------------------------------------------- --- Data Types -- --------------------------------------------------------------------------------- - --- | ServerId -type ServerId = ProcessId - --- | Server monad -newtype Server s a = Server { - unServer :: ST.StateT s Process a - } - deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) - --- | Initialize handler result -data InitResult - = InitOk Delay - | InitStop String - -initOk :: Delay -> Server s InitResult -initOk t = return (InitOk t) - -initStop :: String -> Server s InitResult -initStop reason = return (InitStop reason) - --- | Terminate reason -data TerminateReason - = TerminateNormal - | TerminateShutdown - | TerminateReason String - deriving (Show, Typeable) -$(derive makeBinary ''TerminateReason) - --- | The result of a call -data Result a - = Ok a - | Forward ServerId - | Stop a String - deriving (Show, Typeable) - -ok :: (Serializable a, Show a) => a -> Server s (Result a) -ok resp = return (Ok resp) - -forward :: (Serializable a, Show a) => ServerId -> Server s (Result a) -forward sid = return (Forward sid) - -stop :: (Serializable a, Show a) => a -> String -> Server s (Result a) -stop resp reason = return (Stop resp reason) - --- | Handlers -type InitHandler s = Server s InitResult -type TerminateHandler s = TerminateReason -> Server s () -type Handler s a b = a -> Server s (Result b) - --- | Adds routing metadata to the actual payload -data Message a = - CallMessage { msgFrom :: ProcessId, msgPayload :: a } - | CastMessage { msgFrom :: ProcessId, msgPayload :: a } - deriving (Show, Typeable) -$(derive makeBinary ''Message) - --- | Dispatcher that knows how to dispatch messages to a handler --- s The server state -data MessageDispatcher s = - forall a . (Serializable a) => MessageDispatcher { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason) - } - | forall a . (Serializable a) => MessageDispatcherIf { - dispatcher :: s -> Message a -> Process (s, Maybe TerminateReason), - dispatchIf :: s -> Message a -> Bool - } - | MessageDispatcherAny { - dispatcherAny :: s -> AbstractMessage -> Process (s, Maybe TerminateReason) - } - --- | Matches messages using a dispatcher -class MessageMatcher d where - matchMessage :: s -> d s -> Match (s, Maybe TerminateReason) - --- | Matches messages to a MessageDispatcher -instance MessageMatcher MessageDispatcher where - matchMessage s (MessageDispatcher d) = match (d s) - matchMessage s (MessageDispatcherIf d cond) = matchIf (cond s) (d s) - matchMessage s (MessageDispatcherAny d) = matchAny (d s) - --- | Constructs a call message dispatcher --- -handle :: (Serializable a, Show a, Serializable b, Show b) => Handler s a b -> MessageDispatcher s -handle = handleIf (const True) - -handleIf :: (Serializable a, Show a, Serializable b, Show b) => (a -> Bool) -> Handler s a b -> MessageDispatcher s -handleIf cond handler = MessageDispatcherIf { - dispatcher = (\s msg -> case msg of - CallMessage cid payload -> do - --say $ "Server got CALL: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Ok resp -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Nothing) - Forward sid -> do - --say $ "Server FORWARD to: " ++ show sid - send sid msg - return (s', Nothing) - Stop resp reason -> do - --say $ "Server REPLY: " ++ show r - send cid resp - return (s', Just (TerminateReason reason)) - CastMessage _ payload -> do - --say $ "Server got CAST: [" ++ show cid ++ " / " ++ show payload ++ "]" - (r, s') <- runServer (handler payload) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - send sid msg - return (s', Nothing) - ), - dispatchIf = \_ msg -> cond (msgPayload msg) -} - --- | Constructs a dispatcher for any message --- Note that since we don't know the type of this message it assumes the protocol of a cast --- i.e. no reply's -handleAny :: (Serializable a, Show a) => (AbstractMessage -> Server s (Result a)) -> MessageDispatcher s -handleAny handler = MessageDispatcherAny { - dispatcherAny = (\s m -> do - (r, s') <- runServer (handler m) s - case r of - Stop _ reason -> return (s', Just $ TerminateReason reason) - Ok _ -> return (s', Nothing) - Forward sid -> do - (P.forward m) sid - return (s', Nothing) - ) -} - --- | The server callbacks -data LocalServer s = LocalServer { - initHandler :: InitHandler s, -- ^ initialization handler - handlers :: [MessageDispatcher s], - terminateHandler :: TerminateHandler s -- ^ termination handler - } - ----- | Default record ----- Starting point for creating new servers -defaultServer :: LocalServer s -defaultServer = LocalServer { - initHandler = return $ InitOk Infinity, - handlers = [], - terminateHandler = \_ -> return () -} - --------------------------------------------------------------------------------- --- API -- --------------------------------------------------------------------------------- - --- | Start a new server and return it's id -start :: s -> LocalServer s -> Process ServerId -start s ls = spawnLocal proc - where - proc = processServer initH terminateH hs s - initH = initHandler ls - terminateH = terminateHandler ls - hs = handlers ls - --- | Spawn a process and link to it -startLink :: s -> LocalServer s -> Process ServerId -startLink s ls = do - pid <- start s ls - link pid - return pid - --- | Like 'spawnServerLink', but monitor the spawned process -startMonitor :: s -> LocalServer s -> Process (ServerId, MonitorRef) -startMonitor s ls = do - pid <- start s ls - ref <- monitor pid - return (pid, ref) - --- | Sync call with no timeout -call :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> rq -> Process rs -call sid rq = callTimeout sid Infinity rq >>= return . fromJust - --- | Sync call -callTimeout :: (Serializable rq, Show rq, Serializable rs, Show rs) => ServerId -> Delay -> rq -> Process (Maybe rs) -callTimeout sid t rq = undefined - --- | Cast a message to a server identified by it's ServerId -cast :: (Serializable a) => ServerId -> a -> Process () -cast sid msg = do - cid <- getSelfPid - --say $ "Casting server " ++ show cid - send sid (CastMessage cid msg) - --- | Stops a server identified by it's ServerId -terminate :: Serializable a => ServerId -> a -> Process () -terminate sid reason = do - --say $ "Stop server " ++ show sid - exit sid reason - --- | Get the server state -getState :: Server s s -getState = ST.get - --- | Put the server state -putState :: s -> Server s () -putState = ST.put - --- | Modify the server state -modifyState :: (s -> s) -> Server s () -modifyState = ST.modify - --------------------------------------------------------------------------------- --- Implementation -- --------------------------------------------------------------------------------- - --- | server process -processServer :: InitHandler s -> TerminateHandler s -> [MessageDispatcher s] -> s -> Process () -processServer initH terminateH dispatchers s = do - (ir, s') <- runServer initH s - P.catch (proc ir s') (exitHandler s') - where - proc ir s' = do - (tr, s'') <- runServer (processLoop dispatchers ir) s' - _ <- runServer (terminateH tr) s'' - return () - exitHandler s' e = do - let tr = TerminateReason $ show (e :: SomeException) - _ <- runServer (terminateH tr) s' - return () - --- | server loop -processLoop :: [MessageDispatcher s] -> InitResult -> Server s TerminateReason -processLoop dispatchers ir = do - case ir of - InitOk t -> loop dispatchers t - InitStop r -> return $ TerminateReason r - where - loop ds t = do - msgM <- processReceive ds t - case msgM of - Nothing -> loop ds t - Just r -> return r - --- | -processReceive :: [MessageDispatcher s] -> Delay -> Server s (Maybe TerminateReason) -processReceive ds t = do - s <- getState - let ms = map (matchMessage s) ds - case t of - Infinity -> do - (s', r) <- lift $ receiveWait ms - putState s' - return r - Delay t' -> do - mayResult <- lift $ receiveTimeout (asTimeout t') ms - case mayResult of - Just (s', r) -> do - putState s' - return r - Nothing -> do - --trace "Receive timed out ..." - return $ Just (TerminateReason "Receive timed out") - --- | Log a trace message using the underlying Process's say -trace :: String -> Server s () -trace msg = lift . say $ msg - --- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a -lift :: Process a -> Server s a -lift p = Server $ ST.lift p - --- | -runServer :: Server s a -> s -> Process (a, s) -runServer server state = ST.runStateT (unServer server) state From 2347d90f6748e6e045f8bf53312c5b0a4f690c32 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 29 Jan 2013 07:01:41 -0800 Subject: [PATCH 0897/2357] Initial commit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..20333de9 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +haskell-distributed.github.com +============================== + +haskell-distributed website \ No newline at end of file From 930953981b92e10c08670d20bd582afc6f1b9dd0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 29 Jan 2013 15:04:42 +0000 Subject: [PATCH 0898/2357] Set up a new website --- .gitignore | 4 + Makefile | 19 + _config.yml | 16 + _includes/footer.html | 32 + _includes/head.html | 29 + _includes/js.html | 3 + _includes/nav.html | 44 + _layouts/default.html | 18 + _layouts/documentation.html | 41 + _layouts/marketing.html | 55 + _layouts/nt_tutorial.html | 40 + _layouts/page.html | 12 + _layouts/post.html | 14 + _layouts/site.html | 72 + _layouts/team.html | 39 + _layouts/tutorial.html | 41 + _layouts/wiki.html | 42 + _posts/2012-01-21-cloud-haskell-appetiser.md | 9 + _posts/2012-10-04-the-new-cloud-haskell.md | 9 + ...communication-patterns-in-cloud-haskell.md | 9 + ...mmunication-patterns-in-cloud-haskell-2.md | 9 + ...mmunication-patterns-in-cloud-haskell-3.md | 9 + ...mmunication-patterns-in-cloud-haskell-4.md | 9 + _posts/2013-01-29-announce-0.4.2.md | 9 + about.html | 16 + contact.html | 37 + css/bootstrap-responsive.css | 1092 +++ css/bootstrap-responsive.min.css | 9 + css/bootstrap.css | 6039 +++++++++++++++++ css/bootstrap.min.css | 9 + css/footer.css | 23 + css/nav.css | 26 + css/page.css | 20 + css/pygments.css | 61 + css/sidenav.css | 157 + css/site.css | 171 + css/social.css | 31 + .../Paths_distributed_process_platform.hs | 32 + dist/build/autogen/cabal_macros.h | 58 + dist/setup-config | 2 + documentation.md | 353 + ico/favicon.ico | Bin 0 -> 1406 bytes img/NetworkTCP.png | Bin 0 -> 109271 bytes img/back1.jpg | Bin 0 -> 636713 bytes img/back2.jpg | Bin 0 -> 628846 bytes img/back3.jpg | Bin 0 -> 219614 bytes img/email_128x128.png | Bin 0 -> 4718 bytes img/feed-icon-14x14.png | Bin 0 -> 689 bytes img/feed-icon-28x28.png | Bin 0 -> 1737 bytes img/github_logo_300x300.png | Bin 0 -> 27871 bytes img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes img/glyphicons-halflings.png | Bin 0 -> 12799 bytes img/icon_irc_400x400.png | Bin 0 -> 9756 bytes img/irc_icon.png | Bin 0 -> 3069 bytes img/logoBambooPNG.png | Bin 0 -> 19067 bytes img/logoJIRAPNG.png | Bin 0 -> 10274 bytes index.md | 36 + js/bootstrap.js | 2159 ++++++ js/bootstrap.min.js | 6 + js/jquery.js | 2 + posts.md | 15 + ...uted-Process-Platform-Async-AsyncChan.html | 109 + ...buted-Process-Platform-Async-AsyncSTM.html | 90 + ...ol-Distributed-Process-Platform-Async.html | 118 + ...rol-Distributed-Process-Platform-Call.html | 34 + ...rocess-Platform-ManagedProcess-Client.html | 36 + ...rocess-Platform-ManagedProcess-Server.html | 94 + ...buted-Process-Platform-ManagedProcess.html | 287 + ...rol-Distributed-Process-Platform-Test.html | 17 + ...rol-Distributed-Process-Platform-Time.html | 27 + ...ol-Distributed-Process-Platform-Timer.html | 33 + .../Control-Distributed-Process-Platform.html | 54 + .../distributed-process-platform.haddock | Bin 0 -> 134679 bytes .../doc-index-95.html | 4 + .../doc-index-A.html | 4 + .../doc-index-All.html | 4 + .../doc-index-C.html | 4 + .../doc-index-D.html | 4 + .../doc-index-E.html | 4 + .../doc-index-F.html | 4 + .../doc-index-G.html | 4 + .../doc-index-H.html | 4 + .../doc-index-I.html | 4 + .../doc-index-K.html | 4 + .../doc-index-L.html | 4 + .../doc-index-M.html | 4 + .../doc-index-N.html | 4 + .../doc-index-P.html | 4 + .../doc-index-R.html | 4 + .../doc-index-S.html | 4 + .../doc-index-T.html | 4 + .../doc-index-U.html | 4 + .../doc-index-W.html | 4 + .../doc-index.html | 4 + .../distributed-process-platform/frames.html | 30 + .../haddock-util.js | 344 + .../hslogo-16.png | Bin 0 -> 1684 bytes .../index-frames.html | 4 + .../distributed-process-platform/index.html | 8 + ...uted-Process-Platform-Async-AsyncChan.html | 9 + ...buted-Process-Platform-Async-AsyncSTM.html | 10 + ...ol-Distributed-Process-Platform-Async.html | 9 + ...rol-Distributed-Process-Platform-Call.html | 4 + ...rocess-Platform-ManagedProcess-Client.html | 5 + ...rocess-Platform-ManagedProcess-Server.html | 7 + ...buted-Process-Platform-ManagedProcess.html | 9 + ...rol-Distributed-Process-Platform-Test.html | 4 + ...rol-Distributed-Process-Platform-Time.html | 4 + ...ol-Distributed-Process-Platform-Timer.html | 4 + ..._Control-Distributed-Process-Platform.html | 8 + .../distributed-process-platform/minus.gif | Bin 0 -> 56 bytes .../distributed-process-platform/ocean.css | 546 ++ .../doc/distributed-process-platform/plus.gif | Bin 0 -> 59 bytes .../distributed-process-platform/synopsis.png | Bin 0 -> 11327 bytes static/semantics.pdf | Bin 0 -> 167226 bytes static/templates/wikipage.md | 10 + team.md | 53 + terms.md | 6 + tutorials/1.tutorial.md | 294 + tutorials/2.nt_tutorial.md | 294 + wiki.md | 33 + wiki/contributing.md | 106 + wiki/maintainers.md | 40 + wiki/networktransport.md | 114 + wiki/newdesign.md | 577 ++ wiki/newtransports.md | 140 + wiki/style.md | 120 + 127 files changed, 14713 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 _config.yml create mode 100644 _includes/footer.html create mode 100644 _includes/head.html create mode 100644 _includes/js.html create mode 100644 _includes/nav.html create mode 100644 _layouts/default.html create mode 100644 _layouts/documentation.html create mode 100644 _layouts/marketing.html create mode 100644 _layouts/nt_tutorial.html create mode 100644 _layouts/page.html create mode 100644 _layouts/post.html create mode 100644 _layouts/site.html create mode 100644 _layouts/team.html create mode 100644 _layouts/tutorial.html create mode 100644 _layouts/wiki.html create mode 100644 _posts/2012-01-21-cloud-haskell-appetiser.md create mode 100644 _posts/2012-10-04-the-new-cloud-haskell.md create mode 100644 _posts/2012-10-05-communication-patterns-in-cloud-haskell.md create mode 100644 _posts/2012-10-08-communication-patterns-in-cloud-haskell-2.md create mode 100644 _posts/2012-10-12-communication-patterns-in-cloud-haskell-3.md create mode 100644 _posts/2012-10-15-communication-patterns-in-cloud-haskell-4.md create mode 100644 _posts/2013-01-29-announce-0.4.2.md create mode 100644 about.html create mode 100644 contact.html create mode 100644 css/bootstrap-responsive.css create mode 100644 css/bootstrap-responsive.min.css create mode 100644 css/bootstrap.css create mode 100644 css/bootstrap.min.css create mode 100644 css/footer.css create mode 100644 css/nav.css create mode 100644 css/page.css create mode 100644 css/pygments.css create mode 100644 css/sidenav.css create mode 100644 css/site.css create mode 100644 css/social.css create mode 100644 dist/build/autogen/Paths_distributed_process_platform.hs create mode 100644 dist/build/autogen/cabal_macros.h create mode 100644 dist/setup-config create mode 100644 documentation.md create mode 100644 ico/favicon.ico create mode 100644 img/NetworkTCP.png create mode 100644 img/back1.jpg create mode 100755 img/back2.jpg create mode 100644 img/back3.jpg create mode 100644 img/email_128x128.png create mode 100755 img/feed-icon-14x14.png create mode 100755 img/feed-icon-28x28.png create mode 100644 img/github_logo_300x300.png create mode 100644 img/glyphicons-halflings-white.png create mode 100644 img/glyphicons-halflings.png create mode 100644 img/icon_irc_400x400.png create mode 100644 img/irc_icon.png create mode 100644 img/logoBambooPNG.png create mode 100644 img/logoJIRAPNG.png create mode 100644 index.md create mode 100644 js/bootstrap.js create mode 100644 js/bootstrap.min.js create mode 100644 js/jquery.js create mode 100644 posts.md create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async-AsyncChan.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async-AsyncSTM.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Call.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Client.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Server.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Test.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Time.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Timer.html create mode 100644 static/doc/distributed-process-platform/Control-Distributed-Process-Platform.html create mode 100644 static/doc/distributed-process-platform/distributed-process-platform.haddock create mode 100644 static/doc/distributed-process-platform/doc-index-95.html create mode 100644 static/doc/distributed-process-platform/doc-index-A.html create mode 100644 static/doc/distributed-process-platform/doc-index-All.html create mode 100644 static/doc/distributed-process-platform/doc-index-C.html create mode 100644 static/doc/distributed-process-platform/doc-index-D.html create mode 100644 static/doc/distributed-process-platform/doc-index-E.html create mode 100644 static/doc/distributed-process-platform/doc-index-F.html create mode 100644 static/doc/distributed-process-platform/doc-index-G.html create mode 100644 static/doc/distributed-process-platform/doc-index-H.html create mode 100644 static/doc/distributed-process-platform/doc-index-I.html create mode 100644 static/doc/distributed-process-platform/doc-index-K.html create mode 100644 static/doc/distributed-process-platform/doc-index-L.html create mode 100644 static/doc/distributed-process-platform/doc-index-M.html create mode 100644 static/doc/distributed-process-platform/doc-index-N.html create mode 100644 static/doc/distributed-process-platform/doc-index-P.html create mode 100644 static/doc/distributed-process-platform/doc-index-R.html create mode 100644 static/doc/distributed-process-platform/doc-index-S.html create mode 100644 static/doc/distributed-process-platform/doc-index-T.html create mode 100644 static/doc/distributed-process-platform/doc-index-U.html create mode 100644 static/doc/distributed-process-platform/doc-index-W.html create mode 100644 static/doc/distributed-process-platform/doc-index.html create mode 100644 static/doc/distributed-process-platform/frames.html create mode 100644 static/doc/distributed-process-platform/haddock-util.js create mode 100644 static/doc/distributed-process-platform/hslogo-16.png create mode 100644 static/doc/distributed-process-platform/index-frames.html create mode 100644 static/doc/distributed-process-platform/index.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncChan.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncSTM.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Call.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Client.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Server.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Test.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Time.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Timer.html create mode 100644 static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform.html create mode 100644 static/doc/distributed-process-platform/minus.gif create mode 100644 static/doc/distributed-process-platform/ocean.css create mode 100644 static/doc/distributed-process-platform/plus.gif create mode 100644 static/doc/distributed-process-platform/synopsis.png create mode 100644 static/semantics.pdf create mode 100644 static/templates/wikipage.md create mode 100644 team.md create mode 100644 terms.md create mode 100644 tutorials/1.tutorial.md create mode 100644 tutorials/2.nt_tutorial.md create mode 100644 wiki.md create mode 100644 wiki/contributing.md create mode 100644 wiki/maintainers.md create mode 100644 wiki/networktransport.md create mode 100644 wiki/newdesign.md create mode 100644 wiki/newtransports.md create mode 100644 wiki/style.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0bf0b87a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.*.swo +.*.swp +_site +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f535c02c --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ + +NAME ?= '' +FNAME = $(shell echo $(NAME) | tr A-Z a-z) +ROOT_DIRECTORY=. +TEMPLATE_DIR=${ROOT_DIRECTORY}/static/templates +TEMPLATE_FILES=$(wildcard ${TEMPLATE_DIR}/*) +TEMPLATES=$(basename $(notdir ${TEMPLATE_FILES})) + +all: + $(info select a target) + $(info ${TEMPLATES}) + +ifneq ($(NAME), '') +$(TEMPLATES): + cat ${TEMPLATE_DIR}/$@.md | sed s/@PAGE@/${NAME}/g >> ${ROOT_DIRECTORY}/wiki/${FNAME}.md +else +$(TEMPLATES): + $(error you need to specify NAME= to run this target) +endif diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..ca446068 --- /dev/null +++ b/_config.yml @@ -0,0 +1,16 @@ +exclude: [".rvmrc", ".rbenv-version", "README.md", "Rakefile", "changelog.md"] +auto: true +lsi: false +pygments: true +safe: true + +title: Cloud Haskell +tagline: Erlang-style concurrency for Haskell + +author: + name: Tim Watson + email: watson.timothy@gmail.com + github: hyperthunk + twitter: hyperthunk + +production_url: http://haskell-distributed.github.com/distributed-process-platform diff --git a/_includes/footer.html b/_includes/footer.html new file mode 100644 index 00000000..072d0822 --- /dev/null +++ b/_includes/footer.html @@ -0,0 +1,32 @@ + diff --git a/_includes/head.html b/_includes/head.html new file mode 100644 index 00000000..b55da483 --- /dev/null +++ b/_includes/head.html @@ -0,0 +1,29 @@ + + {{ page.title }} + + {% if page.description %}{% endif %} + + + + + + + + + + + + + + + + + + diff --git a/_includes/js.html b/_includes/js.html new file mode 100644 index 00000000..9461c48f --- /dev/null +++ b/_includes/js.html @@ -0,0 +1,3 @@ + + + diff --git a/_includes/nav.html b/_includes/nav.html new file mode 100644 index 00000000..02cc7486 --- /dev/null +++ b/_includes/nav.html @@ -0,0 +1,44 @@ + diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 00000000..eeaa12ca --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,18 @@ + + + + {% include head.html %} + + + + {% include nav.html %} + {{ content }} + {% include footer.html %} + {% include js.html %} + + + diff --git a/_layouts/documentation.html b/_layouts/documentation.html new file mode 100644 index 00000000..936ad94b --- /dev/null +++ b/_layouts/documentation.html @@ -0,0 +1,41 @@ + + + + {% include head.html %} + + + + + {% include nav.html %} +
+ +
+ + {% include footer.html %} + {% include js.html %} + + + diff --git a/_layouts/marketing.html b/_layouts/marketing.html new file mode 100644 index 00000000..6a22cf27 --- /dev/null +++ b/_layouts/marketing.html @@ -0,0 +1,55 @@ + + + + {% include head.html %} + + + + + + {% include nav.html %} + {{ content }} + {% include footer.html %} + {% include js.html %} + + + diff --git a/_layouts/nt_tutorial.html b/_layouts/nt_tutorial.html new file mode 100644 index 00000000..904b2bb4 --- /dev/null +++ b/_layouts/nt_tutorial.html @@ -0,0 +1,40 @@ + + + + {% include head.html %} + + + + + {% include nav.html %} +
+ +
+ + {% include footer.html %} + {% include js.html %} + + diff --git a/_layouts/page.html b/_layouts/page.html new file mode 100644 index 00000000..77126c5b --- /dev/null +++ b/_layouts/page.html @@ -0,0 +1,12 @@ +--- +layout: default +--- +
+ +
+
+ {{ content }} +
diff --git a/_layouts/post.html b/_layouts/post.html new file mode 100644 index 00000000..b187f732 --- /dev/null +++ b/_layouts/post.html @@ -0,0 +1,14 @@ +--- +layout: default +--- +
+ +
+
+ {{ content }} +
diff --git a/_layouts/site.html b/_layouts/site.html new file mode 100644 index 00000000..ec81725e --- /dev/null +++ b/_layouts/site.html @@ -0,0 +1,72 @@ +--- +title: Cloud Haskell +--- + + + + {% include head.html %} + + + + + {% include nav.html %} + + + +
+ {{ content }} +
+ + {% include footer.html %} + {% include js.html %} + + + + diff --git a/_layouts/team.html b/_layouts/team.html new file mode 100644 index 00000000..dd54a2d3 --- /dev/null +++ b/_layouts/team.html @@ -0,0 +1,39 @@ + + + + {% include head.html %} + + + + + {% include nav.html %} +
+ +
+
+
+ +
+ {{ content }} +
+
+
+ {% include footer.html %} + {% include js.html %} + + + diff --git a/_layouts/tutorial.html b/_layouts/tutorial.html new file mode 100644 index 00000000..ee2c1ff0 --- /dev/null +++ b/_layouts/tutorial.html @@ -0,0 +1,41 @@ + + + + {% include head.html %} + + + + + {% include nav.html %} +
+ +
+ + {% include footer.html %} + {% include js.html %} + + + diff --git a/_layouts/wiki.html b/_layouts/wiki.html new file mode 100644 index 00000000..937ec08c --- /dev/null +++ b/_layouts/wiki.html @@ -0,0 +1,42 @@ + + + + {% include head.html %} + + + + + {% include nav.html %} +
+ +
+
+
+ +
+ {{ content }} +
+
+
+ {% include footer.html %} + {% include js.html %} + + + diff --git a/_posts/2012-01-21-cloud-haskell-appetiser.md b/_posts/2012-01-21-cloud-haskell-appetiser.md new file mode 100644 index 00000000..7beb402c --- /dev/null +++ b/_posts/2012-01-21-cloud-haskell-appetiser.md @@ -0,0 +1,9 @@ +--- +layout: post +category: well-typed +tags: [well-typed, blog, distributed-process] +title: "A Cloud Haskell Appetiser" +teaser: Hello Haskellers! We mentioned in the last digest that we'd have just a tiny bit more to say about Parallel Haskell.... +author: Eric Kow +link: http://www.well-typed.com/blog/68 +--- diff --git a/_posts/2012-10-04-the-new-cloud-haskell.md b/_posts/2012-10-04-the-new-cloud-haskell.md new file mode 100644 index 00000000..6b684582 --- /dev/null +++ b/_posts/2012-10-04-the-new-cloud-haskell.md @@ -0,0 +1,9 @@ +--- +layout: post +category: well-typed +tags: [well-typed, blog, distributed-process] +title: "The New Cloud Haskell" +teaser: For about the last year we have been working on a new implementation of Cloud Haskell... +author: Duncan Coutts +link: http://www.well-typed.com/blog/70 +--- diff --git a/_posts/2012-10-05-communication-patterns-in-cloud-haskell.md b/_posts/2012-10-05-communication-patterns-in-cloud-haskell.md new file mode 100644 index 00000000..1e300f22 --- /dev/null +++ b/_posts/2012-10-05-communication-patterns-in-cloud-haskell.md @@ -0,0 +1,9 @@ +--- +layout: post +category: well-typed +tags: [well-typed, blog, distributed-process] +title: "Communication Patterns in Cloud Haskell" +teaser: Master-Slave, Work-Stealing and Work-Pushing +author: Edsko de Vries +link: http://www.well-typed.com/blog/71 +--- diff --git a/_posts/2012-10-08-communication-patterns-in-cloud-haskell-2.md b/_posts/2012-10-08-communication-patterns-in-cloud-haskell-2.md new file mode 100644 index 00000000..bd711072 --- /dev/null +++ b/_posts/2012-10-08-communication-patterns-in-cloud-haskell-2.md @@ -0,0 +1,9 @@ +--- +layout: post +category: well-typed +tags: [well-typed, blog, distributed-process] +title: "Communication Patterns in Cloud Haskell (Part 2)" +teaser: Performance... +author: Edsko de Vries +link: http://www.well-typed.com/blog/72 +--- diff --git a/_posts/2012-10-12-communication-patterns-in-cloud-haskell-3.md b/_posts/2012-10-12-communication-patterns-in-cloud-haskell-3.md new file mode 100644 index 00000000..8ef10ca6 --- /dev/null +++ b/_posts/2012-10-12-communication-patterns-in-cloud-haskell-3.md @@ -0,0 +1,9 @@ +--- +layout: post +category: well-typed +tags: [well-typed, blog, distributed-process] +title: "Communication Patterns in Cloud Haskell (Part 3)" +teaser: Map-Reduce... +author: Edsko de Vries +link: http://www.well-typed.com/blog/73 +--- diff --git a/_posts/2012-10-15-communication-patterns-in-cloud-haskell-4.md b/_posts/2012-10-15-communication-patterns-in-cloud-haskell-4.md new file mode 100644 index 00000000..c863e6ce --- /dev/null +++ b/_posts/2012-10-15-communication-patterns-in-cloud-haskell-4.md @@ -0,0 +1,9 @@ +--- +layout: post +category: well-typed +tags: [well-typed, blog, distributed-process] +title: "Communication Patterns in Cloud Haskell (Part 4)" +teaser: K-Means... +author: Edsko de Vries +link: http://www.well-typed.com/blog/74 +--- diff --git a/_posts/2013-01-29-announce-0.4.2.md b/_posts/2013-01-29-announce-0.4.2.md new file mode 100644 index 00000000..7abd3490 --- /dev/null +++ b/_posts/2013-01-29-announce-0.4.2.md @@ -0,0 +1,9 @@ +--- +layout: post +category: announcements +tags: [announcement, distributed-process] +title: "[ANNOUNCE] distributed-process-0.4.2" +teaser: I am happy to announce the release of version 0.4.2 of the distributed-process package… +author: Tim Watson +link: http://groups.google.com/group/parallel-haskell/browse_thread/thread/15a2b0365059e59a/c5c8a373ab5a2f38?show_docid=c5c8a373ab5a2f38 +--- diff --git a/about.html b/about.html new file mode 100644 index 00000000..5a13f069 --- /dev/null +++ b/about.html @@ -0,0 +1,16 @@ +--- +layout: page +title: About +--- + +Cloud Haskell is cool + +
+

Install Cloud Haskell

+

You can retrieve the latest version of Cloud Haskell from github.

+

+ + View on Github + +

+
diff --git a/contact.html b/contact.html new file mode 100644 index 00000000..ffed7d7a --- /dev/null +++ b/contact.html @@ -0,0 +1,37 @@ +--- +layout: marketing +title: Contact +--- +
+ +
+ +
+
+ +
+ +

IRC

+

You'll probably find us lurking in the #haskell-distributed channel on freenode, where you can get help with your questions. There is also the general purpose #haskell channel.

+

Visit us on IRC

+
+ +
+ +

Issues Tracker

+

Notice a problem that you don't know how to fix? Or want to request a new feature? + (in order to create new issues you will need to create a login)

+

Browse Issues

+
+ +
+ +

Email

+

If you have any questions or concerns, please email the parallel-haskell mailing list in the first instace.

+

Send Email

+
+
+
diff --git a/css/bootstrap-responsive.css b/css/bootstrap-responsive.css new file mode 100644 index 00000000..a3352d77 --- /dev/null +++ b/css/bootstrap-responsive.css @@ -0,0 +1,1092 @@ +/*! + * Bootstrap Responsive v2.2.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +@-ms-viewport { + width: device-width; +} + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.564102564102564%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.7624309392265194%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom, + .navbar-static-top { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .uneditable-input[class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: 100%; + margin-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="offset"]:first-child { + margin-left: 0; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 0; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade { + top: -100px; + } + .modal.fade.in { + top: 20px; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .media .pull-left, + .media .pull-right { + display: block; + float: none; + margin-bottom: 10px; + } + .media-object { + margin-right: 0; + margin-left: 0; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #777777; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #777777; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .dropdown-menu a:hover { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a, + .navbar-inverse .nav-collapse .dropdown-menu a { + color: #999999; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:hover { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: none; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .open > .dropdown-menu { + display: block; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .nav > li > .dropdown-menu:before, + .nav-collapse .nav > li > .dropdown-menu:after { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-inverse .nav-collapse .navbar-form, + .navbar-inverse .nav-collapse .navbar-search { + border-top-color: #111111; + border-bottom-color: #111111; + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/css/bootstrap-responsive.min.css b/css/bootstrap-responsive.min.css new file mode 100644 index 00000000..5cb833ff --- /dev/null +++ b/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.2.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */@-ms-viewport{width:device-width}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/css/bootstrap.css b/css/bootstrap.css new file mode 100644 index 00000000..8ab3cefc --- /dev/null +++ b/css/bootstrap.css @@ -0,0 +1,6039 @@ +/*! + * Bootstrap v2.2.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img, +.google-maps img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +label, +select, +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +input[type="radio"], +input[type="checkbox"] { + cursor: pointer; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.127659574468085%; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +a.muted:hover { + color: #808080; +} + +.text-warning { + color: #c09853; +} + +a.text-warning:hover { + color: #a47e3c; +} + +.text-error { + color: #b94a48; +} + +a.text-error:hover { + color: #953b39; +} + +.text-info { + color: #3a87ad; +} + +a.text-info:hover { + color: #2d6987; +} + +.text-success { + color: #468847; +} + +a.text-success:hover { + color: #356635; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + line-height: 40px; +} + +h1 { + font-size: 38.5px; +} + +h2 { + font-size: 31.5px; +} + +h3 { + font-size: 24.5px; +} + +h4 { + font-size: 17.5px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 11.9px; +} + +h1 small { + font-size: 24.5px; +} + +h2 small { + font-size: 17.5px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +ul.inline, +ol.inline { + margin-left: 0; + list-style: none; +} + +ul.inline > li, +ol.inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 25px; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + white-space: pre; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + color: #555555; + vertical-align: middle; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 20px; + padding-left: 20px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"], +.row-fluid .controls-row [class*="span"] { + float: left; +} + +.controls-row .checkbox[class*="span"], +.controls-row .radio[class*="span"] { + padding-top: 5px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning .control-label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success .control-label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info .control-label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + margin-bottom: 5px; + font-size: 0; + white-space: nowrap; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input, +.input-append .dropdown-menu, +.input-prepend .dropdown-menu { + font-size: 14px; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: top; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn, +.input-append .btn-group > .dropdown-toggle, +.input-prepend .btn-group > .dropdown-toggle { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input + .btn-group .btn:last-child, +.input-append select + .btn-group .btn:last-child, +.input-append .uneditable-input + .btn-group .btn:last-child { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append .add-on, +.input-append .btn, +.input-append .btn-group { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child, +.input-append .btn-group:last-child > .dropdown-toggle { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append input + .btn-group .btn, +.input-prepend.input-append select + .btn-group .btn, +.input-prepend.input-append .uneditable-input + .btn-group .btn { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .btn-group:first-child { + margin-left: 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block, +.form-horizontal .uneditable-input + .help-block, +.form-horizontal .input-prepend + .help-block, +.form-horizontal .input-append + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child > th:first-child, +.table-bordered tbody:first-child tr:first-child > td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child > th:last-child, +.table-bordered tbody:first-child tr:first-child > td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:first-child, +.table-bordered tbody:last-child tr:last-child > td:first-child, +.table-bordered tfoot:last-child tr:last-child > td:first-child { + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:last-child, +.table-bordered tbody:last-child tr:last-child > td:last-child, +.table-bordered tfoot:last-child tr:last-child > td:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + -moz-border-radius-bottomleft: 0; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; + -moz-border-radius-bottomright: 0; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-striped tbody > tr:nth-child(odd) > td, +.table-striped tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover td, +.table-hover tbody tr:hover th { + background-color: #f5f5f5; +} + +table td[class*="span"], +table th[class*="span"], +.row-fluid table td[class*="span"], +.row-fluid table th[class*="span"] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table td.span1, +.table th.span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table td.span2, +.table th.span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table td.span3, +.table th.span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table td.span4, +.table th.span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table td.span5, +.table th.span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table td.span6, +.table th.span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table td.span7, +.table th.span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table td.span8, +.table th.span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table td.span9, +.table th.span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table td.span10, +.table th.span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table td.span11, +.table th.span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table td.span12, +.table th.span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table tbody tr.success td { + background-color: #dff0d8; +} + +.table tbody tr.error td { + background-color: #f2dede; +} + +.table tbody tr.warning td { + background-color: #fcf8e3; +} + +.table tbody tr.info td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/active states of certain elements */ + +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu li > a:hover, +.dropdown-menu li > a:focus, +.dropdown-submenu:hover > a { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .disabled > a, +.dropdown-menu .disabled > a:hover { + color: #999999; +} + +.dropdown-menu .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropup .dropdown-submenu > .dropdown-menu { + top: auto; + bottom: 0; + margin-top: 0; + margin-bottom: -2px; + -webkit-border-radius: 5px 5px 5px 0; + -moz-border-radius: 5px 5px 5px 0; + border-radius: 5px 5px 5px 0; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left > .dropdown-menu { + left: -100%; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + z-index: 1051; + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 12px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #bbbbbb; + *border: 0; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-bottom-color: #a2a2a2; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 11px 19px; + font-size: 17.5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.btn-large [class^="icon-"], +.btn-large [class*=" icon-"] { + margin-top: 4px; +} + +.btn-small { + padding: 2px 10px; + font-size: 11.9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-small [class^="icon-"], +.btn-small [class*=" icon-"] { + margin-top: 0; +} + +.btn-mini [class^="icon-"], +.btn-mini [class*=" icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 0 6px; + font-size: 10.5px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn { + border-color: #c5c5c5; + border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -moz-linear-gradient(top, #444444, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + display: inline-block; + *display: inline; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; + *zoom: 1; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu, +.btn-group > .popover { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 10.5px; +} + +.btn-group > .btn-small { + font-size: 11.9px; +} + +.btn-group > .btn-large { + font-size: 17.5px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-mini .caret, +.btn-small .caret, +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.dropup .btn-large .caret { + border-bottom-width: 5px; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical > .btn { + display: block; + float: none; + max-width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical > .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical > .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical > .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical > .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert, +.alert h4 { + color: #c09853; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success h4 { + color: #468847; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger h4, +.alert-error h4 { + color: #b94a48; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info h4 { + color: #3a87ad; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li > a > img { + max-width: none; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"], +.nav-list [class*=" icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; + overflow: visible; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #777777; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 5px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown > a:hover .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; +} + +.navbar-inverse .brand { + color: #999999; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -moz-linear-gradient(top, #151515, #040404); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb > li > .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pagination-large ul > li > a, +.pagination-large ul > li > span { + padding: 11px 19px; + font-size: 17.5px; +} + +.pagination-large ul > li:first-child > a, +.pagination-large ul > li:first-child > span { + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.pagination-large ul > li:last-child > a, +.pagination-large ul > li:last-child > span { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.pagination-mini ul > li:first-child > a, +.pagination-small ul > li:first-child > a, +.pagination-mini ul > li:first-child > span, +.pagination-small ul > li:first-child > span { + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -webkit-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-topleft: 3px; +} + +.pagination-mini ul > li:last-child > a, +.pagination-small ul > li:last-child > a, +.pagination-mini ul > li:last-child > span, +.pagination-small ul > li:last-child > span { + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -moz-border-radius-topright: 3px; + -moz-border-radius-bottomright: 3px; +} + +.pagination-small ul > li > a, +.pagination-small ul > li > span { + padding: 2px 10px; + font-size: 11.9px; +} + +.pagination-mini ul > li > a, +.pagination-mini ul > li > span { + padding: 0 6px; + font-size: 10.5px; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager li > a:hover { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 10%; + left: 50%; + z-index: 1050; + width: 560px; + margin-left: -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + position: relative; + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + margin-top: -3px; +} + +.tooltip.right { + margin-left: 3px; +} + +.tooltip.bottom { + margin-top: 3px; +} + +.tooltip.left { + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + width: 236px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.media, +.media-body { + overflow: hidden; + *overflow: visible; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media .pull-left { + margin-right: 10px; +} + +.media .pull-right { + margin-left: 10px; +} + +.media-list { + margin-left: 0; + list-style: none; +} + +.label, +.badge { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding-right: 9px; + padding-left: 9px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +.label:empty, +.badge:empty { + display: none; +} + +a.label:hover, +a.badge:hover { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img { + display: block; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit li { + line-height: 30px; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/css/bootstrap.min.css b/css/bootstrap.min.css new file mode 100644 index 00000000..140f731d --- /dev/null +++ b/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.2.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover{color:#808080}.text-warning{color:#c09853}a.text-warning:hover{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover{color:#2d6987}.text-success{color:#468847}a.text-success:hover{color:#356635}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:25px}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{margin-bottom:5px;font-size:0;white-space:nowrap}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover td,.table-hover tbody tr:hover th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success td{background-color:#dff0d8}.table tbody tr.error td{background-color:#f2dede}.table tbody tr.warning td{background-color:#fcf8e3}.table tbody tr.info td{background-color:#d9edf7}.table-hover tbody tr.success:hover td{background-color:#d0e9c6}.table-hover tbody tr.error:hover td{background-color:#ebcccc}.table-hover tbody tr.warning:hover td{background-color:#faf2cc}.table-hover tbody tr.info:hover td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu li>a:hover,.dropdown-menu li>a:focus,.dropdown-submenu:hover>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:#999}.dropdown-menu .disabled>a:hover{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #bbb;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#a2a2a2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn{border-color:#c5c5c5;border-color:rgba(0,0,0,0.15) rgba(0,0,0,0.15) rgba(0,0,0,0.25)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-mini .caret,.btn-small .caret,.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret{border-top-color:#555;border-bottom-color:#555}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{margin-top:-3px}.tooltip.right{margin-left:3px}.tooltip.bottom{margin-top:3px}.tooltip.left{margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;width:236px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media .pull-left{margin-right:10px}.media .pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/css/footer.css b/css/footer.css new file mode 100644 index 00000000..10803437 --- /dev/null +++ b/css/footer.css @@ -0,0 +1,23 @@ +/* Footer +-------------------------------------------------- */ + +.footer { + padding: 70px 0px 70px 0px; + margin-top: 70px; + border-top: 1px solid #e5e5e5; + background-color: #f5f5f5; +} +.footer p { + margin-bottom: 0; + color: #777; +} +.footer-links { + margin: 10px 0; +} +.footer-links li { + display: inline; + padding: 0 2px; +} +.footer-links li:first-child { + padding-left: 0; +} diff --git a/css/nav.css b/css/nav.css new file mode 100644 index 00000000..091142cd --- /dev/null +++ b/css/nav.css @@ -0,0 +1,26 @@ +/* Remove border and change up box shadow for more contrast */ +.navbar .navbar-inner { + border: 0; + -webkit-box-shadow: 0 2px 10px rgba(0,0,0,.25); + -moz-box-shadow: 0 2px 10px rgba(0,0,0,.25); + box-shadow: 0 2px 10px rgba(0,0,0,.25); +} + +/* Downsize the brand/project name a bit */ +.navbar .brand { + padding: 14px 20px 16px; /* Increase vertical padding to match navbar links */ + font-size: 16px; + font-weight: bold; + text-shadow: 0 -1px 0 rgba(0,0,0,.5); +} + +/* Navbar links: increase padding for taller navbar */ +.navbar .nav > li > a { + padding: 15px 20px; +} + +/* Offset the responsive button for proper vertical alignment */ +.navbar .btn-navbar { + margin-top: 10px; +} + diff --git a/css/page.css b/css/page.css new file mode 100644 index 00000000..77fcb2ec --- /dev/null +++ b/css/page.css @@ -0,0 +1,20 @@ +body { +/* padding: 40px 20px; */ + color: #555; + text-shadow: 0 1px 0 #fff; + background-color: #fff; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#eee), color-stop(25%, #fff), to(#fff)); + background-image: -webkit-linear-gradient(#eee, #fff 25%, #fff); + background-image: -moz-linear-gradient(top, #eee, #fff 25%, #fff); + background-image: -ms-linear-gradient(#eee, #fff 25%, #fff); + background-image: -o-linear-gradient(#eee, #fff 25%, #fff); + background-image: linear-gradient(#eee, #fff 25%, #fff); + background-repeat: no-repeat; + background-attachment: fixed; + overflow: auto; +} + +h3 { + padding-top: 40px; + +} diff --git a/css/pygments.css b/css/pygments.css new file mode 100644 index 00000000..122b4294 --- /dev/null +++ b/css/pygments.css @@ -0,0 +1,61 @@ +.hll { background-color: #ffffcc } +.c { color: #408080; font-style: italic } /* Comment */ +.err { border: 1px solid #FF0000 } /* Error */ +.k { color: #008000; font-weight: bold } /* Keyword */ +.o { color: #666666 } /* Operator */ +.cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.cp { color: #BC7A00 } /* Comment.Preproc */ +.c1 { color: #408080; font-style: italic } /* Comment.Single */ +.cs { color: #408080; font-style: italic } /* Comment.Special */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #FF0000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #00A000 } /* Generic.Inserted */ +.go { color: #808080 } /* Generic.Output */ +.gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #0040D0 } /* Generic.Traceback */ +.kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #008000 } /* Keyword.Pseudo */ +.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #B00040 } /* Keyword.Type */ +.m { color: #666666 } /* Literal.Number */ +.s { color: #BA2121 } /* Literal.String */ +.na { color: #7D9029 } /* Name.Attribute */ +.nb { color: #008000 } /* Name.Builtin */ +.nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.no { color: #880000 } /* Name.Constant */ +.nd { color: #AA22FF } /* Name.Decorator */ +.ni { color: #999999; font-weight: bold } /* Name.Entity */ +.ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.nf { color: #0000FF } /* Name.Function */ +.nl { color: #A0A000 } /* Name.Label */ +.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.nt { color: #008000; font-weight: bold } /* Name.Tag */ +.nv { color: #19177C } /* Name.Variable */ +.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #666666 } /* Literal.Number.Float */ +.mh { color: #666666 } /* Literal.Number.Hex */ +.mi { color: #666666 } /* Literal.Number.Integer */ +.mo { color: #666666 } /* Literal.Number.Oct */ +.sb { color: #BA2121 } /* Literal.String.Backtick */ +.sc { color: #BA2121 } /* Literal.String.Char */ +.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #BA2121 } /* Literal.String.Double */ +.se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.sh { color: #BA2121 } /* Literal.String.Heredoc */ +.si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.sx { color: #008000 } /* Literal.String.Other */ +.sr { color: #BB6688 } /* Literal.String.Regex */ +.s1 { color: #BA2121 } /* Literal.String.Single */ +.ss { color: #19177C } /* Literal.String.Symbol */ +.bp { color: #008000 } /* Name.Builtin.Pseudo */ +.vc { color: #19177C } /* Name.Variable.Class */ +.vg { color: #19177C } /* Name.Variable.Global */ +.vi { color: #19177C } /* Name.Variable.Instance */ +.il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/css/sidenav.css b/css/sidenav.css new file mode 100644 index 00000000..0bd93f95 --- /dev/null +++ b/css/sidenav.css @@ -0,0 +1,157 @@ +.sidenav { + width: 228px; + margin: 30px 0 0; + padding: 0; + background-color: #fff; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 1px 4px rgba(0,0,0,.065); + -moz-box-shadow: 0 1px 4px rgba(0,0,0,.065); + box-shadow: 0 1px 4px rgba(0,0,0,.065); +} +.sidenav > li > a { + display: block; + width: 190px \9; + margin: 0 0 -1px; + padding: 8px 14px; + border: 1px solid #e5e5e5; +} +.sidenav > li:first-child > a { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} +.sidenav > li:last-child > a { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} +.sidenav > .active > a { + position: relative; + z-index: 2; + padding: 9px 15px; + border: 0; + text-shadow: 0 1px 0 rgba(0,0,0,.15); + -webkit-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); + -moz-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); + box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); +} + +/* Chevrons */ +.sidenav .icon-chevron-right { + float: right; + margin-top: 2px; + margin-right: -6px; + opacity: .25; +} +.sidenav > li > a:hover { + background-color: #f5f5f5; +} +.sidenav a:hover .icon-chevron-right { + opacity: .5; +} +.sidenav .active .icon-chevron-right, +.sidenav .active a:hover .icon-chevron-right { + background-image: url(../img/glyphicons-halflings-white.png); + opacity: 1; +} + + +/* +.sidenav.affix-bottom { + position: absolute; + top: auto; + bottom: 270px; +} +*/ + + +/* Responsive +-------------------------------------------------- */ + +/* Desktop large +------------------------- */ +@media (min-width: 1200px) { + .sidenav { + width: 258px; + top: 0; + margin-top: 30px; + margin-right: 0; + } + .sidenav > li > a { + width: 230px \9; /* Override the previous IE8-9 hack */ + } +} + +/* Desktop +------------------------- */ +@media (max-width: 980px) { + /* Unfloat brand */ + body > .navbar-fixed-top .brand { + float: left; + margin-left: 0; + padding-left: 10px; + padding-right: 10px; + } + + /* When affixed, space properly */ + .sidenav { + top: 0; + width: 218px; + margin-top: 30px; + margin-right: 0; + } +} + +/* Tablet to desktop +------------------------- */ +@media (min-width: 768px) and (max-width: 979px) { + /* Remove any padding from the body */ + body { + padding-top: 0; + } + /* Adjust sidenav width */ + .sidenav { + width: 166px; + margin-top: 20px; + } + .sidenav.affix { + top: 0; + } +} +@media (max-width:767px) { + .sidenav { + visibility: hidden + } +} + +/* Tablet +------------------------- */ +@media (max-width: 767px) { + /* Remove any padding from the body */ + body { + padding-top: 0; + } + + /* Sidenav */ + .sidenav { + width: auto; + margin-bottom: 20px; + } + /* + .sidenav.affix { + position: static; + width: auto; + top: 0; + } */ +} + +/* Landscape phones +------------------------- */ +@media (max-width: 480px) { + /* Remove padding above jumbotron */ + body { + padding-top: 0; + } +} diff --git a/css/site.css b/css/site.css new file mode 100644 index 00000000..06ed7fb9 --- /dev/null +++ b/css/site.css @@ -0,0 +1,171 @@ + /* GLOBAL STYLES + -------------------------------------------------- */ + /* Padding below the footer and lighter body text */ + + body { + color: #5a5a5a; + } + + + + /* CUSTOMIZE THE NAVBAR + -------------------------------------------------- */ + + /* Special class on .container surrounding .navbar, used for positioning it into place. */ + .navbar-wrapper { + position: relative; + z-index: 10; + margin-top: 20px; + margin-bottom: -90px; /* Negative margin to pull up carousel. 90px is roughly margins and height of navbar. */ + } + + /* Remove border and change up box shadow for more contrast */ + .navbar .navbar-inner { + border: 0; + -webkit-box-shadow: 0 2px 10px rgba(0,0,0,.25); + -moz-box-shadow: 0 2px 10px rgba(0,0,0,.25); + box-shadow: 0 2px 10px rgba(0,0,0,.25); + } + + /* Downsize the brand/project name a bit */ + .navbar .brand { + padding: 14px 20px 16px; /* Increase vertical padding to match navbar links */ + font-size: 16px; + font-weight: bold; + text-shadow: 0 -1px 0 rgba(0,0,0,.5); + } + + /* Navbar links: increase padding for taller navbar */ + .navbar .nav > li > a { + padding: 15px 20px; + } + + /* Offset the responsive button for proper vertical alignment */ + .navbar .btn-navbar { + margin-top: 10px; + } + + /* CUSTOMIZE THE NAVBAR + -------------------------------------------------- */ + + /* Carousel base class */ + .carousel { + margin-bottom: 60px; + } + + .carousel .container { + position: absolute; + right: 0; + bottom: 0; + left: 0; + } + + .carousel-control { + background-color: transparent; + border: 0; + font-size: 120px; + margin-top: 0; + text-shadow: 0 1px 1px rgba(0,0,0,.4); + } + + .carousel .item { + height: 500px; + } + .carousel img { + min-width: 100%; + height: 500px; + } + + .carousel-caption { + background-color: transparent; + position: static; + max-width: 550px; + padding: 0 20px; + margin-bottom: 100px; + } + .carousel-caption h1, + .carousel-caption .lead { + margin: 0; + line-height: 1.25; + color: #fff; + text-shadow: 0 1px 1px rgba(0,0,0,.4); + } + .carousel-caption .btn { + margin-top: 10px; + } + + /* MARKETING CONTENT + -------------------------------------------------- */ + + /* Center align the text within the three columns below the carousel */ + .marketing .span4 { + text-align: center; + } + .marketing h2 { + font-weight: normal; + } + .marketing .span4 p { + margin-left: 10px; + margin-right: 10px; + } + + /* RESPONSIVE CSS + -------------------------------------------------- */ + + @media (max-width: 979px) { + + .container.navbar-wrapper { + margin-bottom: 0; + width: auto; + } + .navbar-inner { + border-radius: 0; + margin: -20px 0; + } + + .carousel .item { + height: 500px; + } + .carousel img { + width: auto; + height: 500px; + } + } + + + @media (max-width: 767px) { + + .navbar-inner { + margin: -20px; + } + + .carousel { + margin-left: -20px; + margin-right: -20px; + } + .carousel .container { + + } + .carousel .item { + height: 300px; + } + .carousel img { + height: 300px; + } + .carousel-caption { + width: 65%; + padding: 0 70px; + margin-bottom: 40px; + } + .carousel-caption h1 { + font-size: 30px; + } + .carousel-caption .lead, + .carousel-caption .btn { + font-size: 18px; + } + + .marketing .span4 + .span4 { + margin-top: 40px; + } + } diff --git a/css/social.css b/css/social.css new file mode 100644 index 00000000..e30b7657 --- /dev/null +++ b/css/social.css @@ -0,0 +1,31 @@ +/* Social proof buttons from GitHub & Twitter */ +.social { + padding: 15px 0; + text-align: center; + background-color: #f5f5f5; + border-top: 1px solid #fff; + border-bottom: 1px solid #ddd; + margin-top: -60px; +} + +/* Quick links on Home */ +.social-buttons { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; +} +.social-buttons li { + display: inline-block; + padding: 5px 8px; + line-height: 1; + *display: inline; + *zoom: 1; +} + + +@media (max-width: 767px) { + .social { + margin: 0 -20px; + } +} diff --git a/dist/build/autogen/Paths_distributed_process_platform.hs b/dist/build/autogen/Paths_distributed_process_platform.hs new file mode 100644 index 00000000..170d157a --- /dev/null +++ b/dist/build/autogen/Paths_distributed_process_platform.hs @@ -0,0 +1,32 @@ +module Paths_distributed_process_platform ( + version, + getBinDir, getLibDir, getDataDir, getLibexecDir, + getDataFileName + ) where + +import qualified Control.Exception as Exception +import Data.Version (Version(..)) +import System.Environment (getEnv) +catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a +catchIO = Exception.catch + + +version :: Version +version = Version {versionBranch = [0,1,0], versionTags = []} +bindir, libdir, datadir, libexecdir :: FilePath + +bindir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/bin" +libdir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/lib" +datadir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/share" +libexecdir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/libexec" + +getBinDir, getLibDir, getDataDir, getLibexecDir :: IO FilePath +getBinDir = catchIO (getEnv "distributed_process_platform_bindir") (\_ -> return bindir) +getLibDir = catchIO (getEnv "distributed_process_platform_libdir") (\_ -> return libdir) +getDataDir = catchIO (getEnv "distributed_process_platform_datadir") (\_ -> return datadir) +getLibexecDir = catchIO (getEnv "distributed_process_platform_libexecdir") (\_ -> return libexecdir) + +getDataFileName :: FilePath -> IO FilePath +getDataFileName name = do + dir <- getDataDir + return (dir ++ "/" ++ name) diff --git a/dist/build/autogen/cabal_macros.h b/dist/build/autogen/cabal_macros.h new file mode 100644 index 00000000..f47225c8 --- /dev/null +++ b/dist/build/autogen/cabal_macros.h @@ -0,0 +1,58 @@ +/* DO NOT EDIT: This file is automatically generated by Cabal */ + +/* package base-4.5.1.0 */ +#define VERSION_base "4.5.1.0" +#define MIN_VERSION_base(major1,major2,minor) (\ + (major1) < 4 || \ + (major1) == 4 && (major2) < 5 || \ + (major1) == 4 && (major2) == 5 && (minor) <= 1) + +/* package binary-0.5.1.0 */ +#define VERSION_binary "0.5.1.0" +#define MIN_VERSION_binary(major1,major2,minor) (\ + (major1) < 0 || \ + (major1) == 0 && (major2) < 5 || \ + (major1) == 0 && (major2) == 5 && (minor) <= 1) + +/* package containers-0.4.2.1 */ +#define VERSION_containers "0.4.2.1" +#define MIN_VERSION_containers(major1,major2,minor) (\ + (major1) < 0 || \ + (major1) == 0 && (major2) < 4 || \ + (major1) == 0 && (major2) == 4 && (minor) <= 2) + +/* package derive-2.5.11 */ +#define VERSION_derive "2.5.11" +#define MIN_VERSION_derive(major1,major2,minor) (\ + (major1) < 2 || \ + (major1) == 2 && (major2) < 5 || \ + (major1) == 2 && (major2) == 5 && (minor) <= 11) + +/* package distributed-process-0.4.2 */ +#define VERSION_distributed_process "0.4.2" +#define MIN_VERSION_distributed_process(major1,major2,minor) (\ + (major1) < 0 || \ + (major1) == 0 && (major2) < 4 || \ + (major1) == 0 && (major2) == 4 && (minor) <= 2) + +/* package mtl-2.1.2 */ +#define VERSION_mtl "2.1.2" +#define MIN_VERSION_mtl(major1,major2,minor) (\ + (major1) < 2 || \ + (major1) == 2 && (major2) < 1 || \ + (major1) == 2 && (major2) == 1 && (minor) <= 2) + +/* package stm-2.4 */ +#define VERSION_stm "2.4" +#define MIN_VERSION_stm(major1,major2,minor) (\ + (major1) < 2 || \ + (major1) == 2 && (major2) < 4 || \ + (major1) == 2 && (major2) == 4 && (minor) <= 0) + +/* package transformers-0.3.0.0 */ +#define VERSION_transformers "0.3.0.0" +#define MIN_VERSION_transformers(major1,major2,minor) (\ + (major1) < 0 || \ + (major1) == 0 && (major2) < 3 || \ + (major1) == 0 && (major2) == 3 && (minor) <= 0) + diff --git a/dist/setup-config b/dist/setup-config new file mode 100644 index 00000000..57ae3b40 --- /dev/null +++ b/dist/setup-config @@ -0,0 +1,2 @@ +Saved package config for distributed-process-platform-0.1.0 written by Cabal-1.14.0 using ghc-7.4 +LocalBuildInfo {configFlags = ConfigFlags {configPrograms = [], configProgramPaths = [], configProgramArgs = [], configHcFlavor = Flag GHC, configHcPath = NoFlag, configHcPkg = NoFlag, configVanillaLib = Flag True, configProfLib = Flag True, configSharedLib = Flag False, configDynExe = Flag False, configProfExe = Flag False, configConfigureArgs = [], configOptimization = Flag NormalOptimisation, configProgPrefix = Flag "", configProgSuffix = Flag "", configInstallDirs = InstallDirs {prefix = Flag "/Users/t4/Library/Haskell/$compiler/lib/$pkgid", bindir = NoFlag, libdir = NoFlag, libsubdir = Flag "", dynlibdir = NoFlag, libexecdir = NoFlag, progdir = NoFlag, includedir = NoFlag, datadir = NoFlag, datasubdir = Flag "", docdir = Flag "$prefix/doc", mandir = NoFlag, htmldir = NoFlag, haddockdir = NoFlag}, configScratchDir = NoFlag, configExtraLibDirs = [], configExtraIncludeDirs = [], configDistPref = Flag "dist", configVerbosity = Flag Normal, configUserInstall = Flag True, configPackageDB = NoFlag, configGHCiLib = Flag True, configSplitObjs = Flag False, configStripExes = Flag True, configConstraints = [Dependency (PackageName "transformers") (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})),Dependency (PackageName "stm") (ThisVersion (Version {versionBranch = [2,4], versionTags = []})),Dependency (PackageName "mtl") (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []})),Dependency (PackageName "distributed-process") (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []})),Dependency (PackageName "derive") (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []})),Dependency (PackageName "containers") (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []})),Dependency (PackageName "binary") (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []})),Dependency (PackageName "base") (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))], configConfigurationsFlags = [], configTests = Flag False, configBenchmarks = Flag False, configLibCoverage = Flag False}, extraConfigArgs = [], installDirTemplates = InstallDirs {prefix = "/Users/t4/Library/Haskell/$compiler/lib/$pkgid", bindir = "$prefix/bin", libdir = "$prefix/lib", libsubdir = "", dynlibdir = "$libdir", libexecdir = "$prefix/libexec", progdir = "$libdir/hugs/programs", includedir = "$libdir/$libsubdir/include", datadir = "$prefix/share", datasubdir = "", docdir = "$prefix/doc", mandir = "$datadir/man", htmldir = "$docdir/html", haddockdir = "$htmldir"}, compiler = Compiler {compilerId = CompilerId GHC (Version {versionBranch = [7,4,2], versionTags = []}), compilerLanguages = [(Haskell98,"-XHaskell98"),(Haskell2010,"-XHaskell2010")], compilerExtensions = [(UnknownExtension "Haskell98","-XHaskell98"),(UnknownExtension "Haskell2010","-XHaskell2010"),(UnknownExtension "Unsafe","-XUnsafe"),(EnableExtension Trustworthy,"-XTrustworthy"),(EnableExtension Safe,"-XSafe"),(EnableExtension CPP,"-XCPP"),(DisableExtension CPP,"-XNoCPP"),(EnableExtension PostfixOperators,"-XPostfixOperators"),(DisableExtension PostfixOperators,"-XNoPostfixOperators"),(EnableExtension TupleSections,"-XTupleSections"),(DisableExtension TupleSections,"-XNoTupleSections"),(EnableExtension PatternGuards,"-XPatternGuards"),(DisableExtension PatternGuards,"-XNoPatternGuards"),(EnableExtension UnicodeSyntax,"-XUnicodeSyntax"),(DisableExtension UnicodeSyntax,"-XNoUnicodeSyntax"),(EnableExtension MagicHash,"-XMagicHash"),(DisableExtension MagicHash,"-XNoMagicHash"),(EnableExtension PolymorphicComponents,"-XPolymorphicComponents"),(DisableExtension PolymorphicComponents,"-XNoPolymorphicComponents"),(EnableExtension ExistentialQuantification,"-XExistentialQuantification"),(DisableExtension ExistentialQuantification,"-XNoExistentialQuantification"),(EnableExtension KindSignatures,"-XKindSignatures"),(DisableExtension KindSignatures,"-XNoKindSignatures"),(EnableExtension EmptyDataDecls,"-XEmptyDataDecls"),(DisableExtension EmptyDataDecls,"-XNoEmptyDataDecls"),(EnableExtension ParallelListComp,"-XParallelListComp"),(DisableExtension ParallelListComp,"-XNoParallelListComp"),(EnableExtension TransformListComp,"-XTransformListComp"),(DisableExtension TransformListComp,"-XNoTransformListComp"),(UnknownExtension "MonadComprehensions","-XMonadComprehensions"),(UnknownExtension "NoMonadComprehensions","-XNoMonadComprehensions"),(EnableExtension ForeignFunctionInterface,"-XForeignFunctionInterface"),(DisableExtension ForeignFunctionInterface,"-XNoForeignFunctionInterface"),(EnableExtension UnliftedFFITypes,"-XUnliftedFFITypes"),(DisableExtension UnliftedFFITypes,"-XNoUnliftedFFITypes"),(UnknownExtension "InterruptibleFFI","-XInterruptibleFFI"),(UnknownExtension "NoInterruptibleFFI","-XNoInterruptibleFFI"),(UnknownExtension "CApiFFI","-XCApiFFI"),(UnknownExtension "NoCApiFFI","-XNoCApiFFI"),(EnableExtension GHCForeignImportPrim,"-XGHCForeignImportPrim"),(DisableExtension GHCForeignImportPrim,"-XNoGHCForeignImportPrim"),(EnableExtension LiberalTypeSynonyms,"-XLiberalTypeSynonyms"),(DisableExtension LiberalTypeSynonyms,"-XNoLiberalTypeSynonyms"),(EnableExtension Rank2Types,"-XRank2Types"),(DisableExtension Rank2Types,"-XNoRank2Types"),(EnableExtension RankNTypes,"-XRankNTypes"),(DisableExtension RankNTypes,"-XNoRankNTypes"),(EnableExtension ImpredicativeTypes,"-XImpredicativeTypes"),(DisableExtension ImpredicativeTypes,"-XNoImpredicativeTypes"),(EnableExtension TypeOperators,"-XTypeOperators"),(DisableExtension TypeOperators,"-XNoTypeOperators"),(EnableExtension RecursiveDo,"-XRecursiveDo"),(DisableExtension RecursiveDo,"-XNoRecursiveDo"),(EnableExtension DoRec,"-XDoRec"),(DisableExtension DoRec,"-XNoDoRec"),(EnableExtension Arrows,"-XArrows"),(DisableExtension Arrows,"-XNoArrows"),(UnknownExtension "ParallelArrays","-XParallelArrays"),(UnknownExtension "NoParallelArrays","-XNoParallelArrays"),(EnableExtension TemplateHaskell,"-XTemplateHaskell"),(DisableExtension TemplateHaskell,"-XNoTemplateHaskell"),(EnableExtension QuasiQuotes,"-XQuasiQuotes"),(DisableExtension QuasiQuotes,"-XNoQuasiQuotes"),(EnableExtension ImplicitPrelude,"-XImplicitPrelude"),(DisableExtension ImplicitPrelude,"-XNoImplicitPrelude"),(EnableExtension RecordWildCards,"-XRecordWildCards"),(DisableExtension RecordWildCards,"-XNoRecordWildCards"),(EnableExtension NamedFieldPuns,"-XNamedFieldPuns"),(DisableExtension NamedFieldPuns,"-XNoNamedFieldPuns"),(EnableExtension RecordPuns,"-XRecordPuns"),(DisableExtension RecordPuns,"-XNoRecordPuns"),(EnableExtension DisambiguateRecordFields,"-XDisambiguateRecordFields"),(DisableExtension DisambiguateRecordFields,"-XNoDisambiguateRecordFields"),(EnableExtension OverloadedStrings,"-XOverloadedStrings"),(DisableExtension OverloadedStrings,"-XNoOverloadedStrings"),(EnableExtension GADTs,"-XGADTs"),(DisableExtension GADTs,"-XNoGADTs"),(EnableExtension GADTSyntax,"-XGADTSyntax"),(DisableExtension GADTSyntax,"-XNoGADTSyntax"),(EnableExtension ViewPatterns,"-XViewPatterns"),(DisableExtension ViewPatterns,"-XNoViewPatterns"),(EnableExtension TypeFamilies,"-XTypeFamilies"),(DisableExtension TypeFamilies,"-XNoTypeFamilies"),(EnableExtension BangPatterns,"-XBangPatterns"),(DisableExtension BangPatterns,"-XNoBangPatterns"),(EnableExtension MonomorphismRestriction,"-XMonomorphismRestriction"),(DisableExtension MonomorphismRestriction,"-XNoMonomorphismRestriction"),(EnableExtension NPlusKPatterns,"-XNPlusKPatterns"),(DisableExtension NPlusKPatterns,"-XNoNPlusKPatterns"),(EnableExtension DoAndIfThenElse,"-XDoAndIfThenElse"),(DisableExtension DoAndIfThenElse,"-XNoDoAndIfThenElse"),(EnableExtension RebindableSyntax,"-XRebindableSyntax"),(DisableExtension RebindableSyntax,"-XNoRebindableSyntax"),(EnableExtension ConstraintKinds,"-XConstraintKinds"),(DisableExtension ConstraintKinds,"-XNoConstraintKinds"),(UnknownExtension "PolyKinds","-XPolyKinds"),(UnknownExtension "NoPolyKinds","-XNoPolyKinds"),(UnknownExtension "DataKinds","-XDataKinds"),(UnknownExtension "NoDataKinds","-XNoDataKinds"),(EnableExtension MonoPatBinds,"-XMonoPatBinds"),(DisableExtension MonoPatBinds,"-XNoMonoPatBinds"),(EnableExtension ExplicitForAll,"-XExplicitForAll"),(DisableExtension ExplicitForAll,"-XNoExplicitForAll"),(UnknownExtension "AlternativeLayoutRule","-XAlternativeLayoutRule"),(UnknownExtension "NoAlternativeLayoutRule","-XNoAlternativeLayoutRule"),(UnknownExtension "AlternativeLayoutRuleTransitional","-XAlternativeLayoutRuleTransitional"),(UnknownExtension "NoAlternativeLayoutRuleTransitional","-XNoAlternativeLayoutRuleTransitional"),(EnableExtension DatatypeContexts,"-XDatatypeContexts"),(DisableExtension DatatypeContexts,"-XNoDatatypeContexts"),(EnableExtension NondecreasingIndentation,"-XNondecreasingIndentation"),(DisableExtension NondecreasingIndentation,"-XNoNondecreasingIndentation"),(UnknownExtension "RelaxedLayout","-XRelaxedLayout"),(UnknownExtension "NoRelaxedLayout","-XNoRelaxedLayout"),(UnknownExtension "TraditionalRecordSyntax","-XTraditionalRecordSyntax"),(UnknownExtension "NoTraditionalRecordSyntax","-XNoTraditionalRecordSyntax"),(EnableExtension MonoLocalBinds,"-XMonoLocalBinds"),(DisableExtension MonoLocalBinds,"-XNoMonoLocalBinds"),(EnableExtension RelaxedPolyRec,"-XRelaxedPolyRec"),(DisableExtension RelaxedPolyRec,"-XNoRelaxedPolyRec"),(EnableExtension ExtendedDefaultRules,"-XExtendedDefaultRules"),(DisableExtension ExtendedDefaultRules,"-XNoExtendedDefaultRules"),(EnableExtension ImplicitParams,"-XImplicitParams"),(DisableExtension ImplicitParams,"-XNoImplicitParams"),(EnableExtension ScopedTypeVariables,"-XScopedTypeVariables"),(DisableExtension ScopedTypeVariables,"-XNoScopedTypeVariables"),(EnableExtension PatternSignatures,"-XPatternSignatures"),(DisableExtension PatternSignatures,"-XNoPatternSignatures"),(EnableExtension UnboxedTuples,"-XUnboxedTuples"),(DisableExtension UnboxedTuples,"-XNoUnboxedTuples"),(EnableExtension StandaloneDeriving,"-XStandaloneDeriving"),(DisableExtension StandaloneDeriving,"-XNoStandaloneDeriving"),(EnableExtension DeriveDataTypeable,"-XDeriveDataTypeable"),(DisableExtension DeriveDataTypeable,"-XNoDeriveDataTypeable"),(EnableExtension DeriveFunctor,"-XDeriveFunctor"),(DisableExtension DeriveFunctor,"-XNoDeriveFunctor"),(EnableExtension DeriveTraversable,"-XDeriveTraversable"),(DisableExtension DeriveTraversable,"-XNoDeriveTraversable"),(EnableExtension DeriveFoldable,"-XDeriveFoldable"),(DisableExtension DeriveFoldable,"-XNoDeriveFoldable"),(UnknownExtension "DeriveGeneric","-XDeriveGeneric"),(UnknownExtension "NoDeriveGeneric","-XNoDeriveGeneric"),(UnknownExtension "DefaultSignatures","-XDefaultSignatures"),(UnknownExtension "NoDefaultSignatures","-XNoDefaultSignatures"),(EnableExtension TypeSynonymInstances,"-XTypeSynonymInstances"),(DisableExtension TypeSynonymInstances,"-XNoTypeSynonymInstances"),(EnableExtension FlexibleContexts,"-XFlexibleContexts"),(DisableExtension FlexibleContexts,"-XNoFlexibleContexts"),(EnableExtension FlexibleInstances,"-XFlexibleInstances"),(DisableExtension FlexibleInstances,"-XNoFlexibleInstances"),(EnableExtension ConstrainedClassMethods,"-XConstrainedClassMethods"),(DisableExtension ConstrainedClassMethods,"-XNoConstrainedClassMethods"),(EnableExtension MultiParamTypeClasses,"-XMultiParamTypeClasses"),(DisableExtension MultiParamTypeClasses,"-XNoMultiParamTypeClasses"),(EnableExtension FunctionalDependencies,"-XFunctionalDependencies"),(DisableExtension FunctionalDependencies,"-XNoFunctionalDependencies"),(EnableExtension GeneralizedNewtypeDeriving,"-XGeneralizedNewtypeDeriving"),(DisableExtension GeneralizedNewtypeDeriving,"-XNoGeneralizedNewtypeDeriving"),(EnableExtension OverlappingInstances,"-XOverlappingInstances"),(DisableExtension OverlappingInstances,"-XNoOverlappingInstances"),(EnableExtension UndecidableInstances,"-XUndecidableInstances"),(DisableExtension UndecidableInstances,"-XNoUndecidableInstances"),(EnableExtension IncoherentInstances,"-XIncoherentInstances"),(DisableExtension IncoherentInstances,"-XNoIncoherentInstances"),(EnableExtension PackageImports,"-XPackageImports"),(DisableExtension PackageImports,"-XNoPackageImports")]}, buildDir = "dist/build", scratchDir = "dist/scratch", libraryConfig = Just (ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}), executableConfigs = [], compBuildOrder = [CLibName], testSuiteConfigs = [("GenServerTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}),("AsyncTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}),("PrimitivesTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}),("TimerTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]})], benchmarkConfigs = [], installedPkgs = PackageIndex (fromList [(InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageInfo {installedPackageId = InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297", sourcePackageId = PackageIdentifier {pkgName = PackageName "array", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Mutable and immutable arrays", description = "This package defines the classes @IArray@ of immutable arrays and\n@MArray@ of arrays mutable within appropriate monads, as well as\nsome instances of these classes.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Array","Base"],ModuleName ["Data","Array","IArray"],ModuleName ["Data","Array","IO"],ModuleName ["Data","Array","IO","Safe"],ModuleName ["Data","Array","IO","Internals"],ModuleName ["Data","Array","MArray"],ModuleName ["Data","Array","MArray","Safe"],ModuleName ["Data","Array","ST"],ModuleName ["Data","Array","ST","Safe"],ModuleName ["Data","Array","Storable"],ModuleName ["Data","Array","Storable","Safe"],ModuleName ["Data","Array","Storable","Internals"],ModuleName ["Data","Array","Unboxed"],ModuleName ["Data","Array","Unsafe"],ModuleName ["Data","Array"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], hsLibraries = ["HSarray-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0/array.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0"]}),(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageInfo {installedPackageId = InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd", sourcePackageId = PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Basic libraries", description = "This package contains the Prelude and its support libraries,\nand a large collection of useful libraries ranging from data\nstructures to parsing combinators and debugging utilities.", category = "", exposed = True, exposedModules = [ModuleName ["Foreign","Concurrent"],ModuleName ["GHC","Arr"],ModuleName ["GHC","Base"],ModuleName ["GHC","Conc"],ModuleName ["GHC","Conc","IO"],ModuleName ["GHC","Conc","Signal"],ModuleName ["GHC","Conc","Sync"],ModuleName ["GHC","ConsoleHandler"],ModuleName ["GHC","Constants"],ModuleName ["GHC","Desugar"],ModuleName ["GHC","Enum"],ModuleName ["GHC","Environment"],ModuleName ["GHC","Err"],ModuleName ["GHC","Exception"],ModuleName ["GHC","Exts"],ModuleName ["GHC","Fingerprint"],ModuleName ["GHC","Fingerprint","Type"],ModuleName ["GHC","Float"],ModuleName ["GHC","Float","ConversionUtils"],ModuleName ["GHC","Float","RealFracMethods"],ModuleName ["GHC","Foreign"],ModuleName ["GHC","ForeignPtr"],ModuleName ["GHC","Handle"],ModuleName ["GHC","IO"],ModuleName ["GHC","IO","Buffer"],ModuleName ["GHC","IO","BufferedIO"],ModuleName ["GHC","IO","Device"],ModuleName ["GHC","IO","Encoding"],ModuleName ["GHC","IO","Encoding","CodePage"],ModuleName ["GHC","IO","Encoding","Failure"],ModuleName ["GHC","IO","Encoding","Iconv"],ModuleName ["GHC","IO","Encoding","Latin1"],ModuleName ["GHC","IO","Encoding","Types"],ModuleName ["GHC","IO","Encoding","UTF16"],ModuleName ["GHC","IO","Encoding","UTF32"],ModuleName ["GHC","IO","Encoding","UTF8"],ModuleName ["GHC","IO","Exception"],ModuleName ["GHC","IO","FD"],ModuleName ["GHC","IO","Handle"],ModuleName ["GHC","IO","Handle","FD"],ModuleName ["GHC","IO","Handle","Internals"],ModuleName ["GHC","IO","Handle","Text"],ModuleName ["GHC","IO","Handle","Types"],ModuleName ["GHC","IO","IOMode"],ModuleName ["GHC","IOArray"],ModuleName ["GHC","IOBase"],ModuleName ["GHC","IORef"],ModuleName ["GHC","Int"],ModuleName ["GHC","List"],ModuleName ["GHC","MVar"],ModuleName ["GHC","Num"],ModuleName ["GHC","PArr"],ModuleName ["GHC","Pack"],ModuleName ["GHC","Ptr"],ModuleName ["GHC","Read"],ModuleName ["GHC","Real"],ModuleName ["GHC","ST"],ModuleName ["GHC","Stack"],ModuleName ["GHC","Stats"],ModuleName ["GHC","Show"],ModuleName ["GHC","Stable"],ModuleName ["GHC","Storable"],ModuleName ["GHC","STRef"],ModuleName ["GHC","TopHandler"],ModuleName ["GHC","Unicode"],ModuleName ["GHC","Weak"],ModuleName ["GHC","Word"],ModuleName ["System","Timeout"],ModuleName ["GHC","Event"],ModuleName ["Control","Applicative"],ModuleName ["Control","Arrow"],ModuleName ["Control","Category"],ModuleName ["Control","Concurrent"],ModuleName ["Control","Concurrent","Chan"],ModuleName ["Control","Concurrent","MVar"],ModuleName ["Control","Concurrent","QSem"],ModuleName ["Control","Concurrent","QSemN"],ModuleName ["Control","Concurrent","SampleVar"],ModuleName ["Control","Exception"],ModuleName ["Control","Exception","Base"],ModuleName ["Control","OldException"],ModuleName ["Control","Monad"],ModuleName ["Control","Monad","Fix"],ModuleName ["Control","Monad","Instances"],ModuleName ["Control","Monad","ST"],ModuleName ["Control","Monad","ST","Safe"],ModuleName ["Control","Monad","ST","Unsafe"],ModuleName ["Control","Monad","ST","Lazy"],ModuleName ["Control","Monad","ST","Lazy","Safe"],ModuleName ["Control","Monad","ST","Lazy","Unsafe"],ModuleName ["Control","Monad","ST","Strict"],ModuleName ["Control","Monad","Zip"],ModuleName ["Data","Bits"],ModuleName ["Data","Bool"],ModuleName ["Data","Char"],ModuleName ["Data","Complex"],ModuleName ["Data","Dynamic"],ModuleName ["Data","Either"],ModuleName ["Data","Eq"],ModuleName ["Data","Data"],ModuleName ["Data","Fixed"],ModuleName ["Data","Foldable"],ModuleName ["Data","Function"],ModuleName ["Data","Functor"],ModuleName ["Data","HashTable"],ModuleName ["Data","IORef"],ModuleName ["Data","Int"],ModuleName ["Data","Ix"],ModuleName ["Data","List"],ModuleName ["Data","Maybe"],ModuleName ["Data","Monoid"],ModuleName ["Data","Ord"],ModuleName ["Data","Ratio"],ModuleName ["Data","STRef"],ModuleName ["Data","STRef","Lazy"],ModuleName ["Data","STRef","Strict"],ModuleName ["Data","String"],ModuleName ["Data","Traversable"],ModuleName ["Data","Tuple"],ModuleName ["Data","Typeable"],ModuleName ["Data","Typeable","Internal"],ModuleName ["Data","Unique"],ModuleName ["Data","Version"],ModuleName ["Data","Word"],ModuleName ["Debug","Trace"],ModuleName ["Foreign"],ModuleName ["Foreign","C"],ModuleName ["Foreign","C","Error"],ModuleName ["Foreign","C","String"],ModuleName ["Foreign","C","Types"],ModuleName ["Foreign","ForeignPtr"],ModuleName ["Foreign","ForeignPtr","Safe"],ModuleName ["Foreign","ForeignPtr","Unsafe"],ModuleName ["Foreign","Marshal"],ModuleName ["Foreign","Marshal","Alloc"],ModuleName ["Foreign","Marshal","Array"],ModuleName ["Foreign","Marshal","Error"],ModuleName ["Foreign","Marshal","Pool"],ModuleName ["Foreign","Marshal","Safe"],ModuleName ["Foreign","Marshal","Utils"],ModuleName ["Foreign","Marshal","Unsafe"],ModuleName ["Foreign","Ptr"],ModuleName ["Foreign","Safe"],ModuleName ["Foreign","StablePtr"],ModuleName ["Foreign","Storable"],ModuleName ["Numeric"],ModuleName ["Prelude"],ModuleName ["System","Console","GetOpt"],ModuleName ["System","CPUTime"],ModuleName ["System","Environment"],ModuleName ["System","Exit"],ModuleName ["System","IO"],ModuleName ["System","IO","Error"],ModuleName ["System","IO","Unsafe"],ModuleName ["System","Info"],ModuleName ["System","Mem"],ModuleName ["System","Mem","StableName"],ModuleName ["System","Mem","Weak"],ModuleName ["System","Posix","Internals"],ModuleName ["System","Posix","Types"],ModuleName ["Text","ParserCombinators","ReadP"],ModuleName ["Text","ParserCombinators","ReadPrec"],ModuleName ["Text","Printf"],ModuleName ["Text","Read"],ModuleName ["Text","Read","Lex"],ModuleName ["Text","Show"],ModuleName ["Text","Show","Functions"],ModuleName ["Unsafe","Coerce"]], hiddenModules = [ModuleName ["GHC","Event","Array"],ModuleName ["GHC","Event","Clock"],ModuleName ["GHC","Event","Control"],ModuleName ["GHC","Event","EPoll"],ModuleName ["GHC","Event","IntMap"],ModuleName ["GHC","Event","Internal"],ModuleName ["GHC","Event","KQueue"],ModuleName ["GHC","Event","Manager"],ModuleName ["GHC","Event","PSQ"],ModuleName ["GHC","Event","Poll"],ModuleName ["GHC","Event","Thread"],ModuleName ["GHC","Event","Unique"],ModuleName ["Control","Monad","ST","Imp"],ModuleName ["Control","Monad","ST","Lazy","Imp"],ModuleName ["Foreign","ForeignPtr","Imp"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], hsLibraries = ["HSbase-4.5.1.0"], extraLibraries = ["iconv"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0/include"], includes = ["HsBase.h"], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0/base.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0"]}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageInfo {installedPackageId = InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b", sourcePackageId = PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Lennart Kolmodin, Don Stewart ", author = "Lennart Kolmodin ", stability = "provisional", homepage = "http://code.haskell.org/binary/", pkgUrl = "", synopsis = "Binary serialisation for Haskell values using lazy ByteStrings", description = "Efficient, pure binary serialisation using lazy ByteStrings.\nHaskell values may be encoded to and from binary formats,\nwritten to disk as binary, or sent over the network.\nSerialisation speeds of over 1 G\\/sec have been observed,\nso this library should be suitable for high performance\nscenarios.", category = "Data, Parsing", exposed = True, exposedModules = [ModuleName ["Data","Binary"],ModuleName ["Data","Binary","Put"],ModuleName ["Data","Binary","Get"],ModuleName ["Data","Binary","Builder"],ModuleName ["Data","Binary","Builder","Internal"]], hiddenModules = [ModuleName ["Data","Binary","Builder","Base"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], hsLibraries = ["HSbinary-0.5.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0/binary.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0"]}),(InstalledPackageId "builtin_rts",InstalledPackageInfo {installedPackageId = InstalledPackageId "builtin_rts", sourcePackageId = PackageIdentifier {pkgName = PackageName "rts", pkgVersion = Version {versionBranch = [1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "glasgow-haskell-users@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "", category = "", exposed = True, exposedModules = [], hiddenModules = [], trusted = False, importDirs = [], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2"], hsLibraries = ["HSrts"], extraLibraries = ["m","dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/include"], includes = ["Stg.h"], depends = [], hugsOptions = [], ccOptions = [], ldOptions = ["-u","_ghczmprim_GHCziTypes_Izh_static_info","-u","_ghczmprim_GHCziTypes_Czh_static_info","-u","_ghczmprim_GHCziTypes_Fzh_static_info","-u","_ghczmprim_GHCziTypes_Dzh_static_info","-u","_base_GHCziPtr_Ptr_static_info","-u","_base_GHCziWord_Wzh_static_info","-u","_base_GHCziInt_I8zh_static_info","-u","_base_GHCziInt_I16zh_static_info","-u","_base_GHCziInt_I32zh_static_info","-u","_base_GHCziInt_I64zh_static_info","-u","_base_GHCziWord_W8zh_static_info","-u","_base_GHCziWord_W16zh_static_info","-u","_base_GHCziWord_W32zh_static_info","-u","_base_GHCziWord_W64zh_static_info","-u","_base_GHCziStable_StablePtr_static_info","-u","_ghczmprim_GHCziTypes_Izh_con_info","-u","_ghczmprim_GHCziTypes_Czh_con_info","-u","_ghczmprim_GHCziTypes_Fzh_con_info","-u","_ghczmprim_GHCziTypes_Dzh_con_info","-u","_base_GHCziPtr_Ptr_con_info","-u","_base_GHCziPtr_FunPtr_con_info","-u","_base_GHCziStable_StablePtr_con_info","-u","_ghczmprim_GHCziTypes_False_closure","-u","_ghczmprim_GHCziTypes_True_closure","-u","_base_GHCziPack_unpackCString_closure","-u","_base_GHCziIOziException_stackOverflow_closure","-u","_base_GHCziIOziException_heapOverflow_closure","-u","_base_ControlziExceptionziBase_nonTermination_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnMVar_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnSTM_closure","-u","_base_ControlziExceptionziBase_nestedAtomically_closure","-u","_base_GHCziWeak_runFinalizzerBatch_closure","-u","_base_GHCziTopHandler_flushStdHandles_closure","-u","_base_GHCziTopHandler_runIO_closure","-u","_base_GHCziTopHandler_runNonIO_closure","-u","_base_GHCziConcziIO_ensureIOManagerIsRunning_closure","-u","_base_GHCziConcziSync_runSparks_closure","-u","_base_GHCziConcziSignal_runHandlers_closure","-Wl,-search_paths_first"], frameworkDirs = [], frameworks = [], haddockInterfaces = [], haddockHTMLs = []}),(InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageInfo {installedPackageId = InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065", sourcePackageId = PackageIdentifier {pkgName = PackageName "bytestring", pkgVersion = Version {versionBranch = [0,9,2,1], versionTags = []}}, license = BSD3, copyright = "Copyright (c) Don Stewart 2005-2009,\n(c) Duncan Coutts 2006-2009,\n(c) David Roundy 2003-2005.", maintainer = "dons00@gmail.com, duncan@community.haskell.org", author = "Don Stewart, Duncan Coutts", stability = "", homepage = "http://www.cse.unsw.edu.au/~dons/fps.html", pkgUrl = "", synopsis = "Fast, packed, strict and lazy byte arrays with a list interface", description = "A time and space-efficient implementation of byte vectors using\npacked Word8 arrays, suitable for high performance use, both in terms\nof large data quantities, or high speed requirements. Byte vectors\nare encoded as strict 'Word8' arrays of bytes, and lazy lists of\nstrict chunks, held in a 'ForeignPtr', and can be passed between C\nand Haskell with little effort.\n\nTest coverage data for this library is available at:\n", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","ByteString"],ModuleName ["Data","ByteString","Char8"],ModuleName ["Data","ByteString","Unsafe"],ModuleName ["Data","ByteString","Internal"],ModuleName ["Data","ByteString","Lazy"],ModuleName ["Data","ByteString","Lazy","Char8"],ModuleName ["Data","ByteString","Lazy","Internal"],ModuleName ["Data","ByteString","Fusion"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], hsLibraries = ["HSbytestring-0.9.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1/include"], includes = ["fpstring.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1/bytestring.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1"]}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageInfo {installedPackageId = InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce", sourcePackageId = PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "fox@ucw.cz", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Assorted concrete container types", description = "This package contains efficient general-purpose implementations\nof various basic immutable container types. The declared cost of\neach operation is either worst-case or amortized, but remains\nvalid even if structures are shared.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Graph"],ModuleName ["Data","Sequence"],ModuleName ["Data","Tree"],ModuleName ["Data","IntMap"],ModuleName ["Data","IntSet"],ModuleName ["Data","Map"],ModuleName ["Data","Set"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], hsLibraries = ["HScontainers-0.4.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1/containers.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1"]}),(InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4",InstalledPackageInfo {installedPackageId = InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "cpphs", pkgVersion = Version {versionBranch = [1,14], versionTags = []}}, license = LGPL Nothing, copyright = "2004-2012, Malcolm Wallace", maintainer = "Malcolm Wallace ", author = "Malcolm Wallace ", stability = "", homepage = "http://haskell.org/cpphs/", pkgUrl = "", synopsis = "A liberalised re-implementation of cpp, the C pre-processor.", description = "Cpphs is a re-implementation of the C pre-processor that is both\nmore compatible with Haskell, and itself written in Haskell so\nthat it can be distributed with compilers.\n\nThis version of the C pre-processor is pretty-much\nfeature-complete and compatible with traditional (K&R)\npre-processors. Additional features include: a plain-text mode;\nan option to unlit literate code files; and an option to turn\noff macro-expansion.", category = "Development", exposed = True, exposedModules = [ModuleName ["Language","Preprocessor","Cpphs"],ModuleName ["Language","Preprocessor","Unlit"]], hiddenModules = [ModuleName ["Language","Preprocessor","Cpphs","CppIfdef"],ModuleName ["Language","Preprocessor","Cpphs","HashDefine"],ModuleName ["Language","Preprocessor","Cpphs","MacroPass"],ModuleName ["Language","Preprocessor","Cpphs","Options"],ModuleName ["Language","Preprocessor","Cpphs","Position"],ModuleName ["Language","Preprocessor","Cpphs","ReadFirst"],ModuleName ["Language","Preprocessor","Cpphs","RunCpphs"],ModuleName ["Language","Preprocessor","Cpphs","SymTab"],ModuleName ["Language","Preprocessor","Cpphs","Tokenise"],ModuleName ["Text","ParserCombinators","HuttonMeijer"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], hsLibraries = ["HScpphs-1.14"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html/cpphs.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html"]}),(InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb",InstalledPackageInfo {installedPackageId = InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb", sourcePackageId = PackageIdentifier {pkgName = PackageName "data-accessor", pkgVersion = Version {versionBranch = [0,2,2,3], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Henning Thielemann ", author = "Henning Thielemann , Luke Palmer ", stability = "", homepage = "http://www.haskell.org/haskellwiki/Record_access", pkgUrl = "", synopsis = "Utilities for accessing and manipulating fields of records", description = "In Haskell 98 the name of a record field\nis automatically also the name of a function which gets the value\nof the according field.\nE.g. if we have\n\ndata Pair a b = Pair\nfirst :: a, second :: b\n\nthen\n\n> first :: Pair a b -> a\n> second :: Pair a b -> b\n\nHowever for setting or modifying a field value\nwe need to use some syntactic sugar, which is often clumsy.\n\nmodifyFirst :: (a -> a) -> (Pair a b -> Pair a b)\nmodifyFirst f r\\@(Pair\nfirst=a\n) = r\nfirst = f a\n\nWith this package you can define record field accessors\nwhich allow setting, getting and modifying values easily.\nThe package clearly demonstrates the power of the functional approach:\nYou can combine accessors of a record and sub-records,\nto make the access look like the fields of the sub-record belong to the main record.\n\nExample:\n\n> *Data.Accessor.Example> (first^:second^=10) (('b',7),\"hallo\")\n> (('b',10),\"hallo\")\n\nYou can easily manipulate record fields in a 'Control.Monad.State.State' monad,\nyou can easily code 'Show' instances that use the Accessor syntax\nand you can parse binary streams into records.\nSee @Data.Accessor.Example@ for demonstration of all features.\n\nIt would be great if in revised Haskell versions the names of record fields\nare automatically 'Data.Accessor.Accessor's\nrather than plain @get@ functions.\nFor now, the package @data-accessor-template@ provides Template Haskell functions\nfor automated generation of 'Data.Acesssor.Accessor's.\nSee also the other @data-accessor@ packages\nthat provide an Accessor interface to other data types.\nThe package @enumset@ provides accessors to bit-packed records.\n\nFor similar packages see @lenses@ and @fclabel@.\nA related concept are editors\n.\nEditors only consist of a modify method\n(and @modify@ applied to a 'const' function is a @set@ function).\nThis way, they can modify all function values of a function at once,\nwhereas an accessor can only change a single function value,\nsay, it can change @f 0 = 1@ to @f 0 = 2@.\nThis way, editors can even change the type of a record or a function.\nAn Arrow instance can be defined for editors,\nbut for accessors only a Category instance is possible ('(.)' method).\nThe reason is the @arr@ method of the @Arrow@ class,\nthat conflicts with the two-way nature (set and get) of accessors.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Accessor"],ModuleName ["Data","Accessor","Basic"],ModuleName ["Data","Accessor","Container"],ModuleName ["Data","Accessor","Show"],ModuleName ["Data","Accessor","Tuple"],ModuleName ["Data","Accessor","BinaryRead"],ModuleName ["Data","Accessor","MonadState"]], hiddenModules = [ModuleName ["Data","Accessor","Example"],ModuleName ["Data","Accessor","Private"],ModuleName ["Data","Accessor","MonadStatePrivate"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], hsLibraries = ["HSdata-accessor-0.2.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html/data-accessor.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html"]}),(InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageInfo {installedPackageId = InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6", sourcePackageId = PackageIdentifier {pkgName = PackageName "deepseq", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Deep evaluation of data structures", description = "This package provides methods for fully evaluating data structures\n(\\\"deep evaluation\\\"). Deep evaluation is often used for adding\nstrictness to a program, e.g. in order to force pending exceptions,\nremove space leaks, or force lazy I/O to happen. It is also useful\nin parallel programs, to ensure pending work does not migrate to the\nwrong thread.\n\nThe primary use of this package is via the 'deepseq' function, a\n\\\"deep\\\" version of 'seq'. It is implemented on top of an 'NFData'\ntypeclass (\\\"Normal Form Data\\\", data structures with no unevaluated\ncomponents) which defines strategies for fully evaluating different\ndata types.\n", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","DeepSeq"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], hsLibraries = ["HSdeepseq-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0/deepseq.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0"]}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",InstalledPackageInfo {installedPackageId = InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/derive/", pkgUrl = "", synopsis = "A program and library to derive instances for data types", description = "Data.Derive is a library and a tool for deriving instances for Haskell programs.\nIt is designed to work with custom derivations, SYB and Template Haskell mechanisms.\nThe tool requires GHC, but the generated code is portable to all compilers.\nWe see this tool as a competitor to DrIFT.", category = "Development", exposed = True, exposedModules = [ModuleName ["Data","DeriveMain"],ModuleName ["Data","DeriveTH"],ModuleName ["Data","DeriveDSL"],ModuleName ["Data","Derive","All"],ModuleName ["Data","Derive","DSL","Apply"],ModuleName ["Data","Derive","DSL","Derive"],ModuleName ["Data","Derive","DSL","DSL"],ModuleName ["Data","Derive","DSL","HSE"],ModuleName ["Data","Derive","DSL","SYB"],ModuleName ["Data","Derive","Instance","Arities"],ModuleName ["Data","Derive","Class","Arities"],ModuleName ["Data","Derive","Class","Default"],ModuleName ["Language","Haskell"],ModuleName ["Language","Haskell","Convert"],ModuleName ["Language","Haskell","TH","All"],ModuleName ["Language","Haskell","TH","Compat"],ModuleName ["Language","Haskell","TH","Data"],ModuleName ["Language","Haskell","TH","ExpandSynonym"],ModuleName ["Language","Haskell","TH","FixedPpr"],ModuleName ["Language","Haskell","TH","Helper"],ModuleName ["Language","Haskell","TH","Peephole"],ModuleName ["Data","Derive","Arbitrary"],ModuleName ["Data","Derive","ArbitraryOld"],ModuleName ["Data","Derive","Arities"],ModuleName ["Data","Derive","Binary"],ModuleName ["Data","Derive","BinaryDefer"],ModuleName ["Data","Derive","Bounded"],ModuleName ["Data","Derive","Data"],ModuleName ["Data","Derive","DataAbstract"],ModuleName ["Data","Derive","Default"],ModuleName ["Data","Derive","Enum"],ModuleName ["Data","Derive","EnumCyclic"],ModuleName ["Data","Derive","Eq"],ModuleName ["Data","Derive","Fold"],ModuleName ["Data","Derive","Foldable"],ModuleName ["Data","Derive","From"],ModuleName ["Data","Derive","Functor"],ModuleName ["Data","Derive","Has"],ModuleName ["Data","Derive","Is"],ModuleName ["Data","Derive","JSON"],ModuleName ["Data","Derive","LazySet"],ModuleName ["Data","Derive","Lens"],ModuleName ["Data","Derive","Monoid"],ModuleName ["Data","Derive","NFData"],ModuleName ["Data","Derive","Ord"],ModuleName ["Data","Derive","Read"],ModuleName ["Data","Derive","Ref"],ModuleName ["Data","Derive","Serial"],ModuleName ["Data","Derive","Serialize"],ModuleName ["Data","Derive","Set"],ModuleName ["Data","Derive","Show"],ModuleName ["Data","Derive","Traversable"],ModuleName ["Data","Derive","Typeable"],ModuleName ["Data","Derive","UniplateDirect"],ModuleName ["Data","Derive","UniplateTypeable"],ModuleName ["Data","Derive","Update"],ModuleName ["Data","Derive","Internal","Derivation"]], hiddenModules = [ModuleName ["Data","Derive","Internal","Instance"],ModuleName ["Data","Derive","Internal","Traversal"],ModuleName ["Derive","Main"],ModuleName ["Derive","Derivation"],ModuleName ["Derive","Flags"],ModuleName ["Derive","Generate"],ModuleName ["Derive","Test"],ModuleName ["Derive","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], hsLibraries = ["HSderive-2.5.11"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b",InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html/derive.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html"]}),(InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageInfo {installedPackageId = InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691", sourcePackageId = PackageIdentifier {pkgName = PackageName "directory", pkgVersion = Version {versionBranch = [1,1,0,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "library for directory handling", description = "This package provides a library for handling directories.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Directory"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], hsLibraries = ["HSdirectory-1.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2/include"], includes = ["HsDirectory.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2/directory.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2"]}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "watson.timothy@gmail.com, edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Cloud Haskell: Erlang-style concurrency in Haskell", description = "This is an implementation of Cloud Haskell, as described in\n/Towards Haskell in the Cloud/ by Jeff Epstein, Andrew Black,\nand Simon Peyton Jones\n(),\nalthough some of the details are different. The precise message\npassing semantics are based on /A unified semantics for future Erlang/\nby Hans Svensson, Lars-\197ke Fredlund and Clara Benac Earle.\nYou will probably also want to install a Cloud Haskell backend such\nas distributed-process-simplelocalnet.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Process","Internal","Closure","TH"],ModuleName ["Control","Distributed","Process"],ModuleName ["Control","Distributed","Process","Serializable"],ModuleName ["Control","Distributed","Process","Closure"],ModuleName ["Control","Distributed","Process","Node"],ModuleName ["Control","Distributed","Process","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Internal","CQueue"],ModuleName ["Control","Distributed","Process","Internal","Types"],ModuleName ["Control","Distributed","Process","Internal","Trace"],ModuleName ["Control","Distributed","Process","Internal","Closure","BuiltIn"],ModuleName ["Control","Distributed","Process","Internal","Messaging"],ModuleName ["Control","Distributed","Process","Internal","StrictList"],ModuleName ["Control","Distributed","Process","Internal","StrictMVar"],ModuleName ["Control","Distributed","Process","Internal","WeakTQueue"],ModuleName ["Control","Distributed","Process","Internal","StrictContainerAccessors"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], hsLibraries = ["HSdistributed-process-0.4.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb",InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0",InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html/distributed-process.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html"]}),(InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15",InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-static", pkgVersion = Version {versionBranch = [0,2,1,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://www.github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Compositional, type-safe, polymorphic static values and closures", description = "/Towards Haskell in the Cloud/ (Epstein et al, Haskell\nSymposium 2011) introduces the concept of /static/ values:\nvalues that are known at compile time. In a distributed\nsetting where all nodes are running the same executable,\nstatic values can be serialized simply by transmitting a\ncode pointer to the value. This however requires special\ncompiler support, which is not yet available in ghc. We\ncan mimick the behaviour by keeping an explicit mapping\n('RemoteTable') from labels to values (and making sure\nthat all distributed nodes are using the same\n'RemoteTable'). In this module we implement this mimickry\nand various extensions: type safety (including for\npolymorphic static values) and compositionality.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Static"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], hsLibraries = ["HSdistributed-static-0.2.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html/distributed-static.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html"]}),(InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageInfo {installedPackageId = InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57", sourcePackageId = PackageIdentifier {pkgName = PackageName "filepath", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Neil Mitchell", stability = "", homepage = "http://www-users.cs.york.ac.uk/~ndm/filepath/", pkgUrl = "", synopsis = "Library for manipulating FilePaths in a cross platform way.", description = "", category = "System", exposed = True, exposedModules = [ModuleName ["System","FilePath"],ModuleName ["System","FilePath","Posix"],ModuleName ["System","FilePath","Windows"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], hsLibraries = ["HSfilepath-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0/filepath.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0"]}),(InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageInfo {installedPackageId = InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7", sourcePackageId = PackageIdentifier {pkgName = PackageName "ghc-prim", pkgVersion = Version {versionBranch = [0,2,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "GHC primitives", description = "GHC primitives.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Prim"],ModuleName ["GHC","Classes"],ModuleName ["GHC","CString"],ModuleName ["GHC","Debug"],ModuleName ["GHC","Generics"],ModuleName ["GHC","Magic"],ModuleName ["GHC","PrimopWrappers"],ModuleName ["GHC","IntWord64"],ModuleName ["GHC","Tuple"],ModuleName ["GHC","Types"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], hsLibraries = ["HSghc-prim-0.2.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0/ghc-prim.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0"]}),(InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c",InstalledPackageInfo {installedPackageId = InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c", sourcePackageId = PackageIdentifier {pkgName = PackageName "hashable", pkgVersion = Version {versionBranch = [1,1,2,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "johan.tibell@gmail.com", author = "Milan Straka \nJohan Tibell ", stability = "Provisional", homepage = "http://github.com/tibbe/hashable", pkgUrl = "", synopsis = "A class for types that can be converted to a hash value", description = "This package defines a class, 'Hashable', for types that\ncan be converted to a hash value. This class\nexists for the benefit of hashing-based data\nstructures. The package provides instances for\nbasic types and a way to combine hash values.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Hashable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], hsLibraries = ["HShashable-1.1.2.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html/hashable.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html"]}),(InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697",InstalledPackageInfo {installedPackageId = InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697", sourcePackageId = PackageIdentifier {pkgName = PackageName "haskell-src-exts", pkgVersion = Version {versionBranch = [1,13,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Niklas Broberg ", author = "Niklas Broberg", stability = "Stable", homepage = "http://code.haskell.org/haskell-src-exts", pkgUrl = "", synopsis = "Manipulating Haskell source: abstract syntax, lexer, parser, and pretty-printer", description = "Haskell-Source with Extensions (HSE, haskell-src-exts)\nis an extension of the standard haskell-src package,\nand handles most registered syntactic extensions to Haskell, including:\n\n* Multi-parameter type classes with functional dependencies\n\n* Indexed type families (including associated types)\n\n* Empty data declarations\n\n* GADTs\n\n* Implicit parameters\n\n* Template Haskell\n\nand a few more. All extensions implemented in GHC are supported.\nApart from these standard extensions,\nit also handles regular patterns as per the HaRP extension\nas well as HSX-style embedded XML syntax.", category = "Language", exposed = True, exposedModules = [ModuleName ["Language","Haskell","Exts"],ModuleName ["Language","Haskell","Exts","Lexer"],ModuleName ["Language","Haskell","Exts","Parser"],ModuleName ["Language","Haskell","Exts","Pretty"],ModuleName ["Language","Haskell","Exts","Syntax"],ModuleName ["Language","Haskell","Exts","Extension"],ModuleName ["Language","Haskell","Exts","Build"],ModuleName ["Language","Haskell","Exts","Fixity"],ModuleName ["Language","Haskell","Exts","Comments"],ModuleName ["Language","Haskell","Exts","SrcLoc"],ModuleName ["Language","Haskell","Exts","Annotated"],ModuleName ["Language","Haskell","Exts","Annotated","Syntax"],ModuleName ["Language","Haskell","Exts","Annotated","Fixity"],ModuleName ["Language","Haskell","Exts","Annotated","Build"],ModuleName ["Language","Haskell","Exts","Annotated","ExactPrint"],ModuleName ["Language","Haskell","Exts","Annotated","Simplify"]], hiddenModules = [ModuleName ["Language","Haskell","Exts","ExtScheme"],ModuleName ["Language","Haskell","Exts","ParseMonad"],ModuleName ["Language","Haskell","Exts","ParseSyntax"],ModuleName ["Language","Haskell","Exts","InternalLexer"],ModuleName ["Language","Haskell","Exts","ParseUtils"],ModuleName ["Language","Haskell","Exts","InternalParser"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], hsLibraries = ["HShaskell-src-exts-1.13.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html/haskell-src-exts.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html"]}),(InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageInfo {installedPackageId = InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43", sourcePackageId = PackageIdentifier {pkgName = PackageName "integer-gmp", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Integer library based on GMP", description = "This package contains an Integer library based on GMP.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Integer"],ModuleName ["GHC","Integer","GMP","Internals"],ModuleName ["GHC","Integer","GMP","Prim"],ModuleName ["GHC","Integer","Logarithms"],ModuleName ["GHC","Integer","Logarithms","Internals"]], hiddenModules = [ModuleName ["GHC","Integer","Type"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], hsLibraries = ["HSinteger-gmp-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0/integer-gmp.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0"]}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",InstalledPackageInfo {installedPackageId = InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29", sourcePackageId = PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Edward Kmett ", author = "Andy Gill", stability = "", homepage = "http://github.com/ekmett/mtl", pkgUrl = "", synopsis = "Monad classes, using functional dependencies", description = "Monad classes using functional dependencies, with instances\nfor various monad transformers, inspired by the paper\n/Functional Programming with Overloading and Higher-Order Polymorphism/,\nby Mark P Jones, in /Advanced School of Functional Programming/, 1995\n().", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Monad","Cont"],ModuleName ["Control","Monad","Cont","Class"],ModuleName ["Control","Monad","Error"],ModuleName ["Control","Monad","Error","Class"],ModuleName ["Control","Monad","Identity"],ModuleName ["Control","Monad","List"],ModuleName ["Control","Monad","RWS"],ModuleName ["Control","Monad","RWS","Class"],ModuleName ["Control","Monad","RWS","Lazy"],ModuleName ["Control","Monad","RWS","Strict"],ModuleName ["Control","Monad","Reader"],ModuleName ["Control","Monad","Reader","Class"],ModuleName ["Control","Monad","State"],ModuleName ["Control","Monad","State","Class"],ModuleName ["Control","Monad","State","Lazy"],ModuleName ["Control","Monad","State","Strict"],ModuleName ["Control","Monad","Trans"],ModuleName ["Control","Monad","Writer"],ModuleName ["Control","Monad","Writer","Class"],ModuleName ["Control","Monad","Writer","Lazy"],ModuleName ["Control","Monad","Writer","Strict"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], hsLibraries = ["HSmtl-2.1.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html/mtl.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html"]}),(InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812",InstalledPackageInfo {installedPackageId = InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812", sourcePackageId = PackageIdentifier {pkgName = PackageName "network-transport", pkgVersion = Version {versionBranch = [0,3,0,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Network abstraction layer", description = "\"Network.Transport\" is a Network Abstraction Layer which provides\nthe following high-level concepts:\n\n* Nodes in the network are represented by 'EndPoint's. These are\nheavyweight stateful objects.\n\n* Each 'EndPoint' has an 'EndPointAddress'.\n\n* Connections can be established from one 'EndPoint' to another\nusing the 'EndPointAddress' of the remote end.\n\n* The 'EndPointAddress' can be serialised and sent over the\nnetwork, where as 'EndPoint's and connections cannot.\n\n* Connections between 'EndPoint's are unidirectional and lightweight.\n\n* Outgoing messages are sent via a 'Connection' object that\nrepresents the sending end of the connection.\n\n* Incoming messages for /all/ of the incoming connections on\nan 'EndPoint' are collected via a shared receive queue.\n\n* In addition to incoming messages, 'EndPoint's are notified of\nother 'Event's such as new connections or broken connections.\n\nThis design was heavily influenced by the design of the Common\nCommunication Interface\n().\nImportant design goals are:\n\n* Connections should be lightweight: it should be no problem to\ncreate thousands of connections between endpoints.\n\n* Error handling is explicit: every function declares as part of\nits type which errors it can return (no exceptions are thrown)\n\n* Error handling is \"abstract\": errors that originate from\nimplementation specific problems (such as \"no more sockets\" in\nthe TCP implementation) get mapped to generic errors\n(\"insufficient resources\") at the Transport level.\n\nThis package provides the generic interface only; you will\nprobably also want to install at least one transport\nimplementation (network-transport-*).", category = "Network", exposed = True, exposedModules = [ModuleName ["Network","Transport"],ModuleName ["Network","Transport","Util"],ModuleName ["Network","Transport","Internal"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], hsLibraries = ["HSnetwork-transport-0.3.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html/network-transport.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html"]}),(InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageInfo {installedPackageId = InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-locale", pkgVersion = Version {versionBranch = [1,0,0,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "locale library", description = "This package provides the old locale library.\nFor new code, the new locale library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Locale"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], hsLibraries = ["HSold-locale-1.0.0.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4/old-locale.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4"]}),(InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d",InstalledPackageInfo {installedPackageId = InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-time", pkgVersion = Version {versionBranch = [1,1,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Time library", description = "This package provides the old time library.\nFor new code, the new time library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Time"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], hsLibraries = ["HSold-time-1.1.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0/include"], includes = ["HsTime.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0/old-time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0"]}),(InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b",InstalledPackageInfo {installedPackageId = InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b", sourcePackageId = PackageIdentifier {pkgName = PackageName "pretty", pkgVersion = Version {versionBranch = [1,1,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "David Terei ", author = "", stability = "Stable", homepage = "http://github.com/haskell/pretty", pkgUrl = "", synopsis = "Pretty-printing library", description = "This package contains a pretty-printing library, a set of API's\nthat provides a way to easily print out text in a consistent\nformat of your choosing. This is useful for compilers and related\ntools.\n\nThis library was originally designed by John Hughes's and has since\nbeen heavily modified by Simon Peyton Jones.", category = "Text", exposed = True, exposedModules = [ModuleName ["Text","PrettyPrint"],ModuleName ["Text","PrettyPrint","HughesPJ"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], hsLibraries = ["HSpretty-1.1.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0/pretty.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0"]}),(InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4",InstalledPackageInfo {installedPackageId = InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4", sourcePackageId = PackageIdentifier {pkgName = PackageName "process", pkgVersion = Version {versionBranch = [1,1,0,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Process libraries", description = "This package contains libraries for dealing with system processes.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Process","Internals"],ModuleName ["System","Process"],ModuleName ["System","Cmd"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], hsLibraries = ["HSprocess-1.1.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1/include"], includes = ["runProcess.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1/process.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1"]}),(InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1",InstalledPackageInfo {installedPackageId = InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1", sourcePackageId = PackageIdentifier {pkgName = PackageName "random", pkgVersion = Version {versionBranch = [1,0,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "rrnewton@gmail.com", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "random number library", description = "This package provides a basic random number generation\nlibrary, including the ability to split random number\ngenerators.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Random"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], hsLibraries = ["HSrandom-1.0.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html/random.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html"]}),(InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0",InstalledPackageInfo {installedPackageId = InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0", sourcePackageId = PackageIdentifier {pkgName = PackageName "rank1dynamic", pkgVersion = Version {versionBranch = [0,1,0,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types", description = "\"Data.Typeable\" and \"Data.Dynamic\" only support monomorphic types.\nIn this package we provide similar functionality but with\nsupport for rank-1 polymorphic types.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Rank1Dynamic"],ModuleName ["Data","Rank1Typeable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], hsLibraries = ["HSrank1dynamic-0.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html/rank1dynamic.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html"]}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",InstalledPackageInfo {installedPackageId = InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d", sourcePackageId = PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Software Transactional Memory", description = "A modular composable concurrency abstraction.\n\nChanges in version 2.4\n\n* Added \"Control.Concurrent.STM.TQueue\" (a faster @TChan@)\n\n* Added \"Control.Concurrent.STM.TBQueue\" (a bounded channel based on @TQueue@)\n\n* @TChan@ has an @Eq@ instances\n\n* Added @newBroadcastTChan@ and @newBroadcastTChanIO@\n\n* Some performance improvements for @TChan@\n\n* Added @cloneTChan@", category = "Concurrency", exposed = True, exposedModules = [ModuleName ["Control","Concurrent","STM"],ModuleName ["Control","Concurrent","STM","TArray"],ModuleName ["Control","Concurrent","STM","TVar"],ModuleName ["Control","Concurrent","STM","TChan"],ModuleName ["Control","Concurrent","STM","TMVar"],ModuleName ["Control","Concurrent","STM","TQueue"],ModuleName ["Control","Concurrent","STM","TBQueue"],ModuleName ["Control","Monad","STM"]], hiddenModules = [ModuleName ["Control","Sequential","STM"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], hsLibraries = ["HSstm-2.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html/stm.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html"]}),(InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageInfo {installedPackageId = InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24", sourcePackageId = PackageIdentifier {pkgName = PackageName "syb", pkgVersion = Version {versionBranch = [0,3,7], versionTags = []}}, license = BSD3, copyright = "", maintainer = "generics@haskell.org", author = "Ralf Lammel, Simon Peyton Jones, Jose Pedro Magalhaes", stability = "provisional", homepage = "http://www.cs.uu.nl/wiki/GenericProgramming/SYB", pkgUrl = "", synopsis = "Scrap Your Boilerplate", description = "This package contains the generics system described in the\n/Scrap Your Boilerplate/ papers (see\n).\nIt defines the @Data@ class of types permitting folding and unfolding\nof constructor applications, instances of this class for primitive\ntypes, and a variety of traversals.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics"],ModuleName ["Data","Generics","Basics"],ModuleName ["Data","Generics","Instances"],ModuleName ["Data","Generics","Aliases"],ModuleName ["Data","Generics","Schemes"],ModuleName ["Data","Generics","Text"],ModuleName ["Data","Generics","Twins"],ModuleName ["Data","Generics","Builders"],ModuleName ["Generics","SYB"],ModuleName ["Generics","SYB","Basics"],ModuleName ["Generics","SYB","Instances"],ModuleName ["Generics","SYB","Aliases"],ModuleName ["Generics","SYB","Schemes"],ModuleName ["Generics","SYB","Text"],ModuleName ["Generics","SYB","Twins"],ModuleName ["Generics","SYB","Builders"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], hsLibraries = ["HSsyb-0.3.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html/syb.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html"]}),(InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageInfo {installedPackageId = InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949", sourcePackageId = PackageIdentifier {pkgName = PackageName "template-haskell", pkgVersion = Version {versionBranch = [2,7,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "Facilities for manipulating Haskell source code using Template Haskell.", category = "", exposed = True, exposedModules = [ModuleName ["Language","Haskell","TH","Syntax","Internals"],ModuleName ["Language","Haskell","TH","Syntax"],ModuleName ["Language","Haskell","TH","PprLib"],ModuleName ["Language","Haskell","TH","Ppr"],ModuleName ["Language","Haskell","TH","Lib"],ModuleName ["Language","Haskell","TH","Quote"],ModuleName ["Language","Haskell","TH"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], hsLibraries = ["HStemplate-haskell-2.7.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0/template-haskell.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0"]}),(InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c",InstalledPackageInfo {installedPackageId = InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c", sourcePackageId = PackageIdentifier {pkgName = PackageName "text", pkgVersion = Version {versionBranch = [0,11,2,3], versionTags = []}}, license = BSD3, copyright = "2009-2011 Bryan O'Sullivan, 2008-2009 Tom Harper", maintainer = "Bryan O'Sullivan ", author = "Bryan O'Sullivan ", stability = "", homepage = "https://github.com/bos/text", pkgUrl = "", synopsis = "An efficient packed Unicode text type.", description = "\nAn efficient packed, immutable Unicode text type (both strict and\nlazy), with a powerful loop fusion optimization framework.\n\nThe 'Text' type represents Unicode character strings, in a time and\nspace-efficient manner. This package provides text processing\ncapabilities that are optimized for performance critical use, both\nin terms of large data quantities and high speed.\n\nThe 'Text' type provides character-encoding, type-safe case\nconversion via whole-string case conversion functions. It also\nprovides a range of functions for converting 'Text' values to and from\n'ByteStrings', using several standard encodings.\n\nEfficient locale-sensitive support for text IO is also supported.\n\nThese modules are intended to be imported qualified, to avoid name\nclashes with Prelude functions, e.g.\n\n> import qualified Data.Text as T\n\nTo use an extended and very rich family of functions for working\nwith Unicode text (including normalization, regular expressions,\nnon-standard encodings, text breaking, and locales), see\nthe @text-icu@ package:\n\n\n—— RELEASE NOTES ——\n\nChanges in 0.11.2.0:\n\n* String literals are now converted directly from the format in\nwhich GHC stores them into 'Text', without an intermediate\ntransformation through 'String', and without inlining of\nconversion code at each site where a string literal is declared.\n", category = "Data, Text", exposed = True, exposedModules = [ModuleName ["Data","Text"],ModuleName ["Data","Text","Array"],ModuleName ["Data","Text","Encoding"],ModuleName ["Data","Text","Encoding","Error"],ModuleName ["Data","Text","Foreign"],ModuleName ["Data","Text","IO"],ModuleName ["Data","Text","Internal"],ModuleName ["Data","Text","Lazy"],ModuleName ["Data","Text","Lazy","Builder"],ModuleName ["Data","Text","Lazy","Builder","Int"],ModuleName ["Data","Text","Lazy","Builder","RealFloat"],ModuleName ["Data","Text","Lazy","Encoding"],ModuleName ["Data","Text","Lazy","IO"],ModuleName ["Data","Text","Lazy","Internal"],ModuleName ["Data","Text","Lazy","Read"],ModuleName ["Data","Text","Read"]], hiddenModules = [ModuleName ["Data","Text","Encoding","Fusion"],ModuleName ["Data","Text","Encoding","Fusion","Common"],ModuleName ["Data","Text","Encoding","Utf16"],ModuleName ["Data","Text","Encoding","Utf32"],ModuleName ["Data","Text","Encoding","Utf8"],ModuleName ["Data","Text","Fusion"],ModuleName ["Data","Text","Fusion","CaseMapping"],ModuleName ["Data","Text","Fusion","Common"],ModuleName ["Data","Text","Fusion","Internal"],ModuleName ["Data","Text","Fusion","Size"],ModuleName ["Data","Text","IO","Internal"],ModuleName ["Data","Text","Lazy","Builder","Functions"],ModuleName ["Data","Text","Lazy","Builder","RealFloat","Functions"],ModuleName ["Data","Text","Lazy","Encoding","Fusion"],ModuleName ["Data","Text","Lazy","Fusion"],ModuleName ["Data","Text","Lazy","Search"],ModuleName ["Data","Text","Private"],ModuleName ["Data","Text","Search"],ModuleName ["Data","Text","Unsafe"],ModuleName ["Data","Text","Unsafe","Base"],ModuleName ["Data","Text","UnsafeChar"],ModuleName ["Data","Text","UnsafeShift"],ModuleName ["Data","Text","Util"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], hsLibraries = ["HStext-0.11.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html/text.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html"]}),(InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f",InstalledPackageInfo {installedPackageId = InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f", sourcePackageId = PackageIdentifier {pkgName = PackageName "time", pkgVersion = Version {versionBranch = [1,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Ashley Yakeley", stability = "stable", homepage = "http://semantic.org/TimeLib/", pkgUrl = "", synopsis = "A time library", description = "A time library", category = "System", exposed = True, exposedModules = [ModuleName ["Data","Time","Calendar"],ModuleName ["Data","Time","Calendar","MonthDay"],ModuleName ["Data","Time","Calendar","OrdinalDate"],ModuleName ["Data","Time","Calendar","WeekDate"],ModuleName ["Data","Time","Calendar","Julian"],ModuleName ["Data","Time","Calendar","Easter"],ModuleName ["Data","Time","Clock"],ModuleName ["Data","Time","Clock","POSIX"],ModuleName ["Data","Time","Clock","TAI"],ModuleName ["Data","Time","LocalTime"],ModuleName ["Data","Time","Format"],ModuleName ["Data","Time"]], hiddenModules = [ModuleName ["Data","Time","Calendar","Private"],ModuleName ["Data","Time","Calendar","Days"],ModuleName ["Data","Time","Calendar","Gregorian"],ModuleName ["Data","Time","Calendar","JulianYearDay"],ModuleName ["Data","Time","Clock","Scale"],ModuleName ["Data","Time","Clock","UTC"],ModuleName ["Data","Time","Clock","CTimeval"],ModuleName ["Data","Time","Clock","UTCDiff"],ModuleName ["Data","Time","LocalTime","TimeZone"],ModuleName ["Data","Time","LocalTime","TimeOfDay"],ModuleName ["Data","Time","LocalTime","LocalTime"],ModuleName ["Data","Time","Format","Parse"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], hsLibraries = ["HStime-1.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4/include"], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4/time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4"]}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",InstalledPackageInfo {installedPackageId = InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4", sourcePackageId = PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Ross Paterson ", author = "Andy Gill, Ross Paterson", stability = "", homepage = "", pkgUrl = "", synopsis = "Concrete functor and monad transformers", description = "A portable library of functor and monad transformers, inspired by\nthe paper \\\"Functional Programming with Overloading and Higher-Order\nPolymorphism\\\", by Mark P Jones,\nin /Advanced School of Functional Programming/, 1995\n().\n\nThis package contains:\n\n* the monad transformer class (in \"Control.Monad.Trans.Class\")\n\n* concrete functor and monad transformers, each with associated\noperations and functions to lift operations associated with other\ntransformers.\n\nIt can be used on its own in portable Haskell code, or with the monad\nclasses in the @mtl@ or @monads-tf@ packages, which automatically\nlift operations introduced by monad transformers through other\ntransformers.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Applicative","Backwards"],ModuleName ["Control","Applicative","Lift"],ModuleName ["Control","Monad","IO","Class"],ModuleName ["Control","Monad","Trans","Class"],ModuleName ["Control","Monad","Trans","Cont"],ModuleName ["Control","Monad","Trans","Error"],ModuleName ["Control","Monad","Trans","Identity"],ModuleName ["Control","Monad","Trans","List"],ModuleName ["Control","Monad","Trans","Maybe"],ModuleName ["Control","Monad","Trans","Reader"],ModuleName ["Control","Monad","Trans","RWS"],ModuleName ["Control","Monad","Trans","RWS","Lazy"],ModuleName ["Control","Monad","Trans","RWS","Strict"],ModuleName ["Control","Monad","Trans","State"],ModuleName ["Control","Monad","Trans","State","Lazy"],ModuleName ["Control","Monad","Trans","State","Strict"],ModuleName ["Control","Monad","Trans","Writer"],ModuleName ["Control","Monad","Trans","Writer","Lazy"],ModuleName ["Control","Monad","Trans","Writer","Strict"],ModuleName ["Data","Functor","Compose"],ModuleName ["Data","Functor","Constant"],ModuleName ["Data","Functor","Identity"],ModuleName ["Data","Functor","Product"],ModuleName ["Data","Functor","Reverse"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], hsLibraries = ["HStransformers-0.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html/transformers.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html"]}),(InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd",InstalledPackageInfo {installedPackageId = InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd", sourcePackageId = PackageIdentifier {pkgName = PackageName "uniplate", pkgVersion = Version {versionBranch = [1,6,7], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/uniplate/", pkgUrl = "", synopsis = "Help writing simple, concise and fast generic operations.", description = "Uniplate is library for writing simple and concise generic operations.\nUniplate has similar goals to the original Scrap Your Boilerplate work,\nbut is substantially simpler and faster. The Uniplate manual is available at\n.\n\nTo get started with Uniplate you should import one of the three following\nmodules:\n\n* \"Data.Generics.Uniplate.Data\" - to quickly start writing generic functions.\nMost users should start by importing this module.\n\n* \"Data.Generics.Uniplate.Direct\" - a replacement for \"Data.Generics.Uniplate.Data\"\nwith substantially higher performance (around 5 times), but requires writing\ninstance declarations.\n\n* \"Data.Generics.Uniplate.Operations\" - definitions of all the operations defined\nby Uniplate. Both the above two modules re-export this module.\n\nIn addition, some users may want to make use of the following modules:\n\n* \"Data.Generics.Uniplate.Zipper\" - a zipper built on top of Uniplate instances.\n\n* \"Data.Generics.SYB\" - users transitioning from the Scrap Your Boilerplate library.\n\n* \"Data.Generics.Compos\" - users transitioning from the Compos library.\n\n* \"Data.Generics.Uniplate.DataOnly\" - users making use of both @Data@ and @Direct@\nto avoid getting instance conflicts.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics","Str"],ModuleName ["Data","Generics","Compos"],ModuleName ["Data","Generics","SYB"],ModuleName ["Data","Generics","Uniplate","Data"],ModuleName ["Data","Generics","Uniplate","Data","Instances"],ModuleName ["Data","Generics","Uniplate","DataOnly"],ModuleName ["Data","Generics","Uniplate","Direct"],ModuleName ["Data","Generics","Uniplate","Operations"],ModuleName ["Data","Generics","Uniplate","Typeable"],ModuleName ["Data","Generics","Uniplate","Zipper"],ModuleName ["Data","Generics","Uniplate"],ModuleName ["Data","Generics","UniplateOn"],ModuleName ["Data","Generics","UniplateStr"],ModuleName ["Data","Generics","UniplateStrOn"],ModuleName ["Data","Generics","Biplate"],ModuleName ["Data","Generics","PlateDirect"],ModuleName ["Data","Generics","PlateTypeable"],ModuleName ["Data","Generics","PlateData"]], hiddenModules = [ModuleName ["Data","Generics","PlateInternal"],ModuleName ["Data","Generics","Uniplate","Internal","Data"],ModuleName ["Data","Generics","Uniplate","Internal","DataOnlyOperations"],ModuleName ["Data","Generics","Uniplate","Internal","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], hsLibraries = ["HSuniplate-1.6.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html/uniplate.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html"]}),(InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e",InstalledPackageInfo {installedPackageId = InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e", sourcePackageId = PackageIdentifier {pkgName = PackageName "unix", pkgVersion = Version {versionBranch = [2,5,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "POSIX functionality", description = "This package gives you access to the set of operating system\nservices standardised by POSIX 1003.1b (or the IEEE Portable\nOperating System Interface for Computing Environments -\nIEEE Std. 1003.1).\n\nThe package is not supported under Windows (except under Cygwin).", category = "System", exposed = True, exposedModules = [ModuleName ["System","Posix"],ModuleName ["System","Posix","ByteString"],ModuleName ["System","Posix","Error"],ModuleName ["System","Posix","Resource"],ModuleName ["System","Posix","Time"],ModuleName ["System","Posix","Unistd"],ModuleName ["System","Posix","User"],ModuleName ["System","Posix","Signals"],ModuleName ["System","Posix","Signals","Exts"],ModuleName ["System","Posix","Semaphore"],ModuleName ["System","Posix","SharedMem"],ModuleName ["System","Posix","ByteString","FilePath"],ModuleName ["System","Posix","Directory"],ModuleName ["System","Posix","Directory","ByteString"],ModuleName ["System","Posix","DynamicLinker","Module"],ModuleName ["System","Posix","DynamicLinker","Module","ByteString"],ModuleName ["System","Posix","DynamicLinker","Prim"],ModuleName ["System","Posix","DynamicLinker","ByteString"],ModuleName ["System","Posix","DynamicLinker"],ModuleName ["System","Posix","Files"],ModuleName ["System","Posix","Files","ByteString"],ModuleName ["System","Posix","IO"],ModuleName ["System","Posix","IO","ByteString"],ModuleName ["System","Posix","Env"],ModuleName ["System","Posix","Env","ByteString"],ModuleName ["System","Posix","Process"],ModuleName ["System","Posix","Process","Internals"],ModuleName ["System","Posix","Process","ByteString"],ModuleName ["System","Posix","Temp"],ModuleName ["System","Posix","Temp","ByteString"],ModuleName ["System","Posix","Terminal"],ModuleName ["System","Posix","Terminal","ByteString"]], hiddenModules = [ModuleName ["System","Posix","Directory","Common"],ModuleName ["System","Posix","DynamicLinker","Common"],ModuleName ["System","Posix","Files","Common"],ModuleName ["System","Posix","IO","Common"],ModuleName ["System","Posix","Process","Common"],ModuleName ["System","Posix","Terminal","Common"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], hsLibraries = ["HSunix-2.5.1.1"], extraLibraries = ["dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1/include"], includes = ["HsUnix.h","execvpe.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1/unix.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1"]}),(InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d",InstalledPackageInfo {installedPackageId = InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d", sourcePackageId = PackageIdentifier {pkgName = PackageName "unordered-containers", pkgVersion = Version {versionBranch = [0,2,2,1], versionTags = []}}, license = BSD3, copyright = "2010-2012 Johan Tibell\n2010 Edward Z. Yang", maintainer = "johan.tibell@gmail.com", author = "Johan Tibell", stability = "", homepage = "https://github.com/tibbe/unordered-containers", pkgUrl = "", synopsis = "Efficient hashing-based container types", description = "Efficient hashing-based container types. The containers have been\noptimized for performance critical use, both in terms of large data\nquantities and high speed.\n\nThe declared cost of each operation is either worst-case or\namortized, but remains valid even if structures are shared.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","HashMap","Lazy"],ModuleName ["Data","HashMap","Strict"],ModuleName ["Data","HashSet"]], hiddenModules = [ModuleName ["Data","HashMap","Array"],ModuleName ["Data","HashMap","Base"],ModuleName ["Data","HashMap","PopCount"],ModuleName ["Data","HashMap","Unsafe"],ModuleName ["Data","HashMap","UnsafeShift"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], hsLibraries = ["HSunordered-containers-0.2.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html/unordered-containers.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html"]})]) (fromList [(PackageName "array",fromList [(Version {versionBranch = [0,4,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297", sourcePackageId = PackageIdentifier {pkgName = PackageName "array", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Mutable and immutable arrays", description = "This package defines the classes @IArray@ of immutable arrays and\n@MArray@ of arrays mutable within appropriate monads, as well as\nsome instances of these classes.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Array","Base"],ModuleName ["Data","Array","IArray"],ModuleName ["Data","Array","IO"],ModuleName ["Data","Array","IO","Safe"],ModuleName ["Data","Array","IO","Internals"],ModuleName ["Data","Array","MArray"],ModuleName ["Data","Array","MArray","Safe"],ModuleName ["Data","Array","ST"],ModuleName ["Data","Array","ST","Safe"],ModuleName ["Data","Array","Storable"],ModuleName ["Data","Array","Storable","Safe"],ModuleName ["Data","Array","Storable","Internals"],ModuleName ["Data","Array","Unboxed"],ModuleName ["Data","Array","Unsafe"],ModuleName ["Data","Array"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], hsLibraries = ["HSarray-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0/array.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0"]}])]),(PackageName "base",fromList [(Version {versionBranch = [4,5,1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd", sourcePackageId = PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Basic libraries", description = "This package contains the Prelude and its support libraries,\nand a large collection of useful libraries ranging from data\nstructures to parsing combinators and debugging utilities.", category = "", exposed = True, exposedModules = [ModuleName ["Foreign","Concurrent"],ModuleName ["GHC","Arr"],ModuleName ["GHC","Base"],ModuleName ["GHC","Conc"],ModuleName ["GHC","Conc","IO"],ModuleName ["GHC","Conc","Signal"],ModuleName ["GHC","Conc","Sync"],ModuleName ["GHC","ConsoleHandler"],ModuleName ["GHC","Constants"],ModuleName ["GHC","Desugar"],ModuleName ["GHC","Enum"],ModuleName ["GHC","Environment"],ModuleName ["GHC","Err"],ModuleName ["GHC","Exception"],ModuleName ["GHC","Exts"],ModuleName ["GHC","Fingerprint"],ModuleName ["GHC","Fingerprint","Type"],ModuleName ["GHC","Float"],ModuleName ["GHC","Float","ConversionUtils"],ModuleName ["GHC","Float","RealFracMethods"],ModuleName ["GHC","Foreign"],ModuleName ["GHC","ForeignPtr"],ModuleName ["GHC","Handle"],ModuleName ["GHC","IO"],ModuleName ["GHC","IO","Buffer"],ModuleName ["GHC","IO","BufferedIO"],ModuleName ["GHC","IO","Device"],ModuleName ["GHC","IO","Encoding"],ModuleName ["GHC","IO","Encoding","CodePage"],ModuleName ["GHC","IO","Encoding","Failure"],ModuleName ["GHC","IO","Encoding","Iconv"],ModuleName ["GHC","IO","Encoding","Latin1"],ModuleName ["GHC","IO","Encoding","Types"],ModuleName ["GHC","IO","Encoding","UTF16"],ModuleName ["GHC","IO","Encoding","UTF32"],ModuleName ["GHC","IO","Encoding","UTF8"],ModuleName ["GHC","IO","Exception"],ModuleName ["GHC","IO","FD"],ModuleName ["GHC","IO","Handle"],ModuleName ["GHC","IO","Handle","FD"],ModuleName ["GHC","IO","Handle","Internals"],ModuleName ["GHC","IO","Handle","Text"],ModuleName ["GHC","IO","Handle","Types"],ModuleName ["GHC","IO","IOMode"],ModuleName ["GHC","IOArray"],ModuleName ["GHC","IOBase"],ModuleName ["GHC","IORef"],ModuleName ["GHC","Int"],ModuleName ["GHC","List"],ModuleName ["GHC","MVar"],ModuleName ["GHC","Num"],ModuleName ["GHC","PArr"],ModuleName ["GHC","Pack"],ModuleName ["GHC","Ptr"],ModuleName ["GHC","Read"],ModuleName ["GHC","Real"],ModuleName ["GHC","ST"],ModuleName ["GHC","Stack"],ModuleName ["GHC","Stats"],ModuleName ["GHC","Show"],ModuleName ["GHC","Stable"],ModuleName ["GHC","Storable"],ModuleName ["GHC","STRef"],ModuleName ["GHC","TopHandler"],ModuleName ["GHC","Unicode"],ModuleName ["GHC","Weak"],ModuleName ["GHC","Word"],ModuleName ["System","Timeout"],ModuleName ["GHC","Event"],ModuleName ["Control","Applicative"],ModuleName ["Control","Arrow"],ModuleName ["Control","Category"],ModuleName ["Control","Concurrent"],ModuleName ["Control","Concurrent","Chan"],ModuleName ["Control","Concurrent","MVar"],ModuleName ["Control","Concurrent","QSem"],ModuleName ["Control","Concurrent","QSemN"],ModuleName ["Control","Concurrent","SampleVar"],ModuleName ["Control","Exception"],ModuleName ["Control","Exception","Base"],ModuleName ["Control","OldException"],ModuleName ["Control","Monad"],ModuleName ["Control","Monad","Fix"],ModuleName ["Control","Monad","Instances"],ModuleName ["Control","Monad","ST"],ModuleName ["Control","Monad","ST","Safe"],ModuleName ["Control","Monad","ST","Unsafe"],ModuleName ["Control","Monad","ST","Lazy"],ModuleName ["Control","Monad","ST","Lazy","Safe"],ModuleName ["Control","Monad","ST","Lazy","Unsafe"],ModuleName ["Control","Monad","ST","Strict"],ModuleName ["Control","Monad","Zip"],ModuleName ["Data","Bits"],ModuleName ["Data","Bool"],ModuleName ["Data","Char"],ModuleName ["Data","Complex"],ModuleName ["Data","Dynamic"],ModuleName ["Data","Either"],ModuleName ["Data","Eq"],ModuleName ["Data","Data"],ModuleName ["Data","Fixed"],ModuleName ["Data","Foldable"],ModuleName ["Data","Function"],ModuleName ["Data","Functor"],ModuleName ["Data","HashTable"],ModuleName ["Data","IORef"],ModuleName ["Data","Int"],ModuleName ["Data","Ix"],ModuleName ["Data","List"],ModuleName ["Data","Maybe"],ModuleName ["Data","Monoid"],ModuleName ["Data","Ord"],ModuleName ["Data","Ratio"],ModuleName ["Data","STRef"],ModuleName ["Data","STRef","Lazy"],ModuleName ["Data","STRef","Strict"],ModuleName ["Data","String"],ModuleName ["Data","Traversable"],ModuleName ["Data","Tuple"],ModuleName ["Data","Typeable"],ModuleName ["Data","Typeable","Internal"],ModuleName ["Data","Unique"],ModuleName ["Data","Version"],ModuleName ["Data","Word"],ModuleName ["Debug","Trace"],ModuleName ["Foreign"],ModuleName ["Foreign","C"],ModuleName ["Foreign","C","Error"],ModuleName ["Foreign","C","String"],ModuleName ["Foreign","C","Types"],ModuleName ["Foreign","ForeignPtr"],ModuleName ["Foreign","ForeignPtr","Safe"],ModuleName ["Foreign","ForeignPtr","Unsafe"],ModuleName ["Foreign","Marshal"],ModuleName ["Foreign","Marshal","Alloc"],ModuleName ["Foreign","Marshal","Array"],ModuleName ["Foreign","Marshal","Error"],ModuleName ["Foreign","Marshal","Pool"],ModuleName ["Foreign","Marshal","Safe"],ModuleName ["Foreign","Marshal","Utils"],ModuleName ["Foreign","Marshal","Unsafe"],ModuleName ["Foreign","Ptr"],ModuleName ["Foreign","Safe"],ModuleName ["Foreign","StablePtr"],ModuleName ["Foreign","Storable"],ModuleName ["Numeric"],ModuleName ["Prelude"],ModuleName ["System","Console","GetOpt"],ModuleName ["System","CPUTime"],ModuleName ["System","Environment"],ModuleName ["System","Exit"],ModuleName ["System","IO"],ModuleName ["System","IO","Error"],ModuleName ["System","IO","Unsafe"],ModuleName ["System","Info"],ModuleName ["System","Mem"],ModuleName ["System","Mem","StableName"],ModuleName ["System","Mem","Weak"],ModuleName ["System","Posix","Internals"],ModuleName ["System","Posix","Types"],ModuleName ["Text","ParserCombinators","ReadP"],ModuleName ["Text","ParserCombinators","ReadPrec"],ModuleName ["Text","Printf"],ModuleName ["Text","Read"],ModuleName ["Text","Read","Lex"],ModuleName ["Text","Show"],ModuleName ["Text","Show","Functions"],ModuleName ["Unsafe","Coerce"]], hiddenModules = [ModuleName ["GHC","Event","Array"],ModuleName ["GHC","Event","Clock"],ModuleName ["GHC","Event","Control"],ModuleName ["GHC","Event","EPoll"],ModuleName ["GHC","Event","IntMap"],ModuleName ["GHC","Event","Internal"],ModuleName ["GHC","Event","KQueue"],ModuleName ["GHC","Event","Manager"],ModuleName ["GHC","Event","PSQ"],ModuleName ["GHC","Event","Poll"],ModuleName ["GHC","Event","Thread"],ModuleName ["GHC","Event","Unique"],ModuleName ["Control","Monad","ST","Imp"],ModuleName ["Control","Monad","ST","Lazy","Imp"],ModuleName ["Foreign","ForeignPtr","Imp"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], hsLibraries = ["HSbase-4.5.1.0"], extraLibraries = ["iconv"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0/include"], includes = ["HsBase.h"], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0/base.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0"]}])]),(PackageName "binary",fromList [(Version {versionBranch = [0,5,1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b", sourcePackageId = PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Lennart Kolmodin, Don Stewart ", author = "Lennart Kolmodin ", stability = "provisional", homepage = "http://code.haskell.org/binary/", pkgUrl = "", synopsis = "Binary serialisation for Haskell values using lazy ByteStrings", description = "Efficient, pure binary serialisation using lazy ByteStrings.\nHaskell values may be encoded to and from binary formats,\nwritten to disk as binary, or sent over the network.\nSerialisation speeds of over 1 G\\/sec have been observed,\nso this library should be suitable for high performance\nscenarios.", category = "Data, Parsing", exposed = True, exposedModules = [ModuleName ["Data","Binary"],ModuleName ["Data","Binary","Put"],ModuleName ["Data","Binary","Get"],ModuleName ["Data","Binary","Builder"],ModuleName ["Data","Binary","Builder","Internal"]], hiddenModules = [ModuleName ["Data","Binary","Builder","Base"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], hsLibraries = ["HSbinary-0.5.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0/binary.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0"]}])]),(PackageName "bytestring",fromList [(Version {versionBranch = [0,9,2,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065", sourcePackageId = PackageIdentifier {pkgName = PackageName "bytestring", pkgVersion = Version {versionBranch = [0,9,2,1], versionTags = []}}, license = BSD3, copyright = "Copyright (c) Don Stewart 2005-2009,\n(c) Duncan Coutts 2006-2009,\n(c) David Roundy 2003-2005.", maintainer = "dons00@gmail.com, duncan@community.haskell.org", author = "Don Stewart, Duncan Coutts", stability = "", homepage = "http://www.cse.unsw.edu.au/~dons/fps.html", pkgUrl = "", synopsis = "Fast, packed, strict and lazy byte arrays with a list interface", description = "A time and space-efficient implementation of byte vectors using\npacked Word8 arrays, suitable for high performance use, both in terms\nof large data quantities, or high speed requirements. Byte vectors\nare encoded as strict 'Word8' arrays of bytes, and lazy lists of\nstrict chunks, held in a 'ForeignPtr', and can be passed between C\nand Haskell with little effort.\n\nTest coverage data for this library is available at:\n", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","ByteString"],ModuleName ["Data","ByteString","Char8"],ModuleName ["Data","ByteString","Unsafe"],ModuleName ["Data","ByteString","Internal"],ModuleName ["Data","ByteString","Lazy"],ModuleName ["Data","ByteString","Lazy","Char8"],ModuleName ["Data","ByteString","Lazy","Internal"],ModuleName ["Data","ByteString","Fusion"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], hsLibraries = ["HSbytestring-0.9.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1/include"], includes = ["fpstring.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1/bytestring.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1"]}])]),(PackageName "containers",fromList [(Version {versionBranch = [0,4,2,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce", sourcePackageId = PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "fox@ucw.cz", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Assorted concrete container types", description = "This package contains efficient general-purpose implementations\nof various basic immutable container types. The declared cost of\neach operation is either worst-case or amortized, but remains\nvalid even if structures are shared.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Graph"],ModuleName ["Data","Sequence"],ModuleName ["Data","Tree"],ModuleName ["Data","IntMap"],ModuleName ["Data","IntSet"],ModuleName ["Data","Map"],ModuleName ["Data","Set"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], hsLibraries = ["HScontainers-0.4.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1/containers.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1"]}])]),(PackageName "cpphs",fromList [(Version {versionBranch = [1,14], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "cpphs", pkgVersion = Version {versionBranch = [1,14], versionTags = []}}, license = LGPL Nothing, copyright = "2004-2012, Malcolm Wallace", maintainer = "Malcolm Wallace ", author = "Malcolm Wallace ", stability = "", homepage = "http://haskell.org/cpphs/", pkgUrl = "", synopsis = "A liberalised re-implementation of cpp, the C pre-processor.", description = "Cpphs is a re-implementation of the C pre-processor that is both\nmore compatible with Haskell, and itself written in Haskell so\nthat it can be distributed with compilers.\n\nThis version of the C pre-processor is pretty-much\nfeature-complete and compatible with traditional (K&R)\npre-processors. Additional features include: a plain-text mode;\nan option to unlit literate code files; and an option to turn\noff macro-expansion.", category = "Development", exposed = True, exposedModules = [ModuleName ["Language","Preprocessor","Cpphs"],ModuleName ["Language","Preprocessor","Unlit"]], hiddenModules = [ModuleName ["Language","Preprocessor","Cpphs","CppIfdef"],ModuleName ["Language","Preprocessor","Cpphs","HashDefine"],ModuleName ["Language","Preprocessor","Cpphs","MacroPass"],ModuleName ["Language","Preprocessor","Cpphs","Options"],ModuleName ["Language","Preprocessor","Cpphs","Position"],ModuleName ["Language","Preprocessor","Cpphs","ReadFirst"],ModuleName ["Language","Preprocessor","Cpphs","RunCpphs"],ModuleName ["Language","Preprocessor","Cpphs","SymTab"],ModuleName ["Language","Preprocessor","Cpphs","Tokenise"],ModuleName ["Text","ParserCombinators","HuttonMeijer"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], hsLibraries = ["HScpphs-1.14"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html/cpphs.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html"]}])]),(PackageName "data-accessor",fromList [(Version {versionBranch = [0,2,2,3], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb", sourcePackageId = PackageIdentifier {pkgName = PackageName "data-accessor", pkgVersion = Version {versionBranch = [0,2,2,3], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Henning Thielemann ", author = "Henning Thielemann , Luke Palmer ", stability = "", homepage = "http://www.haskell.org/haskellwiki/Record_access", pkgUrl = "", synopsis = "Utilities for accessing and manipulating fields of records", description = "In Haskell 98 the name of a record field\nis automatically also the name of a function which gets the value\nof the according field.\nE.g. if we have\n\ndata Pair a b = Pair\nfirst :: a, second :: b\n\nthen\n\n> first :: Pair a b -> a\n> second :: Pair a b -> b\n\nHowever for setting or modifying a field value\nwe need to use some syntactic sugar, which is often clumsy.\n\nmodifyFirst :: (a -> a) -> (Pair a b -> Pair a b)\nmodifyFirst f r\\@(Pair\nfirst=a\n) = r\nfirst = f a\n\nWith this package you can define record field accessors\nwhich allow setting, getting and modifying values easily.\nThe package clearly demonstrates the power of the functional approach:\nYou can combine accessors of a record and sub-records,\nto make the access look like the fields of the sub-record belong to the main record.\n\nExample:\n\n> *Data.Accessor.Example> (first^:second^=10) (('b',7),\"hallo\")\n> (('b',10),\"hallo\")\n\nYou can easily manipulate record fields in a 'Control.Monad.State.State' monad,\nyou can easily code 'Show' instances that use the Accessor syntax\nand you can parse binary streams into records.\nSee @Data.Accessor.Example@ for demonstration of all features.\n\nIt would be great if in revised Haskell versions the names of record fields\nare automatically 'Data.Accessor.Accessor's\nrather than plain @get@ functions.\nFor now, the package @data-accessor-template@ provides Template Haskell functions\nfor automated generation of 'Data.Acesssor.Accessor's.\nSee also the other @data-accessor@ packages\nthat provide an Accessor interface to other data types.\nThe package @enumset@ provides accessors to bit-packed records.\n\nFor similar packages see @lenses@ and @fclabel@.\nA related concept are editors\n.\nEditors only consist of a modify method\n(and @modify@ applied to a 'const' function is a @set@ function).\nThis way, they can modify all function values of a function at once,\nwhereas an accessor can only change a single function value,\nsay, it can change @f 0 = 1@ to @f 0 = 2@.\nThis way, editors can even change the type of a record or a function.\nAn Arrow instance can be defined for editors,\nbut for accessors only a Category instance is possible ('(.)' method).\nThe reason is the @arr@ method of the @Arrow@ class,\nthat conflicts with the two-way nature (set and get) of accessors.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Accessor"],ModuleName ["Data","Accessor","Basic"],ModuleName ["Data","Accessor","Container"],ModuleName ["Data","Accessor","Show"],ModuleName ["Data","Accessor","Tuple"],ModuleName ["Data","Accessor","BinaryRead"],ModuleName ["Data","Accessor","MonadState"]], hiddenModules = [ModuleName ["Data","Accessor","Example"],ModuleName ["Data","Accessor","Private"],ModuleName ["Data","Accessor","MonadStatePrivate"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], hsLibraries = ["HSdata-accessor-0.2.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html/data-accessor.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html"]}])]),(PackageName "deepseq",fromList [(Version {versionBranch = [1,3,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6", sourcePackageId = PackageIdentifier {pkgName = PackageName "deepseq", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Deep evaluation of data structures", description = "This package provides methods for fully evaluating data structures\n(\\\"deep evaluation\\\"). Deep evaluation is often used for adding\nstrictness to a program, e.g. in order to force pending exceptions,\nremove space leaks, or force lazy I/O to happen. It is also useful\nin parallel programs, to ensure pending work does not migrate to the\nwrong thread.\n\nThe primary use of this package is via the 'deepseq' function, a\n\\\"deep\\\" version of 'seq'. It is implemented on top of an 'NFData'\ntypeclass (\\\"Normal Form Data\\\", data structures with no unevaluated\ncomponents) which defines strategies for fully evaluating different\ndata types.\n", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","DeepSeq"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], hsLibraries = ["HSdeepseq-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0/deepseq.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0"]}])]),(PackageName "derive",fromList [(Version {versionBranch = [2,5,11], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/derive/", pkgUrl = "", synopsis = "A program and library to derive instances for data types", description = "Data.Derive is a library and a tool for deriving instances for Haskell programs.\nIt is designed to work with custom derivations, SYB and Template Haskell mechanisms.\nThe tool requires GHC, but the generated code is portable to all compilers.\nWe see this tool as a competitor to DrIFT.", category = "Development", exposed = True, exposedModules = [ModuleName ["Data","DeriveMain"],ModuleName ["Data","DeriveTH"],ModuleName ["Data","DeriveDSL"],ModuleName ["Data","Derive","All"],ModuleName ["Data","Derive","DSL","Apply"],ModuleName ["Data","Derive","DSL","Derive"],ModuleName ["Data","Derive","DSL","DSL"],ModuleName ["Data","Derive","DSL","HSE"],ModuleName ["Data","Derive","DSL","SYB"],ModuleName ["Data","Derive","Instance","Arities"],ModuleName ["Data","Derive","Class","Arities"],ModuleName ["Data","Derive","Class","Default"],ModuleName ["Language","Haskell"],ModuleName ["Language","Haskell","Convert"],ModuleName ["Language","Haskell","TH","All"],ModuleName ["Language","Haskell","TH","Compat"],ModuleName ["Language","Haskell","TH","Data"],ModuleName ["Language","Haskell","TH","ExpandSynonym"],ModuleName ["Language","Haskell","TH","FixedPpr"],ModuleName ["Language","Haskell","TH","Helper"],ModuleName ["Language","Haskell","TH","Peephole"],ModuleName ["Data","Derive","Arbitrary"],ModuleName ["Data","Derive","ArbitraryOld"],ModuleName ["Data","Derive","Arities"],ModuleName ["Data","Derive","Binary"],ModuleName ["Data","Derive","BinaryDefer"],ModuleName ["Data","Derive","Bounded"],ModuleName ["Data","Derive","Data"],ModuleName ["Data","Derive","DataAbstract"],ModuleName ["Data","Derive","Default"],ModuleName ["Data","Derive","Enum"],ModuleName ["Data","Derive","EnumCyclic"],ModuleName ["Data","Derive","Eq"],ModuleName ["Data","Derive","Fold"],ModuleName ["Data","Derive","Foldable"],ModuleName ["Data","Derive","From"],ModuleName ["Data","Derive","Functor"],ModuleName ["Data","Derive","Has"],ModuleName ["Data","Derive","Is"],ModuleName ["Data","Derive","JSON"],ModuleName ["Data","Derive","LazySet"],ModuleName ["Data","Derive","Lens"],ModuleName ["Data","Derive","Monoid"],ModuleName ["Data","Derive","NFData"],ModuleName ["Data","Derive","Ord"],ModuleName ["Data","Derive","Read"],ModuleName ["Data","Derive","Ref"],ModuleName ["Data","Derive","Serial"],ModuleName ["Data","Derive","Serialize"],ModuleName ["Data","Derive","Set"],ModuleName ["Data","Derive","Show"],ModuleName ["Data","Derive","Traversable"],ModuleName ["Data","Derive","Typeable"],ModuleName ["Data","Derive","UniplateDirect"],ModuleName ["Data","Derive","UniplateTypeable"],ModuleName ["Data","Derive","Update"],ModuleName ["Data","Derive","Internal","Derivation"]], hiddenModules = [ModuleName ["Data","Derive","Internal","Instance"],ModuleName ["Data","Derive","Internal","Traversal"],ModuleName ["Derive","Main"],ModuleName ["Derive","Derivation"],ModuleName ["Derive","Flags"],ModuleName ["Derive","Generate"],ModuleName ["Derive","Test"],ModuleName ["Derive","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], hsLibraries = ["HSderive-2.5.11"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b",InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html/derive.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html"]}])]),(PackageName "directory",fromList [(Version {versionBranch = [1,1,0,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691", sourcePackageId = PackageIdentifier {pkgName = PackageName "directory", pkgVersion = Version {versionBranch = [1,1,0,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "library for directory handling", description = "This package provides a library for handling directories.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Directory"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], hsLibraries = ["HSdirectory-1.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2/include"], includes = ["HsDirectory.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2/directory.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2"]}])]),(PackageName "distributed-process",fromList [(Version {versionBranch = [0,4,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "watson.timothy@gmail.com, edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Cloud Haskell: Erlang-style concurrency in Haskell", description = "This is an implementation of Cloud Haskell, as described in\n/Towards Haskell in the Cloud/ by Jeff Epstein, Andrew Black,\nand Simon Peyton Jones\n(),\nalthough some of the details are different. The precise message\npassing semantics are based on /A unified semantics for future Erlang/\nby Hans Svensson, Lars-\197ke Fredlund and Clara Benac Earle.\nYou will probably also want to install a Cloud Haskell backend such\nas distributed-process-simplelocalnet.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Process","Internal","Closure","TH"],ModuleName ["Control","Distributed","Process"],ModuleName ["Control","Distributed","Process","Serializable"],ModuleName ["Control","Distributed","Process","Closure"],ModuleName ["Control","Distributed","Process","Node"],ModuleName ["Control","Distributed","Process","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Internal","CQueue"],ModuleName ["Control","Distributed","Process","Internal","Types"],ModuleName ["Control","Distributed","Process","Internal","Trace"],ModuleName ["Control","Distributed","Process","Internal","Closure","BuiltIn"],ModuleName ["Control","Distributed","Process","Internal","Messaging"],ModuleName ["Control","Distributed","Process","Internal","StrictList"],ModuleName ["Control","Distributed","Process","Internal","StrictMVar"],ModuleName ["Control","Distributed","Process","Internal","WeakTQueue"],ModuleName ["Control","Distributed","Process","Internal","StrictContainerAccessors"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], hsLibraries = ["HSdistributed-process-0.4.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb",InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0",InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html/distributed-process.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html"]}])]),(PackageName "distributed-static",fromList [(Version {versionBranch = [0,2,1,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-static", pkgVersion = Version {versionBranch = [0,2,1,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://www.github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Compositional, type-safe, polymorphic static values and closures", description = "/Towards Haskell in the Cloud/ (Epstein et al, Haskell\nSymposium 2011) introduces the concept of /static/ values:\nvalues that are known at compile time. In a distributed\nsetting where all nodes are running the same executable,\nstatic values can be serialized simply by transmitting a\ncode pointer to the value. This however requires special\ncompiler support, which is not yet available in ghc. We\ncan mimick the behaviour by keeping an explicit mapping\n('RemoteTable') from labels to values (and making sure\nthat all distributed nodes are using the same\n'RemoteTable'). In this module we implement this mimickry\nand various extensions: type safety (including for\npolymorphic static values) and compositionality.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Static"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], hsLibraries = ["HSdistributed-static-0.2.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html/distributed-static.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html"]}])]),(PackageName "filepath",fromList [(Version {versionBranch = [1,3,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57", sourcePackageId = PackageIdentifier {pkgName = PackageName "filepath", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Neil Mitchell", stability = "", homepage = "http://www-users.cs.york.ac.uk/~ndm/filepath/", pkgUrl = "", synopsis = "Library for manipulating FilePaths in a cross platform way.", description = "", category = "System", exposed = True, exposedModules = [ModuleName ["System","FilePath"],ModuleName ["System","FilePath","Posix"],ModuleName ["System","FilePath","Windows"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], hsLibraries = ["HSfilepath-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0/filepath.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0"]}])]),(PackageName "ghc-prim",fromList [(Version {versionBranch = [0,2,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7", sourcePackageId = PackageIdentifier {pkgName = PackageName "ghc-prim", pkgVersion = Version {versionBranch = [0,2,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "GHC primitives", description = "GHC primitives.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Prim"],ModuleName ["GHC","Classes"],ModuleName ["GHC","CString"],ModuleName ["GHC","Debug"],ModuleName ["GHC","Generics"],ModuleName ["GHC","Magic"],ModuleName ["GHC","PrimopWrappers"],ModuleName ["GHC","IntWord64"],ModuleName ["GHC","Tuple"],ModuleName ["GHC","Types"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], hsLibraries = ["HSghc-prim-0.2.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0/ghc-prim.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0"]}])]),(PackageName "hashable",fromList [(Version {versionBranch = [1,1,2,5], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c", sourcePackageId = PackageIdentifier {pkgName = PackageName "hashable", pkgVersion = Version {versionBranch = [1,1,2,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "johan.tibell@gmail.com", author = "Milan Straka \nJohan Tibell ", stability = "Provisional", homepage = "http://github.com/tibbe/hashable", pkgUrl = "", synopsis = "A class for types that can be converted to a hash value", description = "This package defines a class, 'Hashable', for types that\ncan be converted to a hash value. This class\nexists for the benefit of hashing-based data\nstructures. The package provides instances for\nbasic types and a way to combine hash values.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Hashable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], hsLibraries = ["HShashable-1.1.2.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html/hashable.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html"]}])]),(PackageName "haskell-src-exts",fromList [(Version {versionBranch = [1,13,5], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697", sourcePackageId = PackageIdentifier {pkgName = PackageName "haskell-src-exts", pkgVersion = Version {versionBranch = [1,13,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Niklas Broberg ", author = "Niklas Broberg", stability = "Stable", homepage = "http://code.haskell.org/haskell-src-exts", pkgUrl = "", synopsis = "Manipulating Haskell source: abstract syntax, lexer, parser, and pretty-printer", description = "Haskell-Source with Extensions (HSE, haskell-src-exts)\nis an extension of the standard haskell-src package,\nand handles most registered syntactic extensions to Haskell, including:\n\n* Multi-parameter type classes with functional dependencies\n\n* Indexed type families (including associated types)\n\n* Empty data declarations\n\n* GADTs\n\n* Implicit parameters\n\n* Template Haskell\n\nand a few more. All extensions implemented in GHC are supported.\nApart from these standard extensions,\nit also handles regular patterns as per the HaRP extension\nas well as HSX-style embedded XML syntax.", category = "Language", exposed = True, exposedModules = [ModuleName ["Language","Haskell","Exts"],ModuleName ["Language","Haskell","Exts","Lexer"],ModuleName ["Language","Haskell","Exts","Parser"],ModuleName ["Language","Haskell","Exts","Pretty"],ModuleName ["Language","Haskell","Exts","Syntax"],ModuleName ["Language","Haskell","Exts","Extension"],ModuleName ["Language","Haskell","Exts","Build"],ModuleName ["Language","Haskell","Exts","Fixity"],ModuleName ["Language","Haskell","Exts","Comments"],ModuleName ["Language","Haskell","Exts","SrcLoc"],ModuleName ["Language","Haskell","Exts","Annotated"],ModuleName ["Language","Haskell","Exts","Annotated","Syntax"],ModuleName ["Language","Haskell","Exts","Annotated","Fixity"],ModuleName ["Language","Haskell","Exts","Annotated","Build"],ModuleName ["Language","Haskell","Exts","Annotated","ExactPrint"],ModuleName ["Language","Haskell","Exts","Annotated","Simplify"]], hiddenModules = [ModuleName ["Language","Haskell","Exts","ExtScheme"],ModuleName ["Language","Haskell","Exts","ParseMonad"],ModuleName ["Language","Haskell","Exts","ParseSyntax"],ModuleName ["Language","Haskell","Exts","InternalLexer"],ModuleName ["Language","Haskell","Exts","ParseUtils"],ModuleName ["Language","Haskell","Exts","InternalParser"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], hsLibraries = ["HShaskell-src-exts-1.13.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html/haskell-src-exts.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html"]}])]),(PackageName "integer-gmp",fromList [(Version {versionBranch = [0,4,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43", sourcePackageId = PackageIdentifier {pkgName = PackageName "integer-gmp", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Integer library based on GMP", description = "This package contains an Integer library based on GMP.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Integer"],ModuleName ["GHC","Integer","GMP","Internals"],ModuleName ["GHC","Integer","GMP","Prim"],ModuleName ["GHC","Integer","Logarithms"],ModuleName ["GHC","Integer","Logarithms","Internals"]], hiddenModules = [ModuleName ["GHC","Integer","Type"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], hsLibraries = ["HSinteger-gmp-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0/integer-gmp.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0"]}])]),(PackageName "mtl",fromList [(Version {versionBranch = [2,1,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29", sourcePackageId = PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Edward Kmett ", author = "Andy Gill", stability = "", homepage = "http://github.com/ekmett/mtl", pkgUrl = "", synopsis = "Monad classes, using functional dependencies", description = "Monad classes using functional dependencies, with instances\nfor various monad transformers, inspired by the paper\n/Functional Programming with Overloading and Higher-Order Polymorphism/,\nby Mark P Jones, in /Advanced School of Functional Programming/, 1995\n().", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Monad","Cont"],ModuleName ["Control","Monad","Cont","Class"],ModuleName ["Control","Monad","Error"],ModuleName ["Control","Monad","Error","Class"],ModuleName ["Control","Monad","Identity"],ModuleName ["Control","Monad","List"],ModuleName ["Control","Monad","RWS"],ModuleName ["Control","Monad","RWS","Class"],ModuleName ["Control","Monad","RWS","Lazy"],ModuleName ["Control","Monad","RWS","Strict"],ModuleName ["Control","Monad","Reader"],ModuleName ["Control","Monad","Reader","Class"],ModuleName ["Control","Monad","State"],ModuleName ["Control","Monad","State","Class"],ModuleName ["Control","Monad","State","Lazy"],ModuleName ["Control","Monad","State","Strict"],ModuleName ["Control","Monad","Trans"],ModuleName ["Control","Monad","Writer"],ModuleName ["Control","Monad","Writer","Class"],ModuleName ["Control","Monad","Writer","Lazy"],ModuleName ["Control","Monad","Writer","Strict"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], hsLibraries = ["HSmtl-2.1.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html/mtl.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html"]}])]),(PackageName "network-transport",fromList [(Version {versionBranch = [0,3,0,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812", sourcePackageId = PackageIdentifier {pkgName = PackageName "network-transport", pkgVersion = Version {versionBranch = [0,3,0,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Network abstraction layer", description = "\"Network.Transport\" is a Network Abstraction Layer which provides\nthe following high-level concepts:\n\n* Nodes in the network are represented by 'EndPoint's. These are\nheavyweight stateful objects.\n\n* Each 'EndPoint' has an 'EndPointAddress'.\n\n* Connections can be established from one 'EndPoint' to another\nusing the 'EndPointAddress' of the remote end.\n\n* The 'EndPointAddress' can be serialised and sent over the\nnetwork, where as 'EndPoint's and connections cannot.\n\n* Connections between 'EndPoint's are unidirectional and lightweight.\n\n* Outgoing messages are sent via a 'Connection' object that\nrepresents the sending end of the connection.\n\n* Incoming messages for /all/ of the incoming connections on\nan 'EndPoint' are collected via a shared receive queue.\n\n* In addition to incoming messages, 'EndPoint's are notified of\nother 'Event's such as new connections or broken connections.\n\nThis design was heavily influenced by the design of the Common\nCommunication Interface\n().\nImportant design goals are:\n\n* Connections should be lightweight: it should be no problem to\ncreate thousands of connections between endpoints.\n\n* Error handling is explicit: every function declares as part of\nits type which errors it can return (no exceptions are thrown)\n\n* Error handling is \"abstract\": errors that originate from\nimplementation specific problems (such as \"no more sockets\" in\nthe TCP implementation) get mapped to generic errors\n(\"insufficient resources\") at the Transport level.\n\nThis package provides the generic interface only; you will\nprobably also want to install at least one transport\nimplementation (network-transport-*).", category = "Network", exposed = True, exposedModules = [ModuleName ["Network","Transport"],ModuleName ["Network","Transport","Util"],ModuleName ["Network","Transport","Internal"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], hsLibraries = ["HSnetwork-transport-0.3.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html/network-transport.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html"]}])]),(PackageName "old-locale",fromList [(Version {versionBranch = [1,0,0,4], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-locale", pkgVersion = Version {versionBranch = [1,0,0,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "locale library", description = "This package provides the old locale library.\nFor new code, the new locale library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Locale"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], hsLibraries = ["HSold-locale-1.0.0.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4/old-locale.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4"]}])]),(PackageName "old-time",fromList [(Version {versionBranch = [1,1,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-time", pkgVersion = Version {versionBranch = [1,1,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Time library", description = "This package provides the old time library.\nFor new code, the new time library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Time"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], hsLibraries = ["HSold-time-1.1.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0/include"], includes = ["HsTime.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0/old-time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0"]}])]),(PackageName "pretty",fromList [(Version {versionBranch = [1,1,1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b", sourcePackageId = PackageIdentifier {pkgName = PackageName "pretty", pkgVersion = Version {versionBranch = [1,1,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "David Terei ", author = "", stability = "Stable", homepage = "http://github.com/haskell/pretty", pkgUrl = "", synopsis = "Pretty-printing library", description = "This package contains a pretty-printing library, a set of API's\nthat provides a way to easily print out text in a consistent\nformat of your choosing. This is useful for compilers and related\ntools.\n\nThis library was originally designed by John Hughes's and has since\nbeen heavily modified by Simon Peyton Jones.", category = "Text", exposed = True, exposedModules = [ModuleName ["Text","PrettyPrint"],ModuleName ["Text","PrettyPrint","HughesPJ"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], hsLibraries = ["HSpretty-1.1.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0/pretty.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0"]}])]),(PackageName "process",fromList [(Version {versionBranch = [1,1,0,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4", sourcePackageId = PackageIdentifier {pkgName = PackageName "process", pkgVersion = Version {versionBranch = [1,1,0,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Process libraries", description = "This package contains libraries for dealing with system processes.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Process","Internals"],ModuleName ["System","Process"],ModuleName ["System","Cmd"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], hsLibraries = ["HSprocess-1.1.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1/include"], includes = ["runProcess.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1/process.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1"]}])]),(PackageName "random",fromList [(Version {versionBranch = [1,0,1,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1", sourcePackageId = PackageIdentifier {pkgName = PackageName "random", pkgVersion = Version {versionBranch = [1,0,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "rrnewton@gmail.com", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "random number library", description = "This package provides a basic random number generation\nlibrary, including the ability to split random number\ngenerators.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Random"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], hsLibraries = ["HSrandom-1.0.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html/random.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html"]}])]),(PackageName "rank1dynamic",fromList [(Version {versionBranch = [0,1,0,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0", sourcePackageId = PackageIdentifier {pkgName = PackageName "rank1dynamic", pkgVersion = Version {versionBranch = [0,1,0,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types", description = "\"Data.Typeable\" and \"Data.Dynamic\" only support monomorphic types.\nIn this package we provide similar functionality but with\nsupport for rank-1 polymorphic types.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Rank1Dynamic"],ModuleName ["Data","Rank1Typeable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], hsLibraries = ["HSrank1dynamic-0.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html/rank1dynamic.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html"]}])]),(PackageName "rts",fromList [(Version {versionBranch = [1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "builtin_rts", sourcePackageId = PackageIdentifier {pkgName = PackageName "rts", pkgVersion = Version {versionBranch = [1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "glasgow-haskell-users@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "", category = "", exposed = True, exposedModules = [], hiddenModules = [], trusted = False, importDirs = [], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2"], hsLibraries = ["HSrts"], extraLibraries = ["m","dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/include"], includes = ["Stg.h"], depends = [], hugsOptions = [], ccOptions = [], ldOptions = ["-u","_ghczmprim_GHCziTypes_Izh_static_info","-u","_ghczmprim_GHCziTypes_Czh_static_info","-u","_ghczmprim_GHCziTypes_Fzh_static_info","-u","_ghczmprim_GHCziTypes_Dzh_static_info","-u","_base_GHCziPtr_Ptr_static_info","-u","_base_GHCziWord_Wzh_static_info","-u","_base_GHCziInt_I8zh_static_info","-u","_base_GHCziInt_I16zh_static_info","-u","_base_GHCziInt_I32zh_static_info","-u","_base_GHCziInt_I64zh_static_info","-u","_base_GHCziWord_W8zh_static_info","-u","_base_GHCziWord_W16zh_static_info","-u","_base_GHCziWord_W32zh_static_info","-u","_base_GHCziWord_W64zh_static_info","-u","_base_GHCziStable_StablePtr_static_info","-u","_ghczmprim_GHCziTypes_Izh_con_info","-u","_ghczmprim_GHCziTypes_Czh_con_info","-u","_ghczmprim_GHCziTypes_Fzh_con_info","-u","_ghczmprim_GHCziTypes_Dzh_con_info","-u","_base_GHCziPtr_Ptr_con_info","-u","_base_GHCziPtr_FunPtr_con_info","-u","_base_GHCziStable_StablePtr_con_info","-u","_ghczmprim_GHCziTypes_False_closure","-u","_ghczmprim_GHCziTypes_True_closure","-u","_base_GHCziPack_unpackCString_closure","-u","_base_GHCziIOziException_stackOverflow_closure","-u","_base_GHCziIOziException_heapOverflow_closure","-u","_base_ControlziExceptionziBase_nonTermination_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnMVar_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnSTM_closure","-u","_base_ControlziExceptionziBase_nestedAtomically_closure","-u","_base_GHCziWeak_runFinalizzerBatch_closure","-u","_base_GHCziTopHandler_flushStdHandles_closure","-u","_base_GHCziTopHandler_runIO_closure","-u","_base_GHCziTopHandler_runNonIO_closure","-u","_base_GHCziConcziIO_ensureIOManagerIsRunning_closure","-u","_base_GHCziConcziSync_runSparks_closure","-u","_base_GHCziConcziSignal_runHandlers_closure","-Wl,-search_paths_first"], frameworkDirs = [], frameworks = [], haddockInterfaces = [], haddockHTMLs = []}])]),(PackageName "stm",fromList [(Version {versionBranch = [2,4], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d", sourcePackageId = PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Software Transactional Memory", description = "A modular composable concurrency abstraction.\n\nChanges in version 2.4\n\n* Added \"Control.Concurrent.STM.TQueue\" (a faster @TChan@)\n\n* Added \"Control.Concurrent.STM.TBQueue\" (a bounded channel based on @TQueue@)\n\n* @TChan@ has an @Eq@ instances\n\n* Added @newBroadcastTChan@ and @newBroadcastTChanIO@\n\n* Some performance improvements for @TChan@\n\n* Added @cloneTChan@", category = "Concurrency", exposed = True, exposedModules = [ModuleName ["Control","Concurrent","STM"],ModuleName ["Control","Concurrent","STM","TArray"],ModuleName ["Control","Concurrent","STM","TVar"],ModuleName ["Control","Concurrent","STM","TChan"],ModuleName ["Control","Concurrent","STM","TMVar"],ModuleName ["Control","Concurrent","STM","TQueue"],ModuleName ["Control","Concurrent","STM","TBQueue"],ModuleName ["Control","Monad","STM"]], hiddenModules = [ModuleName ["Control","Sequential","STM"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], hsLibraries = ["HSstm-2.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html/stm.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html"]}])]),(PackageName "syb",fromList [(Version {versionBranch = [0,3,7], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24", sourcePackageId = PackageIdentifier {pkgName = PackageName "syb", pkgVersion = Version {versionBranch = [0,3,7], versionTags = []}}, license = BSD3, copyright = "", maintainer = "generics@haskell.org", author = "Ralf Lammel, Simon Peyton Jones, Jose Pedro Magalhaes", stability = "provisional", homepage = "http://www.cs.uu.nl/wiki/GenericProgramming/SYB", pkgUrl = "", synopsis = "Scrap Your Boilerplate", description = "This package contains the generics system described in the\n/Scrap Your Boilerplate/ papers (see\n).\nIt defines the @Data@ class of types permitting folding and unfolding\nof constructor applications, instances of this class for primitive\ntypes, and a variety of traversals.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics"],ModuleName ["Data","Generics","Basics"],ModuleName ["Data","Generics","Instances"],ModuleName ["Data","Generics","Aliases"],ModuleName ["Data","Generics","Schemes"],ModuleName ["Data","Generics","Text"],ModuleName ["Data","Generics","Twins"],ModuleName ["Data","Generics","Builders"],ModuleName ["Generics","SYB"],ModuleName ["Generics","SYB","Basics"],ModuleName ["Generics","SYB","Instances"],ModuleName ["Generics","SYB","Aliases"],ModuleName ["Generics","SYB","Schemes"],ModuleName ["Generics","SYB","Text"],ModuleName ["Generics","SYB","Twins"],ModuleName ["Generics","SYB","Builders"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], hsLibraries = ["HSsyb-0.3.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html/syb.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html"]}])]),(PackageName "template-haskell",fromList [(Version {versionBranch = [2,7,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949", sourcePackageId = PackageIdentifier {pkgName = PackageName "template-haskell", pkgVersion = Version {versionBranch = [2,7,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "Facilities for manipulating Haskell source code using Template Haskell.", category = "", exposed = True, exposedModules = [ModuleName ["Language","Haskell","TH","Syntax","Internals"],ModuleName ["Language","Haskell","TH","Syntax"],ModuleName ["Language","Haskell","TH","PprLib"],ModuleName ["Language","Haskell","TH","Ppr"],ModuleName ["Language","Haskell","TH","Lib"],ModuleName ["Language","Haskell","TH","Quote"],ModuleName ["Language","Haskell","TH"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], hsLibraries = ["HStemplate-haskell-2.7.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0/template-haskell.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0"]}])]),(PackageName "text",fromList [(Version {versionBranch = [0,11,2,3], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c", sourcePackageId = PackageIdentifier {pkgName = PackageName "text", pkgVersion = Version {versionBranch = [0,11,2,3], versionTags = []}}, license = BSD3, copyright = "2009-2011 Bryan O'Sullivan, 2008-2009 Tom Harper", maintainer = "Bryan O'Sullivan ", author = "Bryan O'Sullivan ", stability = "", homepage = "https://github.com/bos/text", pkgUrl = "", synopsis = "An efficient packed Unicode text type.", description = "\nAn efficient packed, immutable Unicode text type (both strict and\nlazy), with a powerful loop fusion optimization framework.\n\nThe 'Text' type represents Unicode character strings, in a time and\nspace-efficient manner. This package provides text processing\ncapabilities that are optimized for performance critical use, both\nin terms of large data quantities and high speed.\n\nThe 'Text' type provides character-encoding, type-safe case\nconversion via whole-string case conversion functions. It also\nprovides a range of functions for converting 'Text' values to and from\n'ByteStrings', using several standard encodings.\n\nEfficient locale-sensitive support for text IO is also supported.\n\nThese modules are intended to be imported qualified, to avoid name\nclashes with Prelude functions, e.g.\n\n> import qualified Data.Text as T\n\nTo use an extended and very rich family of functions for working\nwith Unicode text (including normalization, regular expressions,\nnon-standard encodings, text breaking, and locales), see\nthe @text-icu@ package:\n\n\n—— RELEASE NOTES ——\n\nChanges in 0.11.2.0:\n\n* String literals are now converted directly from the format in\nwhich GHC stores them into 'Text', without an intermediate\ntransformation through 'String', and without inlining of\nconversion code at each site where a string literal is declared.\n", category = "Data, Text", exposed = True, exposedModules = [ModuleName ["Data","Text"],ModuleName ["Data","Text","Array"],ModuleName ["Data","Text","Encoding"],ModuleName ["Data","Text","Encoding","Error"],ModuleName ["Data","Text","Foreign"],ModuleName ["Data","Text","IO"],ModuleName ["Data","Text","Internal"],ModuleName ["Data","Text","Lazy"],ModuleName ["Data","Text","Lazy","Builder"],ModuleName ["Data","Text","Lazy","Builder","Int"],ModuleName ["Data","Text","Lazy","Builder","RealFloat"],ModuleName ["Data","Text","Lazy","Encoding"],ModuleName ["Data","Text","Lazy","IO"],ModuleName ["Data","Text","Lazy","Internal"],ModuleName ["Data","Text","Lazy","Read"],ModuleName ["Data","Text","Read"]], hiddenModules = [ModuleName ["Data","Text","Encoding","Fusion"],ModuleName ["Data","Text","Encoding","Fusion","Common"],ModuleName ["Data","Text","Encoding","Utf16"],ModuleName ["Data","Text","Encoding","Utf32"],ModuleName ["Data","Text","Encoding","Utf8"],ModuleName ["Data","Text","Fusion"],ModuleName ["Data","Text","Fusion","CaseMapping"],ModuleName ["Data","Text","Fusion","Common"],ModuleName ["Data","Text","Fusion","Internal"],ModuleName ["Data","Text","Fusion","Size"],ModuleName ["Data","Text","IO","Internal"],ModuleName ["Data","Text","Lazy","Builder","Functions"],ModuleName ["Data","Text","Lazy","Builder","RealFloat","Functions"],ModuleName ["Data","Text","Lazy","Encoding","Fusion"],ModuleName ["Data","Text","Lazy","Fusion"],ModuleName ["Data","Text","Lazy","Search"],ModuleName ["Data","Text","Private"],ModuleName ["Data","Text","Search"],ModuleName ["Data","Text","Unsafe"],ModuleName ["Data","Text","Unsafe","Base"],ModuleName ["Data","Text","UnsafeChar"],ModuleName ["Data","Text","UnsafeShift"],ModuleName ["Data","Text","Util"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], hsLibraries = ["HStext-0.11.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html/text.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html"]}])]),(PackageName "time",fromList [(Version {versionBranch = [1,4], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f", sourcePackageId = PackageIdentifier {pkgName = PackageName "time", pkgVersion = Version {versionBranch = [1,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Ashley Yakeley", stability = "stable", homepage = "http://semantic.org/TimeLib/", pkgUrl = "", synopsis = "A time library", description = "A time library", category = "System", exposed = True, exposedModules = [ModuleName ["Data","Time","Calendar"],ModuleName ["Data","Time","Calendar","MonthDay"],ModuleName ["Data","Time","Calendar","OrdinalDate"],ModuleName ["Data","Time","Calendar","WeekDate"],ModuleName ["Data","Time","Calendar","Julian"],ModuleName ["Data","Time","Calendar","Easter"],ModuleName ["Data","Time","Clock"],ModuleName ["Data","Time","Clock","POSIX"],ModuleName ["Data","Time","Clock","TAI"],ModuleName ["Data","Time","LocalTime"],ModuleName ["Data","Time","Format"],ModuleName ["Data","Time"]], hiddenModules = [ModuleName ["Data","Time","Calendar","Private"],ModuleName ["Data","Time","Calendar","Days"],ModuleName ["Data","Time","Calendar","Gregorian"],ModuleName ["Data","Time","Calendar","JulianYearDay"],ModuleName ["Data","Time","Clock","Scale"],ModuleName ["Data","Time","Clock","UTC"],ModuleName ["Data","Time","Clock","CTimeval"],ModuleName ["Data","Time","Clock","UTCDiff"],ModuleName ["Data","Time","LocalTime","TimeZone"],ModuleName ["Data","Time","LocalTime","TimeOfDay"],ModuleName ["Data","Time","LocalTime","LocalTime"],ModuleName ["Data","Time","Format","Parse"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], hsLibraries = ["HStime-1.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4/include"], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4/time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4"]}])]),(PackageName "transformers",fromList [(Version {versionBranch = [0,3,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4", sourcePackageId = PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Ross Paterson ", author = "Andy Gill, Ross Paterson", stability = "", homepage = "", pkgUrl = "", synopsis = "Concrete functor and monad transformers", description = "A portable library of functor and monad transformers, inspired by\nthe paper \\\"Functional Programming with Overloading and Higher-Order\nPolymorphism\\\", by Mark P Jones,\nin /Advanced School of Functional Programming/, 1995\n().\n\nThis package contains:\n\n* the monad transformer class (in \"Control.Monad.Trans.Class\")\n\n* concrete functor and monad transformers, each with associated\noperations and functions to lift operations associated with other\ntransformers.\n\nIt can be used on its own in portable Haskell code, or with the monad\nclasses in the @mtl@ or @monads-tf@ packages, which automatically\nlift operations introduced by monad transformers through other\ntransformers.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Applicative","Backwards"],ModuleName ["Control","Applicative","Lift"],ModuleName ["Control","Monad","IO","Class"],ModuleName ["Control","Monad","Trans","Class"],ModuleName ["Control","Monad","Trans","Cont"],ModuleName ["Control","Monad","Trans","Error"],ModuleName ["Control","Monad","Trans","Identity"],ModuleName ["Control","Monad","Trans","List"],ModuleName ["Control","Monad","Trans","Maybe"],ModuleName ["Control","Monad","Trans","Reader"],ModuleName ["Control","Monad","Trans","RWS"],ModuleName ["Control","Monad","Trans","RWS","Lazy"],ModuleName ["Control","Monad","Trans","RWS","Strict"],ModuleName ["Control","Monad","Trans","State"],ModuleName ["Control","Monad","Trans","State","Lazy"],ModuleName ["Control","Monad","Trans","State","Strict"],ModuleName ["Control","Monad","Trans","Writer"],ModuleName ["Control","Monad","Trans","Writer","Lazy"],ModuleName ["Control","Monad","Trans","Writer","Strict"],ModuleName ["Data","Functor","Compose"],ModuleName ["Data","Functor","Constant"],ModuleName ["Data","Functor","Identity"],ModuleName ["Data","Functor","Product"],ModuleName ["Data","Functor","Reverse"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], hsLibraries = ["HStransformers-0.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html/transformers.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html"]}])]),(PackageName "uniplate",fromList [(Version {versionBranch = [1,6,7], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd", sourcePackageId = PackageIdentifier {pkgName = PackageName "uniplate", pkgVersion = Version {versionBranch = [1,6,7], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/uniplate/", pkgUrl = "", synopsis = "Help writing simple, concise and fast generic operations.", description = "Uniplate is library for writing simple and concise generic operations.\nUniplate has similar goals to the original Scrap Your Boilerplate work,\nbut is substantially simpler and faster. The Uniplate manual is available at\n.\n\nTo get started with Uniplate you should import one of the three following\nmodules:\n\n* \"Data.Generics.Uniplate.Data\" - to quickly start writing generic functions.\nMost users should start by importing this module.\n\n* \"Data.Generics.Uniplate.Direct\" - a replacement for \"Data.Generics.Uniplate.Data\"\nwith substantially higher performance (around 5 times), but requires writing\ninstance declarations.\n\n* \"Data.Generics.Uniplate.Operations\" - definitions of all the operations defined\nby Uniplate. Both the above two modules re-export this module.\n\nIn addition, some users may want to make use of the following modules:\n\n* \"Data.Generics.Uniplate.Zipper\" - a zipper built on top of Uniplate instances.\n\n* \"Data.Generics.SYB\" - users transitioning from the Scrap Your Boilerplate library.\n\n* \"Data.Generics.Compos\" - users transitioning from the Compos library.\n\n* \"Data.Generics.Uniplate.DataOnly\" - users making use of both @Data@ and @Direct@\nto avoid getting instance conflicts.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics","Str"],ModuleName ["Data","Generics","Compos"],ModuleName ["Data","Generics","SYB"],ModuleName ["Data","Generics","Uniplate","Data"],ModuleName ["Data","Generics","Uniplate","Data","Instances"],ModuleName ["Data","Generics","Uniplate","DataOnly"],ModuleName ["Data","Generics","Uniplate","Direct"],ModuleName ["Data","Generics","Uniplate","Operations"],ModuleName ["Data","Generics","Uniplate","Typeable"],ModuleName ["Data","Generics","Uniplate","Zipper"],ModuleName ["Data","Generics","Uniplate"],ModuleName ["Data","Generics","UniplateOn"],ModuleName ["Data","Generics","UniplateStr"],ModuleName ["Data","Generics","UniplateStrOn"],ModuleName ["Data","Generics","Biplate"],ModuleName ["Data","Generics","PlateDirect"],ModuleName ["Data","Generics","PlateTypeable"],ModuleName ["Data","Generics","PlateData"]], hiddenModules = [ModuleName ["Data","Generics","PlateInternal"],ModuleName ["Data","Generics","Uniplate","Internal","Data"],ModuleName ["Data","Generics","Uniplate","Internal","DataOnlyOperations"],ModuleName ["Data","Generics","Uniplate","Internal","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], hsLibraries = ["HSuniplate-1.6.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html/uniplate.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html"]}])]),(PackageName "unix",fromList [(Version {versionBranch = [2,5,1,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e", sourcePackageId = PackageIdentifier {pkgName = PackageName "unix", pkgVersion = Version {versionBranch = [2,5,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "POSIX functionality", description = "This package gives you access to the set of operating system\nservices standardised by POSIX 1003.1b (or the IEEE Portable\nOperating System Interface for Computing Environments -\nIEEE Std. 1003.1).\n\nThe package is not supported under Windows (except under Cygwin).", category = "System", exposed = True, exposedModules = [ModuleName ["System","Posix"],ModuleName ["System","Posix","ByteString"],ModuleName ["System","Posix","Error"],ModuleName ["System","Posix","Resource"],ModuleName ["System","Posix","Time"],ModuleName ["System","Posix","Unistd"],ModuleName ["System","Posix","User"],ModuleName ["System","Posix","Signals"],ModuleName ["System","Posix","Signals","Exts"],ModuleName ["System","Posix","Semaphore"],ModuleName ["System","Posix","SharedMem"],ModuleName ["System","Posix","ByteString","FilePath"],ModuleName ["System","Posix","Directory"],ModuleName ["System","Posix","Directory","ByteString"],ModuleName ["System","Posix","DynamicLinker","Module"],ModuleName ["System","Posix","DynamicLinker","Module","ByteString"],ModuleName ["System","Posix","DynamicLinker","Prim"],ModuleName ["System","Posix","DynamicLinker","ByteString"],ModuleName ["System","Posix","DynamicLinker"],ModuleName ["System","Posix","Files"],ModuleName ["System","Posix","Files","ByteString"],ModuleName ["System","Posix","IO"],ModuleName ["System","Posix","IO","ByteString"],ModuleName ["System","Posix","Env"],ModuleName ["System","Posix","Env","ByteString"],ModuleName ["System","Posix","Process"],ModuleName ["System","Posix","Process","Internals"],ModuleName ["System","Posix","Process","ByteString"],ModuleName ["System","Posix","Temp"],ModuleName ["System","Posix","Temp","ByteString"],ModuleName ["System","Posix","Terminal"],ModuleName ["System","Posix","Terminal","ByteString"]], hiddenModules = [ModuleName ["System","Posix","Directory","Common"],ModuleName ["System","Posix","DynamicLinker","Common"],ModuleName ["System","Posix","Files","Common"],ModuleName ["System","Posix","IO","Common"],ModuleName ["System","Posix","Process","Common"],ModuleName ["System","Posix","Terminal","Common"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], hsLibraries = ["HSunix-2.5.1.1"], extraLibraries = ["dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1/include"], includes = ["HsUnix.h","execvpe.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1/unix.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1"]}])]),(PackageName "unordered-containers",fromList [(Version {versionBranch = [0,2,2,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d", sourcePackageId = PackageIdentifier {pkgName = PackageName "unordered-containers", pkgVersion = Version {versionBranch = [0,2,2,1], versionTags = []}}, license = BSD3, copyright = "2010-2012 Johan Tibell\n2010 Edward Z. Yang", maintainer = "johan.tibell@gmail.com", author = "Johan Tibell", stability = "", homepage = "https://github.com/tibbe/unordered-containers", pkgUrl = "", synopsis = "Efficient hashing-based container types", description = "Efficient hashing-based container types. The containers have been\noptimized for performance critical use, both in terms of large data\nquantities and high speed.\n\nThe declared cost of each operation is either worst-case or\namortized, but remains valid even if structures are shared.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","HashMap","Lazy"],ModuleName ["Data","HashMap","Strict"],ModuleName ["Data","HashSet"]], hiddenModules = [ModuleName ["Data","HashMap","Array"],ModuleName ["Data","HashMap","Base"],ModuleName ["Data","HashMap","PopCount"],ModuleName ["Data","HashMap","Unsafe"],ModuleName ["Data","HashMap","UnsafeShift"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], hsLibraries = ["HSunordered-containers-0.2.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html/unordered-containers.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html"]}])])]), pkgDescrFile = Just "./distributed-process-platform.cabal", localPkgDescr = PackageDescription {package = PackageIdentifier {pkgName = PackageName "distributed-process-platform", pkgVersion = Version {versionBranch = [0,1,0], versionTags = []}}, license = BSD3, licenseFile = "LICENCE", copyright = "Tim Watson 2012 - 2013", maintainer = "watson.timothy@gmail.com", author = "Tim Watson", stability = "experimental", testedWith = [(GHC,ThisVersion (Version {versionBranch = [7,4,2], versionTags = []}))], homepage = "http://github.com/haskell-distributed/distributed-process-platform", pkgUrl = "", bugReports = "http://github.com/haskell-distributed/distributed-process-platform/issues", sourceRepos = [SourceRepo {repoKind = RepoHead, repoType = Just Git, repoLocation = Just "https://github.com/haskell-distributed/distributed-process-platform", repoModule = Nothing, repoBranch = Nothing, repoTag = Nothing, repoSubdir = Nothing}], synopsis = "The Cloud Haskell Application Platform", description = "Modelled after Erlang's OTP, this framework provides similar\nfacilities for Cloud Haskell, grouping essential practices\ninto a set of modules and standards designed to help you build\nconcurrent, distributed applications with relative ease.", category = "Control", customFieldsPD = [], buildDepends = [Dependency (PackageName "base") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4], versionTags = []})) (LaterVersion (Version {versionBranch = [4], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4,2], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))], specVersionRaw = Right (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,8], versionTags = []})) (LaterVersion (Version {versionBranch = [1,8], versionTags = []}))), buildType = Just Simple, library = Just (Library {exposedModules = [ModuleName ["Control","Distributed","Process","Platform"],ModuleName ["Control","Distributed","Process","Platform","Async"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncChan"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncSTM"],ModuleName ["Control","Distributed","Process","Platform","Call"],ModuleName ["Control","Distributed","Process","Platform","Test"],ModuleName ["Control","Distributed","Process","Platform","Time"],ModuleName ["Control","Distributed","Process","Platform","Timer"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"]], libExposed = True, libBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src"], otherModules = [ModuleName ["Control","Distributed","Process","Platform","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Platform","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","Internal","Common"],ModuleName ["Control","Distributed","Process","Platform","Async","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [], targetBuildDepends = [Dependency (PackageName "base") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4], versionTags = []})) (LaterVersion (Version {versionBranch = [4], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4,2], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}}), executables = [], testSuites = [TestSuite {testName = "GenServerTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestUtils.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["Control","Distributed","Process","Platform"],ModuleName ["Control","Distributed","Process","Platform","Async"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncChan"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncSTM"],ModuleName ["Control","Distributed","Process","Platform","Call"],ModuleName ["Control","Distributed","Process","Platform","Test"],ModuleName ["Control","Distributed","Process","Platform","Time"],ModuleName ["Control","Distributed","Process","Platform","Timer"],ModuleName ["Control","Distributed","Process","Platform","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Platform","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","Internal","Common"],ModuleName ["Control","Distributed","Process","Platform","Async","Types"],ModuleName ["TestUtils"],ModuleName ["MathsDemo"],ModuleName ["Counter"],ModuleName ["SimplePool"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False},TestSuite {testName = "AsyncTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestAsync.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["TestUtils"],ModuleName ["TestGenServer"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False},TestSuite {testName = "PrimitivesTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestPrimitives.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["TestUtils"],ModuleName ["TestGenServer"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False},TestSuite {testName = "TimerTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestTimer.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False}], benchmarks = [], dataFiles = [], dataDir = "", extraSrcFiles = [], extraTmpFiles = []}, withPrograms = [("alex",ConfiguredProgram {programId = "alex", programVersion = Just (Version {versionBranch = [3,0,2], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/alex"}}),("ar",ConfiguredProgram {programId = "ar", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/ar"}}),("gcc",ConfiguredProgram {programId = "gcc", programVersion = Nothing, programDefaultArgs = ["-m64"], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/gcc"}}),("ghc",ConfiguredProgram {programId = "ghc", programVersion = Just (Version {versionBranch = [7,4,2], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/ghc"}}),("ghc-pkg",ConfiguredProgram {programId = "ghc-pkg", programVersion = Just (Version {versionBranch = [7,4,2], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/ghc-pkg"}}),("haddock",ConfiguredProgram {programId = "haddock", programVersion = Just (Version {versionBranch = [2,11,0], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/haddock"}}),("happy",ConfiguredProgram {programId = "happy", programVersion = Just (Version {versionBranch = [1,18,10], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/happy"}}),("hpc",ConfiguredProgram {programId = "hpc", programVersion = Just (Version {versionBranch = [0,6], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/hpc"}}),("hsc2hs",ConfiguredProgram {programId = "hsc2hs", programVersion = Just (Version {versionBranch = [0,67], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/hsc2hs"}}),("ld",ConfiguredProgram {programId = "ld", programVersion = Nothing, programDefaultArgs = ["-x","-arch","x86_64"], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/ld"}}),("ranlib",ConfiguredProgram {programId = "ranlib", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/ranlib"}}),("strip",ConfiguredProgram {programId = "strip", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/strip"}}),("tar",ConfiguredProgram {programId = "tar", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/tar"}})], withPackageDB = [GlobalPackageDB,UserPackageDB], withVanillaLib = True, withProfLib = True, withSharedLib = False, withDynExe = False, withProfExe = False, withOptimization = NormalOptimisation, withGHCiLib = True, splitObjs = False, stripExes = True, progPrefix = "", progSuffix = ""} \ No newline at end of file diff --git a/documentation.md b/documentation.md new file mode 100644 index 00000000..57cb48e1 --- /dev/null +++ b/documentation.md @@ -0,0 +1,353 @@ +--- +layout: documentation +title: Documentation +--- + +### Cloud Haskell Platform + +This is the [*Cloud Haskell Platform*][1]. Cloud Haskell is a set of libraries +that bring Erlang-style concurrency and distribution to Haskell programs. This +project is an implementation of that distributed computing interface, where +processes communicate with one another through explicit message passing rather +than shared memory. + +Originally described by the joint [Towards Haskell in the Cloud][12] paper, +Cloud Haskell has be re-written from the ground up and supports a rich and +growing number of features for + +* building concurrent applications using asynchronous message passing +* building distributed computing applications +* building fault tolerant systems +* running Cloud Haskell nodes on various network transports +* working with several network transport implementations (and more in the pipeline) +* supporting *static* values (required for remote communication) + +API documentation for the latest releases is available on hackage. The latest +(HEAD) API documentation for the platform can be viewed +[here](/static/doc/distributed-process-platform/index.html). + +### Architecture + +Cloud Haskell comprises the following components, some of which are complete, +others experimental. + +* [distributed-process][2]: Base concurrency and distribution support +* [distributed-process-platform][3]: The Cloud Haskell Platform - APIs +* [distributed-static][4]: Support for static values +* [rank1dynamic][5]: Like `Data.Dynamic` and `Data.Typeable` but supporting polymorphic values +* [network-transport][6]: Generic `Network.Transport` API +* [network-transport-tcp][7]: TCP realisation of `Network.Transport` +* [network-transport-inmemory][8]: In-memory realisation of `Network.Transport` (incomplete) +* [network-transport-composed][9]: Compose two transports (very preliminary) +* [distributed-process-simplelocalnet][10]: Simple backend for local networks +* [distributed-process-azure][11]: Azure backend for Cloud Haskell (proof of concept) + + +One goal of Cloud Haskell is to separate the transport layer from the +process layer, so that the transport backend is entirely independent: +it is envisaged that this interface might later be used by models +other than the Cloud Haskell paradigm, and that applications built +using Cloud Haskell might be easily configured to work with different +backend transports. + +Abstracting over the transport layer allows different protocols for +message passing, including TCP/IP, UDP, +[MPI](http://en.wikipedia.org/wiki/Message_Passing_Interface), +[CCI](http://www.olcf.ornl.gov/center-projects/common-communication-interface/), +ZeroMQ, SSH, MVars, Unix pipes, and more. Each of these transports would provide +its own implementation of the `Network.Transport` and provide a means of creating +new connections for use within `Control.Distributed.Process`. This separation means +that transports might be used for other purposes than Cloud Haskell. + +The following diagram shows dependencies between the various subsystems, +in an application using Cloud Haskell, where arrows represent explicit +directional dependencies. + + +In this diagram, the various nodes roughly correspond to specific modules: + + Cloud Haskell : Control.Distributed.Process + Cloud Haskell : Control.Distributed.Process.* + Transport Interface : Network.Transport + Transport Implementation : Network.Transport.* + +An application is built using the primitives provided by the Cloud +Haskell layer, provided by `Control.Distributed.Process` module, which +provides abstractions such as nodes and processes. + +The application also depends on a Cloud Haskell Backend, which +provides functions to allow the initialisation of the transport layer +using whatever topology might be appropriate to the application. + +It is, of course, possible to create new Cloud Haskell nodes by +using a Network Transport Backend such as `Network.Transport.TCP` +directly. + +The Cloud Haskell interface and backend, make use of the Transport +interface provided by the `Network.Transport` module. +This also serves as an interface for the `Network.Transport.*` +module, which provides a specific implementation for this transport, +and may, for example, be based on some external library written in +Haskell or C. + +### Network Transport Abstraction Layer + +Cloud Haskell's generic [network-transport][6] API is entirely independent of +the concurrency and messaging passing capabilities of the *process layer*. +Cloud Haskell applications are built using the primitives provided by the +*process layer* (i.e., [distributed-process][2]), which provides abstractions +such as nodes and processes. Applications must also depend on a Cloud Haskell +Backend, which provides functions to allow the initialisation of the transport +layer using whatever topology might be appropriate to the application. + +`Network.Transport` is a network abstraction layer geared towards specific +classes of applications, offering the following high level concepts: + +* Nodes in the network are represented by `EndPoint`s. These are heavyweight stateful objects. +* Each `EndPoint` has an `EndPointAddress`. +* Connections can be established from one `EndPoint` to another using the `EndPointAddress` of the remote end. +* The `EndPointAddress` can be serialised and sent over the network, where as `EndPoint`s and connections cannot. +* Connections between `EndPoint`s are unidirectional and lightweight. +* Outgoing messages are sent via a `Connection` object that represents the sending end of the connection. +* Incoming messages for **all** of the incoming connections on an `EndPoint` are collected via a shared receive queue. +* In addition to incoming messages, `EndPoint`s are notified of other `Event`s such as new connections or broken connections. + +This design was heavily influenced by the design of the Common Communication Interface ([CCI](http://www.olcf.ornl.gov/center-projects/common-communication-interface/)). Important design goals are: + +* Connections should be lightweight: it should be no problem to create thousands of connections between endpoints. +* Error handling is explicit: every function declares as part of its type which errors it can return (no exceptions are thrown) +* Error handling is "abstract": errors that originate from implementation specific problems (such as "no more sockets" in the TCP implementation) get mapped to generic errors ("insufficient resources") at the Transport level. + +For the purposes of most Cloud Haskell applications, it is sufficient to know +enough about the `Network.Transport` API to instantiate a backend with the +required configuration and pass the returned opaque handle to the `Node` API +in order to establish a new, connected, running node. More involved setups are, +of course, possible; The simplest use of the API is thus + +{% highlight haskell %} +main :: IO +main = do + Right transport <- createTransport "127.0.0.1" "10080" defaultTCPParameters + node1 <- newLocalNode transport initRemoteTable +{% endhighlight %} + +Here we can see that the application depends explicitly on the +`defaultTCPParameters` and `createTransport` functions from +`Network.Transport.TCP`, but little else. The application *can* make use +of other `Network.Transport` APIs if required, but for the most part this +is irrelevant and the application will interact with Cloud Haskell through +the *Process Layer* and *Platform*. + +For more details about `Network.Transport` please see the [wiki page][20]. + +### Concurrency and Distribution + +The *Process Layer* is where Cloud Haskell's support for concurrency and +distributed programming are exposed to application developers. This layer +deals explicitly with + +The core of Cloud Haskell's concurrency and distribution support resides in the +[distributed-process][2] library. As well as the APIs necessary for starting +nodes and forking processes on them, we find all the basic primitives required +to + +* spawn processes locally and remotely +* send and receive messages, optionally using typed channels +* monitor and/or link to processes, channels and other nodes + +Most of this is easy enough to follow in the haddock documentation and the +various tutorials. Here we focus on the essential *concepts* behind the +process layer. + +A concurrent process is somewhat like a Haskell thread - in fact it is a +`forkIO` thread - but one that can send and receive messages through its +*process mailbox*. Each process can send messages asynchronously to other +processes, and can receive messages synchronously from its own mailbox. +The conceptual difference between threads and processes is that the latter +do not share state, but communicate only via message passing. + +Code that is executed in this manner must run in the `Process` monad. + +Processes can send data if the type implements the `Serializable` typeclass, +which is done indirectly by implementing `Binary` and deriving `Typeable`. +Implementations are already provided for primitives and some commonly used +data structures. As programmers, we see the messages in nice high-level form +(e.g., Int, String, Ping, Pong, etc), however these data have to be encoded +in order to be sent over a network. + +Not all types are `Serializable`, for example concurrency primitives such as +`MVar` and `TVar` are meaningless outside the context of threads with a shared +memory. Cloud Haskell programs remain free to use these constructs within +processes or within processes on the same machine though. If you want to +pass data between processes using *ordinary* concurrency primitives such as +`STM` then you're free to do so. Processes spawned locally are free to share +types like `TMVar a` just as normal Haskell threads are. Numerous features +in [distributed-process-platform][3] use this facility, for example the way +that `Control.Distributed.Processes.Platform.Async.AsyncSTM` handles passing +the result of its computation back to the caller: + +{% highlight haskell %} + workerPid <- spawnLocal $ do + -- ... some setup + r <- proc + void $ liftIO $ atomically $ putTMVar result (AsyncDone r) +{% endhighlight %} + +For example, we might implement a local process group using *only* message +passing, and when members enter or leave the group, a *master* process does +the book keeping to ensure the other members of the group can retain a +consist view. If we want to introduce a *group level barrier* to facilitate +mutual exclusion, we have two choices for handling this. If the process +group allows members to enter and leave on an ad-hoc basis, then a shared +memory based solution is a poor choice, because there is no *sane* way to +pass the `MVar` (or whatever) to new joiners. Locking is best achieved using +a messaging based protocol in this instance, which complicates the implementation + + +Processes reside on nodes, which in our implementation map directly to the +`Control.Distributed.Processes.Node` module. Given a configured +`Network.Transport` backend, starting a new node is fairly simple: + +{% highlight haskell %} +newLocalNode :: Transport -> IO LocalNode +{% endhighlight %} + +Once this function returns, the node will be *up and running* and able to +interact with other nodes and host processes. It is possible to start more +than one node in the same running program, though if you do this they will +continue to send messages to one another using the supplied `Network.Transport` +backend. + +Given a new node, there are two primitives for starting a new process. + +{% highlight haskell %} +forkProcess :: LocalNode -> Process () -> IO ProcessId +runProcess :: LocalNode -> Process () -> IO () +{% endhighlight %} + +The `runProcess` function blocks until the forked process has completed. + +### Rethinking the Task Layer + +[Towards Haskell in the Cloud][12] describes a multi-layered architecture, in +which manipulation of concurrent processes and message passing between them +is managed in the *process layer*, whilst a higher level API described as the +*task layer* provides additional features such as + +* automatic recovery from failures +* data centric processing model +* a promise (or *future*) abstraction, representing the result of a calculation that may or may not have yet completed + +The [distributed-process-platform][18] library implements parts of the +*task layer*, but takes a very different approach to that described +in the original paper and implemented by the [remote][14] package. In particular, +we diverge from the original design and defer to many of the principles +defined by Erlang's [Open Telecom Platform][13], taking in some well established +Haskell concurrency design patterns alongside. + +In fact, [distributed-process-platform][18] does not really consider the +*task layer* in great detail. We provide an API comparable to remote's +`Promise` in [Control.Distributed.Process.Platform.Async][17]. This API however, +is derived from Simon Marlow's [Control.Concurrent.Async][19] package, and does +not limit queries on `Async` handles in the same way as a `Promise` would. +Instead our [API][17] handles both blocking and non-blocking queries, polling +and working with lists of `Async` handles. We also eschew throwing exceptions +to indicate asynchronous task failures, instead handling *task* and +connectivity failures using monitors. Users of the API need only concern +themselves with the `AsyncResult`, which encodes the status and (possibly) +outcome of the computation simply. + +------ + +{% highlight haskell %} +demoAsync :: Process () +demoAsync = do + -- spawning a new task is fairly easy - this one is linked + -- so if the caller dies, the task is killed too + hAsync :: Async String + hAsync <- asyncLinked $ (expect >>= return) :: Process String + + -- there is a rich API of functions to query an async handle + AsyncPending <- poll hAsync -- not finished yet + + -- we can cancel the task if we want to + -- cancel hAsync + + -- or cancel it and wait until it has exited + -- cancelWait hAsync + + -- we can wait on the task and timeout if it's still busy + Nothing <- waitTimeout (within 3 Seconds) hAsync + + -- or finally, we can block until the task is finished! + asyncResult <- wait hAsync + case asyncResult of + (AsyncDone res) -> say (show res) -- a finished task/result + AsyncCancelled -> say "it was cancelled!?" + AsyncFailed (DiedException r) -> say $ "it failed: " ++ (show r) +{% endhighlight %} + +------ + +Unlike remote's task layer, we do not exclude IO, allowing tasks to run in +the `Process` monad and execute arbitrary code. Providing a monadic wrapper +around `Async` that disallows side effects is relatively simple, and we +do not consider the presence of side effects a barrier to fault tolerance +and automated process restarts. A thin wrapper API that prevents side effects +in async tasks will be provided in a future release. + +work is also underway to provide abstractions for managing asynchronous tasks +at a higher level, focussing on workload distribution and load regulation. + +#### Fault Tolerance + +The [remote][14] task layer implementation imposes a *master-slave* roles on + +* handles fault tolerance by drawing on the [OTP][13] concept of [supervision trees][15] + + +* does not dictate a data centric processing model, though this is supported +* treats promises/futures as a low level, enabling concept +* its APIs coordination patterns +* has more of a focus on rate limiting and overload protection + +When it comes to failure recovery, we defer to Erlang's approach for handling +process failures in a generic manner, by drawing on the [OTP][13] concept of +[supervision trees][15]. Erlang's [supervisor module][16] implements a process +which supervises other processes called child processes. The supervisor process +is responsible for starting, stopping, monitoring and even restarting its +child processes. A supervisors *children* can be either worker processes or +supervisors, which allows us to build hierarchical process structures (called +supervision trees in Erlang parlance). + +### Haskell's OTP + +Erlang's is a set of libraries and *applications* +designed to help Erlang programmers build reliable systems. These libraries +build on the base primitives for distribution and concurrency, implementing +common patterns and providing skeleton implementations of standard architectural +patterns which are known to work well in practice. + +The [distributed-process-platform][3] package is designed to meet similar goals, +building on the capabilities of [distributed-process][2]. + +[1]: http://www.haskell.org/haskellwiki/Cloud_Haskell +[2]: https://github.com/haskell-distributed/distributed-process +[3]: https://github.com/haskell-distributed/distributed-process-platform +[4]: http://hackage.haskell.org/package/distributed-static +[5]: http://hackage.haskell.org/package/rank1dynamic +[6]: http://hackage.haskell.org/packages/network-transport +[7]: http://hackage.haskell.org/packages/network-transport-tcp +[8]: https://github.com/haskell-distributed/distributed-process/network-transport-inmemory +[9]: https://github.com/haskell-distributed/distributed-process/network-transport-composed +[10]: http://hackage.haskell.org/package/distributed-process-simplelocalnet +[11]: http://hackage.haskell.org/package/distributed-process-azure +[12]: http://research.microsoft.com/en-us/um/people/simonpj/papers/parallel/remote.pdf +[13]: http://en.wikipedia.org/wiki/Open_Telecom_Platform +[14]: http://hackage.haskell.org/packages/remote +[15]: http://www.erlang.org/doc/design_principles/sup_princ.html +[16]: http://www.erlang.org/doc/man/supervisor.html +[17]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html +[18]: https://github.com/haskell-distributed/distributed-process-platform +[19]: http://hackage.haskell.org/package/async +[20]: /wiki/networktransport.html diff --git a/ico/favicon.ico b/ico/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..047f574bb5bf6f10c2372409b50f0f4935fc5398 GIT binary patch literal 1406 zcmZQzU<5(|0R}M0U}azs1F|%L7$l?s#Ec9aKoZP=&`9hpTMvQ7{{R2aprn11q0-%u zVO2{K0~7yIhLznl42M=PWME(}U^sg0Dnn)68wLiJ8U_ZYbOs^WdkpNtHy9XscQfP^ zy=0i2?!|EU@NtHQKx+mD&WQ{yUGEqYQlB$ac^We?u(vZ@yZ(}aS>OW0+~N?1km%+?+?Ld4Oh#vy+2Yv>IAIuC4e;85mKOp}fQ2akT5Chc#0PrJ5 AN&o-= literal 0 HcmV?d00001 diff --git a/img/NetworkTCP.png b/img/NetworkTCP.png new file mode 100644 index 0000000000000000000000000000000000000000..48926b47414c258d8601a485bf1b4b42220bafd3 GIT binary patch literal 109271 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 E0SsVXegFUf literal 0 HcmV?d00001 diff --git a/img/back1.jpg b/img/back1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ba88d759329567b448014dd498c9fab149d28971 GIT binary patch literal 636713 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¨L0?3zmZX_k59p8Q}5^9NWyC*&x>&MG|# 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

d!P zZDsSW%gQQdlD%`2Kz#0!s=B8_`nOlcC9OM@qU#vu!Zu$aClMkocEftb<_)~JReuOh z6^_`(Uo7t5#_aaWl2&h!UfcK7{Yq?0a_XMZ?V8Mj!{?sgP?t$r_-^~BA3iBD#;$NA z)9!7S_gT27QP~*e{bF0^vew_v&;CB+>0FUMZnk9jm#^cGjGjAk>B(;$@wYy1NSaOZ zlGBoI^mh1h^SvD*d(-<~H!buX_t)x#@$T96=(S$TqjkxvP4e2W--TD`UZ^>R)PgjH5VK zBi0h$NmGB!aaXUH4hpRi*wxZ6#C}e&?_}-hm?!>!vaYGQ)w`)Vp_i3}aigVrV6b?1 zX49<$?zW~yPu8SD5on+;4t;t$gziAXMtHA?ND#m#$LuaoKgEgZL@){Fla9qnZk}i8 z>y*IuVkepv2x9{^6Yxc1Pq2lDr+!bMA>EErbpYwQs|@KqfxS>zI2QzxByOOVy8%+S zWuW-1+vplz&gYhS`3i=Hu5GkxgABb?7_QI;6(rI#qKW0zfeEKpvax~CJt1hlaaR(3 z9+4c46Go80W5Of~?4&keBy!f*(kn5mHl8tT_koE1dn}dm(ejV zxc&6o$Pg)8t%;^7S3h}iPpe2@@ln-u{-(yx{Gb#b@h5}w*3}i>B~8W5mBeR_M~CQ} zYjDCP7)pN7K9D_tMG0k&1{_lKL~7AxYFPuoE%VN6^7^ z2z(H-GA?#thFY<4wHFks{9o`RYhmK}BFv2F0lC{O@FFfylF=Xqc9fIr3-(c*?_bCx zoNpZ#SyXu!nTApXjG}|&9z_vZglrF9O0v!a<`io zZgC#<(3o*jOZv=VY`H9ApKJ;CT z^Ij)miO%`mC6H|(g_IyDsGr@q0CQZx9SL*|C z?`_lvMZqW|B}`=2&ajXs0mF2hwOTgHIY0}mN|znAR2Q~3)Y{k$@y5<8MTVM?a5S|& zoPDIxFIdiUyJs0-EpTICmc75!^PN1(fGZxpEFJnPec$nPvvw=9Z)IglA6&Z|Ixr|$ zH+B5_UHQG|lji>ZW?@tAmB;Eut!7tcmYtA#|NWzZx7odI`y1{nuU)c%0ROg@?k-~?HZ@D>Y%m2(h@y3d_ihU)nSp0ZL+TA zb2Vd5^Y8txVaw85S*h6x{T|bTTAgEI12r`w66NMicmLMdx5j$PK6jvXAn;|Vqe8b{ zkgVK=hC`(s+ZR@ONz>old!YEJFfx1MV8N@#Es5VL)isSP9RL2?s9*c5{kM7+eO7pM zGy496-TBE5qZ2ut4QrDR>i-=+(lhvTZS7xW5 zCzYSLnEr$EFM6Auv$UVE4Tusvv0D?H7xfm6nw@gVC;H-^`%n!8uz-W$0`#d5r3r|-=0u4xowlr zu>1I$&TrDb&bwiGf`6vf@v^NCEFDzmV|Tk3!bOJPP7R(diTMUOLcF2FtfKX^rj!VOk6yL!P}s0{#K! zluVIc+brO1*%_{I*~ie$cyz|D`P$_`xQ?-b>wpxrJFCw48OoXEEJg2|)~TW#)YkNJ zs41A3d#40RlF{;g)DZ*W212(cpo%vh!0=-*t(XW5#oP~r_45faaXGG%O>&dzZWGl5 zu~`X4DZG6`QsQxqowxIPMCbM6u3aWaDXim+1xN>lJ6X`-3QVrS4hk{ilPFKnOI5oN z?G?+bm(JF>oi*BFyv~rw>~KZCo>Pp9w|2;=wj{qMu@zr1~f#5ToC2=EL zh~NNYF@$*#50{WEWqWNCQh9qV(v?*vGTrIQs;GOqg5Q^!U>@!(6man870r8M^>T~s zAGQQ9m#vyDb`MPcd%3W{!7Zt4^;4(6^?7Tj=Odt)*FUx81StT!*j{$v^yo?E>l}8LX0k|bX{H9YSBUliX5Y;3iMhJ;S z$_3fjg*W2*PA~b#u(}ju3%3l=J`z(&xEQIUgb0N|TK&$-C{?^Xs_YVp+?9yg zXva|l7>xWcVh{eGC@q{D@QX$jMst^L+;JyS&l6=NY*&`JQ!K>DU4OBP5lIK0(7)nC zAS}2dkP5Fu7*<}cnuSK`JVvGe+l*yItu`k~02$(e@v^mX4yLCTHnkL zA12R)7L(_-Z(sJ(SjGQG)tA6So&WzMBgTz1n= zjaw%pXYMPN+*b;@tCgdc*exBq+IIKv?AeaKTJ)CJZ0QXnt)(n?1pP5{*pR2x*t0Wn9(0gb20D zJ|TF37com1@>StDBxF~Hk$%x*=&Ys8w+;DvzOQ_)r`00q1P!NaS^VjzyUzkYWSO7arF`aGl4O2L_~!kCzJ&7m z4jyJHAs?{N-MCr*CKi)4g?aMvn~6 zugo6`Ra6ODvSH5B(RL!Rve+9q?GASO1xX*;X}dx^o|i6f7S)&tg_Q_$IK^n>bB0DEpN}~eLmqa ztx~^sH+|NtS0uRhvEok>Qf(?zLW=V?-`_*G2-U^%5+yYK=QB~n&Ddch!>A=puLk{0 zLHgOvsKDUBlVcasZ{*`DuDr3j6|qBI@a*2F?^OM`o?f4o*rDFm6st(cDznTldsa#E zoN~`cjQFvN3ECc0Q+0o-_T;-6cCNf9jz5s9tc@KE(NYGocM5jI z<|l(J%@1;pCkH-qjLZ&nfWcWKdp6X-u5W6thSkNc7aG`N48gqxNh5dcpNzP=5w zF&WwjS`?7?lc9vEm3M65Htd56n!G4inh~(3{}Mc9Z~zAvgS>MUa;0l9%oLaO6+ZKAnjd4%H*mMOCgxMyM!$9F8A!J0XO(xToS2qeba8TWUj z(MPiu)->1vK{aOv6dMFs0{S=%YCF)QbOP~K8T*DAUs0aJUQ2SY9h}%9ZFjYGVuzHo zL2N;(8YN^;y1E(lI$r)SHnpYWhe_!NbuYv+xjxue^mNKkeR0?Ev7eNJluUN_9Um(c zxm0w%i=ZzhvfW;Y_2EV~2)6(d(^16nL55w6vF zauj!OfA%NjoXkvdjplqI^K!&_BK{FgSw;#sig=#buC%I(Z=z*XTH0Dq+2^8YNI0Zq zAmS4Rp48%;t^-&Jpd@+w`vy7~Ui2umm@+aT5jThC=C)J@m^nC^F#ns6CERT`1(jAH zsDbCOn+b%j%l|~3NKDF@b6vO}KM@2#!%qy9o8#Lx&Io+{F^J)#8RK}nfEj>Z2B*0# zOhGepX8iw~x=2b{3ywEv=s-FcxpffT!AnFZ3WnhUWJvX}WTI)_0^IM9qsDfzBU}m? z*Z?8OMGRdH$iWVpBKX{lE)f=R>@CweWZ^IXI)CVjfiD4_4ajIUh7ruzQyJ}%a8TiW zO@;td#y1_d>JJE$w~J8Awc(a-r;tGsX(IzO*u1i)GP0#3SF;*YreBO%}I`{$ojE71%+!0fW^&o0JGrX z!}85@u-&u3m5sN+jhsboXOv3sfCf_VbHP(NDbe{S!aE1S=6$QTR>DS}`?+EHV3LgV zUcoMrJpL~H@Q&gX(~?~VIXw@!ROtp(bwd7b#osJ)^4-#V*|oA+J{RbG{#|J}Sp8A* zPr`VK17iEVW|k_tmkdS@tL!*&ZRDuaPsH!9+aF#(K2$Hp{K&0g^iOkR!_B{6um8&7 zxP1Bd=U>kxS#_?TNu6?kqr_b(N-{V7Q)KLP-~0L}I>~FQ7B~1828eIP-Pidm6ZKCR zL^(Gr-;Q8Cl+RMV%nl)S)p42n-(_7@#33>PaqVe{>z2?LhB_B^xCdZGjK~;&X;a@t zI5}4X7g=Q&s8Z6*M#eO+X6a8rHnk+lLq_)=X_|Cb#{FJqPVDFuZl`olq5lMQUh@5B z&OUHen#1$n$g3ll`9@XEPA46*(6}tNq95R5-N3^$+x{e{n0{0J$eHh>n^`~W{?6W{ zo%rq;#OyTu=qY z)f|>%V$20@M(tbbe6w2BIbPEKHAdqt+#m?|$COr8m^B~c!4}=Pc`!+;XQJ<@s?NRk z$<#5wwOy}@fA)H>Ded-~%}o1ijuUI&%fHWUsv1ib+5CRRRQT-IXO`<57U@6hKgl2m z4?NPD`rII_@j_A~U+fochlckdok1I`uKnVRUmOY2nS_{$QT|)V6u3JfhTFrS9xPX_ zTm;VOZgT9JhlP5Kt4fm_W~{Mwi-t|Z%!KAtiYXA28PfP^d3-A4p*4UI zOh>{S4hGKDS7(^{fSIjmkj-G zn4^x3q2P++b|%4U0)AUddAAs3nWJ)hl&FR3xkSU_yOe?jtaS=7RiGkg`4CwJJ3}jY7I;9+ z5o2G6b<0V_RAq_Apu`a1F<~WxJ|}`dC!S$EfN?@}VkGx4P(8*SnGp;f1rgFPt&vs* zD6fuMs6Yl?VW<8rS1~b~H#v`s!@=ZzmOsf*O4E5{Ec1!0v$xt1r~WH6Zxu+|qN1f@ zm>NsXwBpN`98<@`m88wXZ9i(&P5eT>amH<4rGN5sQr16A1nD-pP2=86eL){q@20;E zTif)roZmV5+TnXcbA#b2vO~~p=mhCq8cSX)YEP`ew_>Wu9tEg$765M7(f0yLs!@0=|gh;4u1EIX2=0p-3^jC!7{%f9NgqgH}YaDIf zfLLrl0v^tov5XZH?Yd}vMf)VYo53vtivOP%@ZUEx)JDjlxBbyj5dur)_DNy;*%&q^ zB$h?TfbAel8Uo7YKox{ISfuPlcrg022VN2MATmP0&`E;kFX;!ICWA*t&xVEnE;eoxcHoB%uUkFtC10dN#S*KA_+KhM_ z2qtv!1Xo|MRAqE8O|hqRmcJz?+Y!)pAv$mDB#BI&0T3BWodN$8(8f6*%|Y7CE%6F) zPb?6F*>I(!xeYx$=~9ZF24XO|;rE8uMNl~bs-t#bbpyDZtUsuL9AwRl90v-c7^G|k zs|q@lplFtgH(6jFOqk&l7JxaON5Qd~<{>i%#YwSq;9vr>VnCe_u@lhc9e8)e9Xpiw z0)OhthPQ3cqchfK8>s?>TC}2t+S|MwFzi7RL`6;Tw~+a1OLz|_OB4Leos8wUOukfu zY##FNx}uD*TQ=-B6)>ayQj2rW-eZc-*o@4*kHX@7?i+bzn%urRO+}l!e{FQYt?67Ydj9oM%I~7ifBHA{`36pBe{L8)f9IvqEp@wSxl320zCI@Z z@eh;lDw*?WOZM$ibDP9s9+P_EC_h#6dwuUW4ONZqJw4W|*&_!cPmCLlH=&zl?_v^H zvi+tkV*LXsxDZV%AR#ivd7gkFN?#P6YFm;L+%i8vedcZhxQ$HVT+*dy#c(eMiQy0; zFYY`HSWk5?k(%>_D0oH;4`X}f4*7|-L_X0wsI#&%rAm8w>5v(g^7K)vjbS^Vsp75b zGi4JiEeH8$WZFurdxC{`CB*DJc0o_R zhdb~0&!6^P|8}V-=U0M0b#t^!RZ!C7moH3|YrIjfz8|)JQ9j9_}wA zdo~S5zw?@)XE@!43ttDGNrirj$om{C^7FB+1Dl@ybmJV- zZztM>cV#>^Dr;%stGzr zslI=3a6xTC54a@6_+y24Na1NrNKr6I0$snQArGRXg4(28 z9I*4Xv2b&EpWsqsf(vRxn{&ow2=Um0i(jh(w}Y^OD~#vrmAK^eCf8g(QP)7{8cr8y z7bp=848u21hBzw(hBh8*ox=l#x-Elswlo^^H|+$zRZCcZJj8|1ep=XbOjE%=8|<1C zaII%&N@D^cs|6X)(Nc^Y>zNR-V!z}Y+Kcfqgj7m!X29D#;Vu5`^5#)QQ1DdA+ZVAz zZ-Gd%T^Wnez;$^omV_iRE8tCq3>j|s6bi^ zo)`+Sqn9s4tuot}!~Fz-&d4aAjHEWup;%R`q6l|ZKsoQGH<-~e3D^qSu%xg%<%;9x6Fi&A^~8rE z6SnlLSNC)APY~`dLVJvf;hm^G4=hwlBm?r|5^}9~93JbamL6mxjC35bIcQk5p||r< z)X~i@|EW{2HkQb*+@G`mdeYoxE_>8(SJlPaTTYk^V;b|N_OxfWMdPlenwYi)oigw} za@-axX|^lJfnd|rTXz2KwH0r(i(2Vg54a))|7woStXc5UD$EyA%UwG!S6{%TQYg3> zsZ^}fSatP{@gwi0?D7{27mscgu1L#m_7$dl-AIy`x1ncfuJr8Z-nwt1v8h~k5t98{ zIp@CG-%s0l{k6+SDIdY}MOq3{Ifq2FUJNa+Rgf*#RRnWrLABL7vdR{qk6w2*L0EG}(CE(m;$QYr=N|B+t3Vwwk&u=ya zmQ&PrC5A>3aa6M0|z`2Npab`i$*Kii9td}oD z%&UjY}q3jl~7Xk0!Tjz(#59eT5-XrbmQe1kG z70m+}-~e~fMl~i_h0MFVD$j#DYA~`l4|ZxkmJcLd5_6=W*ibIaYtGKzJkHL({OArY z`RJ2uc+f(r4Hcq#T{Oa~ZE&TXo*8?&0B<)>GzGDg6y`-2ydMkph8@s)H6p>g9QQ9* z+^8kCtb5lhpjIm+%X`Q3l{&=;!uTbBaI9R(JwqQP*Yqu3AGu<>sup#la`|WFBiEvE zeV*JNN`|cb%$uoP8t0&3RN)aBYqGRBmX|e(EZ?HU1upILEQLfA8eC zz?ov)llMg$4=_i9@AY<3vt7(B8j4?doo#c!ssH7v%*7zCE+yq*8!w5(O*K%wC(H^N zz@d&MiGp<*8WK&Z5x8BKQd1*j5J_;0?&VGc&bZScN)CePYc%;{7q zuAeU;VB_kaE>X0HTQ;_$|70GJzZ?7+4nkq#3Hx!)ydvw7dth@5U^Ch2m1Gf+eSA#1 z&AqPr_?!3vgTki%65h98ZyxyD(bX@gbV*^NdA8<7MD(=-V#7&;U!Sc^FPob`Sx$-b zdvW@3`SkDYPaS-Xc`xOz1P}Xj^Qr$e*ARV>?yD}e7^PFw&@7$p{`n92ab6Z?y@IOt zg15iC%Ns7xvd)L<%+BTKlnT`oxz;|t5uRrD2_@cAr9M1TElJ7FPkL&e^i;bqn%90L z!jdgvM?a~bq%g7aVTHi^o&Pu>-JXV@@pC2 zS;F+;0}V~bOMO4R{w+zTw^U8>qvl)zAmnTpmR#E zBbs(5G!0}WfEvmd_GIvZ=)VjEF>j7E0M#&jWr?B9id{9D3^W>A5Er1Jn1N8U6M5tz zqO1~9z<~@#5q5PDTUnJQ)ExjJp0z|{X1g-T-W2yh3pIEnLHo3B3&R=gB+&^jnV`i5 z{1G~rG;0+o8K|$grk68B#U({Fr7?u)$P4!$*h;40<_5;f*ymFk)t+G3@|S{U9nbYF z0&HI~&YD(kfJ8%D7}~PI#~lsAU{r!z#g7nJ*nPmIj|vYVR11JoePlv`T@DTEv|8~< z*K9t!@k7gt3NcwK5+heqs3gOF?yx}Xf+NI>YGd-r51Uf8_yt_a`JLY1PUpT3V{&a4 z7%8?aJ1Oz?6!Gtq7U$RSB-5`?ALTZrwI~s)HwtxBUOuYWJO%cFeSEH^Z1Xl6ANobS zPFpHJ(`vgJxB6Hq3a@Qw8>>usoiDfo2h%$`Hy+=OA}eq>|0S5NQ`5h1XW422OCQFN z&1+b)m`lD!*gO7&k*!6Wgj%uyUzwV|q1b}8!rRN<}O!&DurpY$l?`mE^?K0G=#S(Rp~C-{t#zW33%&mxym zq;FX4#%;B>CA#4NpA{ih+t~&i=;5k1G=+O&+SCy;9!PbiYS0apc?a*(GH4xP^#ke< zpyA;O-(WD*u;8j>raJ5N`BX22_r^Hh%O)Ax9}=g@!OWz>>lCH-&|lgbovEg$IZj zp){pGP77#FhR+5TCIGSlsu_o(Mn<-q3_$x$nGhw=1g{9TW+p^d!>eoc20F!WY-pL* zW(ODq0u>l~=_*ztgO>+5AIM9{mjLh^APRV`!R$!;2E&~x@#D52p$oe2=*Q~0CnN^j z-Ay1-G`v@B&BGArW4hcrfqMb+nARwH8B?;5tu+ze)%Z%q*&kPiz9>+&f$$uncp9fT z%^}Wq!NG=wT2sTbJofwuGRrd zJj*vqIZcr+WO0ye{rXsQnJjt7X5AHov}C3> zZ4{FuG<^o;->v5MJlIh5@hE!<#?ztr8)XLf?tGixG=6ri^p&x{T%s-b{)`v4FL~S7Xfx~@&KwL zT@R#w4v- z{)vrN2nW_!W`vL(UC*qyyhBdD$ExZ}r|>bWQUAg~{7Jr9e%8Sg#r8L)TRQ6wdp-&B z@_l(xUiM6AoV2!^%o!HWh5Mt|Bx$79{o1u5F- zGoDj;jn55#tp~pSz4m!gTHNT`M;oEmYqHt5p0U;#4h`1~@)H$ziu9i5;_ps4;aS5g znC7>Nft-SnIg)mO+RD-deYf+U9=p%fQM2E3?{lGZ@bO)$KgRwkA~O7v`BX>C*4e}F z<-U2(Ot@=|$j^=Kv-D+=47}+OwR2O*zuEM_m9H~g&(|&(xAi@(yn0~%vdQ0v z?XD}O?^VIlb((JhC7u>6pGhcPn3l~nD9>1Q@l6BLm(g=wEfNTZsaGK%iH=;fSz%k; zItxYX@%lrD4>{|qZ5d#=^T0|da#1@4swwe5A2y|+82 zB#9D-@8d_M9oFDn0Y#J}$^<|%%E| zSikKnQxqx|h-iv8SAvEs%DvIW?ujqVcs$H)@Xo-TtzlKOWObff?3`fDg^Y@4`{=$M{2zWGYF~mXM_xgkh zI$Sylv^ETr3%U)dCU=&56sF!yu|G7{-8?oOa7$eM>&Rb6;s@V0y{eZS{QNBIvFu6D z`=O$2uNuz9nTrH#XDeEm{<^DDr*bPQ*uA2h_x>>HK<40+ySp#YQ&)7EDkfA8(B`~4 zzlbUg$LRhMF+!!B@kl6iUajjq6ntHfZ^c+tMnreUJ-~%*7kIFSU$K{Pq|)a=+D0N< zLA}w>N1~<;zTon>`&~zS57?CV{E_;}c<4q)u*CfyxiD)yVceTdrr-F<-vMK3d@4&n zYi;=O@O`;Nt6JQ3;6aO=#TiF~NzXy5S|)|dbzl%XHX#(qXG6@Q+tUYA^sI?7c|`VJ zu_{TteQlaGHeZlM(8YFWz=zW{Oh-O;yfxA`p|QKG7n-D|bmQCeS@?t`3F28JFgL|M1k-bm4xC zdB)ECFX#dv2Ui6essO*xz|HmJxwnJGVN#>Dd;25cK=Hp#8{-Un`oH`4LG;NGQV$Ys z7}?x;4un}R{~22&FZ&wyAkoYe3IFhplS-_*mas|7Z@dL@P%QtuKo zrVJH+FaTKO9|L5*3J->$DCNY?UNI%Qd|dcIj;^)@a=>n1=aAm;T5eMLc8A>ySZMjR z(boi&Qv{h!dM2ZK+57;7^8m%h!kuW@BT)?BAGdh1X0}s!g$XHJOee*}XRnPUd72BP z*V&P!-MEv=P-I`v32t3$H!?>sh)P?a{Q&zK{5F;4>2jdI25s03iX+FJR0+V|qD1@P zbh|BL?2O24?s73%0kCnf=_>7z+UB|;(K9Ho9@;wg#$-|UP6$z6slWj5$|;A~bxfHJ z3ozYCgC>QsB0U$pze9$_!A8j9P~JH8#lUzo8zfM~WH=aPb3d1D)H)y|&6gx4az)ZJ zATR9WASt;#m*t&T*4aP>0#Mo z(CPAu|8lwTZ=nRaCXj|h$-Spy0K z(jfzL!Zo}nEJW%_CAa5ElG;AJqu|eNWaZCVjt^{C(bFm8hzm)1W+UD;oDeLxlA$9_ z_2T*^+&PtkbvUz2%qUzlOiQ!uxbXMx&91enApOY~`yU(>3TXUtIJ0^|?Rz^Ax71kE zz3J>yam7EKUR(WqSccyF$l%2-6;;U;Uf(kpBrnuW>8iNo{jQ}K_41GLe2Ia*F4;o3 zJ6d}M7DJM_WqXprLmM3o9$Nd?Gt{U3WfmK1?}}rMO0s9X24_9T4;u{M%sqD~m5^z% z%Oh|0Y-a!9XqqI?@q6FXx5U-Qbj9x!gbUtz@wK>VzB`Iex*yQ;$q0h`lba6hbIa?gQ_9Wxngej?-VL8 z%AnOpm`%csqw?xvl0x_OC0Zrs)5$9Z#f>?e*cU<RDk7m{+17QD7p z?ij0&{QAsneZy|-lB(f7KBh-ppN!H1Wg;B|4qa@x_edqk*q7Sp_)#axx^cmB_E_Az z;@k8$@}892L&Kvh3E~2tizf1Pc@TRz2j$sVLi9p47)!a#!MekJyGjGXtKb(FxgjUJ zYHBzEeXdw+Zf>2&SEk>tSZGu@z>jNs65w2;V**JLpgm(%cg0SEItx1qP_!NDFt#*I z84uG)1+1OZABKO3Gk(Fqu9p*us#(!kLP^n8O4frWdN9qL!Ge*7h8;R%JX7&98O7&80KEzdlYr%@ zN>+T9=w%)z1)LqeI32Rci54O$P3gDc?j_GSrbI&O0yPi_Z}>&I9mEfyBnY4YZpT!K z^9-s$lp1X?BSKHD1uL5EWeK$Sbf$#?cr5~`fLlG`4YuzTU_%^ZKeuMkS zhA~-+nw8ni71qYRY>qpQ(F+V98MD!_?}qKSXJo3$t>L>ncWRUv{vxDAlWOxY)c(`w zq=ME!M7YrCB=6d{J%f*F4JT<5N0WNeUafxKv2Qc#urc+xmEZn@Q?D(*SH=$7-8j^e zEO?!=Q${oVlI6cBjY=Z9tD-g0m)k%`IenWi(x`{umFtCVx`VRyLWB7&xuee}GeMzg*P&;oCha z4uPNKHpbRejxc{8*(`6#T(+CZ&TCJ*=yBh;@Ja5&O?pFFp6f=@-@YNa?PKp2x%7R@ z^M0M%qucJm-Fc7XL4P8Rca8DEW-A(I$dOx}VoMB_>m7{ut$K-;6B?9{e?BHq zkuNBPyWR=QAG>)uEhD2btt8PL8LA1~+it7od>+;oh=_s#LNTk>vdx;(1u->4a|OsW z*nqKMz=GrqD3XL>1$!va&RA#5Ffr9t5cI+q1yDWx5p zazXiGjWYr^7AOpGmIX!7%CLxLl?DOPJ&W2y&{-k@3{al2@GUYL2N9i#*aHPFPDdK| zig!kYZhZto@6n5gz8JE2h+u$y;ULiq+C*jnz-II4;3{v2m6&9P(wU&=9wJg$_|{Wg zAS9W^1x2~(DZpU|%^xrR`)HY1qgc-f)HB-|@d`#)-92Ct4I`Z1iDQC^Zp9_vX}d#8 z5EgIKJkHew8U&zP(g`Qo#51tblvRM$yG@zTDo@DyQAsM^))%fw=J|b0*<;f3>}HyIop+Q+ngZvk%M0gBv*wY|KdpMfMFlIU?6<>84bL zh^I0EK@a3@^5*WU3wl_J#MTO?8SM1-b{U!s=}d%}{qFy&cR&g zZ062{BeU0XZ3MKQ_ep#U9=MsEQs@`&WqukSJ*OG?J@`MAs2`14_K zrxoveU#H%zdEEHz{kNxRT;9uue~gzM=G42lIPVr=6=vx0l0Hi}NI zYogb}5+F*pFX*W>7OM<3sJ%TarJle8i4<5N$VOL7L}-*W=!3eFpraukZR{ZWF{tG$ z@RyS>gp)(;6j=pQb0K^IfJ@MD^PqbcXn3hEbQ8nv5Qzw!tu6Qqvrz*>2-a= zmF&%@qclK^OBu&g+5a{nh85yMAbeC=l^k}X!EzsrDi*m zyTTW|ZH6|KSEDcBudV1d%0D&&FublmX7&8#_)KOirjaOEvXieH1Y}}mhq$T-c9&k( zRLB}vp2P1>d_JktwB3$V+1XicS?(*NuRC~b;-ty^la4oc&ZqQne^M1Gw!e#er1*Vs zv$pl>@uW+egS{^(aSPWpebWPp<<3?`WA8(4I@TZBr6}$1Xj!!M8#q6rr68Sk$a6=& zptw_*Mf5$=GW6w$<`M${rxVZ0ca@KE0!wNw{a6Dt)iox7lbvo@TrG*6%NMY!O+!TP zEDJ5)l*1*nL_LNZ4dWBAO9QYBhqDZGCjijJ&DO<`un+Xg7?icUOO*y8Pl9U^$=kOK`|QO+>=`H&g{GAkJ% z`hU}#@iCF8pzKGPH6q75tsvMVkp?Wc9AHA=hY|}GtlV1VacZP1{{ZGiU;d#ySeuhPfn^Q{IXf9NYMs1bXiEmr+wBTO5qGFHg9i;= zw#Q`pu&m=pFx~v{ zs)3r1ER8@>R-Nzz9h_l@LgK6$s9-XYZufwoaa1yC>vM+Y{LU zjCWGv++pJ8M>7!cTLRQtIp%m(W-AJ$H#+^BO0JxUmTo}^zJfO%%EP_rtf3RiY%mH3 z$@~nhIf&V4N`lJP4`gyyQ>Y+7KdX$RmuH&YdBYW3UnJfx&7Z`X$&#A397kihdSxYM zxX(u5s>|Mkf|h|d;z6l4&60Zlhc|le_2etOJpvO`+(&~cM8D%0hLn%T7)-?j-!k{CEgJ~425+wB4 ze~1PV&8rYz;J<5Nw|Mhbd8pBWe5mAEb_QN;C01s z__+t9@dgt#Im11Z6kM5WF{-(p_0|Ey$rOG;|6G4nK6* z;_wvLOOKCNo9;ZkUWNUx{%1pJcOF@*NFpGxx_&C3I)AmdM8}rzxVy57ko~EWD7jz% zO0Y@!TZ{ZfCC)wG>+SEsgv-Z=!-o4LdNSJtD_=yUNN4Z_*Ita<;`)b)wZX7AqT0w; zUG(PX(x>Q&iE0=pAlp` zZ~0`~&8ZKT6rL*3X>U+dJA=1rZ1s+)>5@vUj2~2R5TjJDP%4jkAA_WA=pG09fs(kI zv1nJ~!DmrDtY=;sU$M^2?Vaq;EVs5CN%Jwe-gs`FM=MX5!}Fx)xb@#VV|opC^=sD* zPq~?ByRnrlWhgy+49Wh5QQjEs%!$LQ4Nb1Pd&e7vGF5`3FK*l_6i#x!s9=5c>e-po zi-t36b&=0ouB=5A32e}Yl$hrZ1@ZNIUro%%Mpfmlayjf3>8yv-4(uQR+6GSF0UE?#z^s*b*2MW3rZT*jREU9RQxGu)H*z3Pjof6Y zVUz2Lz&$C|2PArfK&~yuuo|31T&HK|nlXst~xX;E7^);RXQvaFA?hPetYs z)OvviZ2@H|g8J?=18)X0}*;aQsW|s z7L+_Nl_C!gKP=|P{P=ozWd+I%+&Dp?t1HfEsrNDi&37WLBTE=K>ljy>#N@c#5-MEp z&SI{xF*d}6xvQB`A2ualj#Eejrn)?bIww4$%bDBFp;&pMdiVM1obSmB^Rny~mm2)( z>z@wfOn-fT^~o2dte*}PSOQ7Cw=2KBKUOnwG}c(AdqD!i+Af#-RC5FhkmE}+b#Xr74GA%_GWf;R~(T6LT>N1d{*THdC zIXY)G+raTU^pq8Vzbzrc1RZAK#XuBsyNCUiPJR4@xG940pLkhz`&-gk8YfB8YOhEb zwe{2ko{4Did2m2<347@RW!47R@O5jJbxd|G(F>{;>^Dsj+U?R!g-&h|qXPvCNyxBC zBlS6yc+3cZ?mLUC9Holj?80*n${ZmnRztGAd~C&= zfE00(#YZC<-W5I3eU2inGP}KRKkX~%Jnvl>;b?ZrUm65xdxOggiS-57M+WxZ9rrGK zzMuB!^d*gzJv7aNYq=MDHn{dl1&{@H_eO#I%*M2LO4Ocdwb_q0amrU}Jb0&G>(lCD zX&vmK8eHbG>quq~hS+d~bTEV+Zm}7y9?YEex<7&tq!O>P@J)u*<2v7@w7Aw1kG-*L zlWKI%Q&7A0i8*%miugXiC5tY0AHO$rcfy`@RhQVT7Wou=<&k?OI6l(zr&K{Th+6Zo zFAU8WIBlrUV^L@Ktr{4oez**DRdJvG zVS4(vsrXFDZ*K};X;j=O6xn!7-MTnFXt`5k=P66$l+V8r|F%8cE!WNMzmX@{wA?yN znm#yr!|3}Js^*|hxagp;PuWr5Lk}IO{d?A)DO`45{dTB)cVg(v^ak@nSMw3SKRs<` z4r{#b6^HeL&7ZWU{c0cit?L9PjcfAsG<)k?^KU-aj?_No5v(q%cJKyT6MCaGa)C|W zow;b=?%w!-SvFWs+SSN7IWU+UnYE6GyE;#2^={W9Z_}nD`KS5766G@J+>`-JS+0!? z?nSSyq68m7uo+lq*4E?pkt>FuetvtChX2Oao_?;B=lm(J_r&p&&+614cbzIcmH){0 zZ9UUtAwJ`=8!=!jP>L!W`r}WRiJ|Nhtxrcy+JmOY&PKg@i9t*79$11MhhwJA06T0F zxR)7Im7tpH~Jah}ek4rEScNObS46dr(b5ZuBZ+yZu1WvIG0 z0QV298M<f%~`lq5ZU} zZ(-%1*ZzzcnK-oEs^%UP_3$62W1H7_Ws4kSZx~arS$?^+e^{JgIO4Q?=HtS7Tu!zK zlWp*IbIE+J@M5_He!&wOFENUA*73srllfv+`Z4+>`>tGtP=bSN3xP7DrMZ4>U~VPmmx_I@iUI<3;*mB}P#Krbra-1Z8!QyU1N>Ow0tL6-{0uvKcc1m%aQZ z!`RFq%^$^SAx$;V$*7hM^iyc(g=>THFmxpX=*!6%!2gb1V0sz8F~mOqSYwe5fI%1k zS1bB|7H58Kr0QnC*Z+P*!vC4a7+5y9_iTovdOJ!J*7H)p!a)okow(@%qi{Pz1=3Ye zre%sKBSaavMI=f@GakMKw8o>$7|wobsOJq0ZUmKYw+3(%Z95?Vo8f{&M*)DW0f64F zT7z>&PG_%Ld!CIs7_ts#GrI^-p~<=(Ws(3}+m*AL4tXY!ObOx4#5%b($cJ$fvR?&^ z!LAC3gbb+eLF+AJGcQkpg}Vby-(E#j4vD3r>Dr-aLSnX6G*M-?pc93YHOf7K9yjP6RhYm9<;#JH z9bO8qRymrsK#4Z{K+?+&@5;4-eVhf1XKJln)+`li3b!P9DRJQ}Dr{seEB2W_vQ~(3 z|Fn$AfQ)icy<&t!kxL8t5oUHFTdFY&jo>Kyz*9AsuMPPJ(tf44o!iBG=d-iZN37E0 zpQJl}mGB+AQCG|K{c6~yveLJ!+Fk~WJy#!>{{CC;S#gLRx%E7*NXeJFv3cY9#D~K3 zxT=RdVNo9^ZVlrNf~8@TTWUE4zd!|PFP0NDdn1Pk@Us;FDIZWPb~Bn*lO!-R;#`%> zLEE~ZY+Xa=;n(!oZTCl{)w&vZPXQLBD%ePP=2CPW9Hg+-2l}O{)=|2h=HkNoIz0=* zEk^A2ek;)$OtnR7LpggAK(upJ$lOT`SJf;nM<#u6VyVy9rqrepKse-FV<~M*fyQv- zocx~BPo-xrB?cXn>Q>(nRTpsPVC&Zq*tBULTkDVv&iOvObuh(SGC{k1U$&>HUgf+WmqvrIbkL=*n?qmuo}c*ck&5!nl;g~Xq^yv-{XM^^f6VK##BI9lAXuTyw=Uvhdr&=>zpCIZwj-1>xm z1cm?u4ev#kDK7iWj1mR9TH;k!xZ%dOuz;!>l?K2I0Z~t#{zUBaTChqp%+1mYBJ6M+ zsB2UUaYiO+`|=e01XMF!!LEZSr4BaXI;aM4+Agl-cY$sI1bf42AABQ*>WRz>9+%>XKkjtZ-&3~A(a+oUl z-7g95Ie0qN^2^5lp%_^wzp!%e2!RiWC67P17USgVz2S03JRz%}rAPKI0cskk4rtrr zui)Z1%iL@KL12zh+t@diYr_I}HV90}EAj+-l9nd25w2pY7{gpncRBA+@w}{jL2LkqoW?(bOKTNo5lvK^;m@N` zGvuTwoXT_~ufaxz0g^us_PFgrR;)qOJ~7CRRaqy1B^$$a8#E7!OIva#T|IPvnt*eZ{tys`G>K&*5l-B4F$Eh&Pvbuku< z3@NQBS1h~}Xjnb91?A{Ucoc}89c(Z}ch3T7Fj05|+G?air_I>4d**tXp<+1C#x-{f zMPP$#>p_cMXCB_g0_H7<+(5z5Y_^cV0LySR%uv@1u1>ohGGKJ3WFbK{gZGVw>Qp++ zQPi&x3*4>pJ(U2?a;W)cP3Y#7Uhv>54;du6s+gQqZh)yBNhOx21ETGG79jR^FLR5S zcvjfwZ3|A3(bfwsnuqcTYHRK#;~!e787cKOLV;-~*xST}m-wx@@_3HvE3lDw%COID zx@%b(8)h?)^cCoa%{<_eKH7XaKX+$nEIDi3%!cIoTX-Ac*Pn7;dtTs}+GooYtGee@ zT$)S$i|(HE$CgLsJI2+8O`iC5xL=agJTf*ucJb$ya2=UHe>Pif@ucwZgdW;By!Y7` z%~lIisuQ(C%V6wB(cfOz?HjfCNxkQ(#yRd&TT^kR~j#b z)?~~8*8qnrIBUI+%~)HeI@gFv!x{pusH{>1)r5fwF$GSk*8 za{f^I6Eg0sg>y-9%Vu`&hRi7!2)i&_iKb8ChM7&|j#0|aQBuGyNq{RC2Ii1zS5?aVaxzR@H{%t+cX9^!tMKxiNgQ z#VL=1i{Cyel7$YaXAeReVCn}AmOp-tPmOLu(rM<&^qvO|RT2oINz8_T^G)^&1`Ot;bvWD`Ky!0vfd^fQKzF zd#={j{zz&1b8}1PQmB!QK{1Djes}uZ`4i$j`_7#U?#aAvri82LCSQ*Hqo^dwxPU(< zfJfx|RRwSI8-Kae_&c==p7x8dybUHw-v#*)+%OPuiuG*3h{NEpg5Q(q3draP5f6fW zQh-(ld@10kjgYnh=&i&nkcNQXDFNtFP5!1B*!w+S_WVj2fkryFD&tJ z;9`e(4m1=A6)+?nfIa0EfNy3k(RP3a>X}4|0pJA+mn-WRpkO)Up?#Ye zFVxrjgDT3#*f{uPzVeuSa?``ipg3-q)NFN9wf3>70cJ}lk%+Y~LSuVtx4Z_jPu5o$ znI4%fPCDlObmi=;`{PPg9jRNIyEuX-y8V2Xc$m|ln;f?aI$rg?Zdq2IW-ZWb-0bRY z86m1Ha%1{#l$8Rfq7C7J$oX?+)ElQ9JO7~OZ4SKi>Kb;hH!)-yB5w?)5dnJFQo(Hcb|)8*JIy@Fv9fp(K$abB?~(s&5|(Pa5<8 zO!{as?bG+ir%%Ge5{p_pMSmZ;;W4q2e)P!2t(CV8`G4qLm0bjXCVJ+3!HPQHy#nAuuV%*AtpE7ezDvr91c4!qBH zhQjW{KQJVXe{MT4#7^&oijg8K`^;1va0&WmLXvVNRgyb7`I_^0Fm^>d>QD)-4T=}w zrVs7bsEH3(1z09fP@rNSK-45Ko=}Rj0>&dyO>Ikz6i`^@50w#I6V*VbBFm6D8hHSw zB6&_75=3ohW&SrLS(>8Z!U1EjgXjGp-s1l=F@HFVAlMo}*7$>2$aM${Bv2FZO(uXU z>IY^Fppwqd5j&4@0eug&|Hr&xTaRHvn8JOU^c2{hmEmT*_s+6Fd??r zMu_qnOL@(kWFyZGQe$Rl@P;*+-5`80-UeLSkSJw61C4am6jO8beF7tEy>0ab3zZ=? z0_P8j8S)Mr-_WIzWdH_Udmb!y=`zJYc|zJH05kAaQb8UKGOTi!&YV>gn^~6tPY83e z6vR*N78)uhP^UTrBB*f(a3KLqo3R%I%&S}fKD=$KBJUovm@u#t+bUY-N4MuWA;=g4 zHNC+sEFRgL0j}X%h?ZtEOU;6e&0aWE9?IlIgV&YKg!TRoZW6pY zAY`e)5!1m#4MOPZ36RHO9hg(a(5H=d-GfoxZO4(Af2E>gxyOaoZZgbWBf91;JNFoS ziJ&rwpS|G!_gE=jQaf_fNpbR5&U`4de@FM$n|&i6JKOGa%x&rhx!b5iqe1G{I?q2$ z@{RphY#w$+CLDSD{r0B|E~dOb^X^7r4@>DShH%Txkvj6%W?w~p1x?+aDecYKZ>NW~ z&CNW`CjudfIO~Py8>NF;@+ou9F$EX@cy-x?KGtO9_f9BO3P-vl0I_yCeaGUpCCz4@ zm_g_9gqU52zVP5U+@(6~7>apd!r(ug;|QBZ2JbUPqj^@|YtmsBU2?38BbfL%lqpC8 zlL6+wf~VpU0|)`C^6b)cdq~)wB=M`W2{Pj6w2G6+Bb?@M+ePvku+Y-scYOctfa2Lk9*nM?qap_dd^Sj%0mcEMbCF=uqk z-7e1>sjn?ZKjaMvmP<-je~@pC!q ze^{8zA6BYwD)RVzd2lMN?(&7po>Bf|5v_)+&fgnapTAq=Kbf_^Lqz&hPUY#&EH-aQ z{+x3@$=6l0RC;Eg-g1+&=}XH0qv}iGq0ayR-N!hR+(&DK+{=(q?K&dIklc4nLnag| z9ox1M%GnsXI}jsh$W^ICM08O?7edF@uGH?%6BTYrJ_M*DZXDgQ8$nj0-#6q0YI(%7U)lH33mqxLZ%Y% z(~gwDt56{asTYWfgpQxwfb0`d46qLUFJ-OqwvZTsmCg}DK@P5SxE^2~X*p9BJ^(o| zPVtuIkpMo-aJ*)@n+-Rd@EobEQo*Qg4%QTMUJ1B<6o_fpZWNsz?v8BVQ^b6WOW%V* zQ>c-^qQMs|-2=xGKs!l#pbBb7C|D>G0UF(Hq;QH(TlaPWjw8A7pF++vxEccd?fo}qJabhu&SJeFTzw8(D-q=VN(K^RSyJXEG z9v;_NdT_uT*CC?HypoSLZlC@bqO_~3_KR7CbXa(Xp9UUYeP-C`Ki+(+`qv)?%MZ97@sK^b%JAen zQ`YmV=f3RG<_o@cW)tYr4I)EyC%*G9Oy}0^re<6DP;@ULNY>m@zXPK-Hg_2W{4oCp zn;7gbkL%8V*#D@}a&k1--Jai9_we2L zM?HJDg=~k{`J10zKW@IQmcr5h#o)=R0tGexH{Ygr^;VT0Pu8w4{@H-k)pV&O+d_??ippM%Ntt1qThhix1zYE|jqSoT zTFbDWg^%t_8v4v?%3GplC@Q1cc&4AdPahNuCBTKa*IBk(GhyDPdk1)VAL-jly4KX};qf$}_%d`(z-krK(DVG}H3c!VT3qrQlmDB|8DKVbnJnkfw9x?`Ns zpmr7E+hH7&1m`*u!q#Rd+8Xx-ap@Qjg{ztQ%^7G1g7jH@XVHjnet)i;mjLv(=AFiR zP~dV;(?O-0m+UAS(R&US;eG0N!%%BVFjBdy%LZQ2pt%# zsKr)H?HO>`AIvJsD5JBAlFR7k~iqm|CIaTgo<(1-v(a~2W zUF(GlkZt{H_qM$OjI-Up-EoiEDLIq#a}iBSdcdwC;|3kt%Rc&yv;ZAbk%-2K93(LZd!C1rP<~006Zb8(lb$;yfD~ zDXvY&Yu~ZOqGoG+r)6AvCia*HX~-4VTpE5C_ycsrLF_BWF&|hp*OcBH6ht1rRI0=` z+s~)#*-1YV*|j={G51KUEZ^o*&7FqTd9r?^Yd#Jz(^@z_ETt-5JZIy80j{NR*0Ft? zAY4;6I3NotTEp^u`Dg>ZM2BFS1uS;vj!ts*>%v`=25KZ0;=SZ6XH!Pq{FccE-gFKr z=y!P=fq?VyKr4;Hh@=pDi*{^%u8)-{l|C?nV2hXv*8Jgpi+}DI%jw$jR$Tc-qVL5c zNAF)<#{7A`-j>;|r_qe~kM~T(@doQx6|eWIopl(EoKg6^>yCap)O9NjU${6CKq4d@zJC10$i$6 zJMCrP<(H2ult8nu*$8$A7g5*+8Iau?#67`f*kKJ_oV(z8xkwL~*eAt2DoacPJOSpI zK2mvh6tU9x(ioCK+#n^5b~80j2WH#x7)X=tB~ysLCm zJviQ#dg;50#XoXic2LfrO{3pEony3N!%t3&6%G2AHZETJys$xa^WvZV{F+B~3pUY| z0t|hJ#457R-+r2;;;mVvY_Ti{!yk%b*4$DJjLNd8i>qtWUx>Gxz9!=OCW&=zwJ6Thy5#HUC#@6v-LCC^7%Lg$U>2e~ z`jE9azOCgJ{b*APro;|`Nr^ArZ8^eImgdJGVQ?)faks`5F;F{ z17V0%{NKC^Vnt1bbwqcUPGdLxq2#kQ8zJ6?LlvuK*i(qfHRE6H4hcur2RH&mtDxRp zT05eN?%!dNIB5w4H5u?KWv4NvksB@@%V7=s-6C_MbNLwPu_I)NwR z;n}Wu^EvPSc#2(QqWtI1d>4`%W^bTw3#ro+vV%Qg0OUdew=a`>Vsj+BWJUv8I`uz)b~wNNE+uY zuJl|-&T}jgQ#A~9hXlg_ir&M*Pvg0m4=HFHHMt12k`n~Fz4}|}dHOi@3U#FVWf>mb zUp0>HB~kteZ{b9ChBIN5rK5^LLpjxkM(=?%NRVQ;U>Z5D#KKUiLF128*1CXJZF#<) zVG*7x$Q+pK-f||yM=r&27tjuezwULb(68-e z6^WgP-ac3C+?Iqet2nW7{2@O}$J97L=G8XwN`u0i>m+U09yOA_N7)w>mGJsXjPRKQ zjL%Q*^jzJi9LiO*9lo=Yx~mzZY_veR`aB9&&q(l-N_dfm|LINUV>;qF?=+%0s7zs3h- z>e7T1K2)wv&$wcM_xBOfHNpzvR7Jhdr5d<;X?nRB8PQq>93(&bsl>{7Y;Q#7N3oTL zvSBa1q!BJ!5@_qERz&H5%99oK(nSS4sta6`nspKp_xo7<;T!I5pBV|#kl{S7jj^7; zwKBWia?rK1^V=7+bH^P9a;)u7FZ#J<$E`M9+#%%qSeP4a7X2+pd@KF&W-3QLANqhc zHd1DAj$PE2iij{`iUT9DL)3p)nGfq1MnoZ0?>dL%BW$KE7b4hL*5$|AvaJu4*96gp z^TJ4blU-Y$Ft}2*Anz>R>#$$5{g*ynpf?lFfb7U&`2c!12m|e++BE2GDUJQE+SV+ zwNVeFmfRIN^EiV|0uuh^D2B^6H43ZRffCsSErU&nfzZS-h)icppalNH$l{MgK43iv zt&Yoxi6`YLD|0{3_mbpuOv4|$Oj1`MT#t#NtR4naJCY|s)ktfpC}=Whv6UtZ%yYR| zA0qVunxh8XB`zGjPDaq@;FKQC4}ci=0U1<_K@fZ{Y%*k<$-s;1xp^r`qSd zBj_lf=n&Mi!krr+bF+=!Dbn6O#OLD>c&gQ;U@%4Iv21Wm9@jOznv98CvM)dVGfA=L z%dWwXTdgaoT7N~luNT>D*7)95dUNAX-~JDuPUvCTXW9s9T5J8I$?IBVdanFlRz|z3N|3jxK!?kePnF5!$aAI1D>)V!p~5EG zTjJyzF*)AY@r&hT2gy8;d(vz-QubT_BZrEG!E{qoe}&U=lc))8e$N=7r;Hk=OB803mi>cU2YRjMVGWAXswdtJy%fY}3tD+!VakR^DD;SyRcyc&4f3l7-t9f#foFAp-|k@=E1 zOqOybfFVnpKf=e88)RJPnMwhTNM0bXD13;}&;Y6wEX4nhQis?;|G!V@vg2uT39Cy+ zM);o)h?q(WWk+Cz0&n{t_$BNp={_X#A*>w`UlX=H3((uKr~|>j#5S-r4iiIZO6LI zWaVSUX#;e$H<7m)wBWl|1WZJ(q8DPJl+Z$URD0YLQBCiNgnODvz&USE9})B%>4N4} zSz|yyiQ8EDaJ6mkBW^!p;k@Db8U zZ}GHJAs>O+nj*mPkw~mE^AR2uOmw-OL^K4IZhc*fMZfMzWsMhnS+)hDI#hMp+3ww@ zDR;GpD%8Hl(`SNPMZZ^~&nSl_Dvdfx=@s;i&*=S~#noGWi$Uu#T&MfX%D?BNe9D*_ zpRCS3Vsr$RSo~GJcyIW6Kzi&%$M}ge34wpFyOlVW1DWUsg|C~HtcPRU1(>Atpe{o&kc}K0|57owYXvUv@7JAMlUVU`- zP4MX3w)NB=gNttK`<86bR5W+^-V>yh#GXDD`Lm^y@ym;Chvga;R?qLt>-@9U z^JL?}@3-&x<*eg_9hOI@zOj7m*fTw&nX$t$(Q;8Tw_{yEN@Qx+wk_>bNRVC#{2L8H zm@_>?f6|i4GO=u-Fd1WZJtj@mjahOJ?M6(zh#;qF)`ilWi^QV7ai3cx>Ihd3bV>;* zU{#E1kua#`<8~7M>{K&0HQnFQAw39!7s(M6v9D@teG`*Xdzm&JRq$}LJXoz z)7*~MDX#HN&;iF7X-FC-!|Iw0^>p7H2NG8-hmrs^z~UZY4Hb|G5UAa*;o&>$-DdPQ^n4E zUa~lqTJXj?I=G3C$uPY9V`!PpcWH~?&prLK{92^p15T|*_TcCO2qQDJg-?5uh@CkTyJ67yk7xbc74V*tIYHLoe5hyh9|4a z9oI#+#~%!_2$BqBay{%Xrm&?iDQd_I&}chvXp7r(x74>NwL? zqoP-HSkGjDGw3uIp&vuj^XkLDxTSnJ^vVPJqe;T_DMQL|l0BGKrYx^%358~jSdPen zbHWh`_=Z@k2JFXNcv>1_{VDG%B3!9efXh?ZcMX~#+ktG9_%#rC;M>AesCZ)D!;=pT zg4QJoi$~Z=fl*DQ+jR*CF?lW_BG2rJaf}RW|LSwdNQfvxa3xE^@x0BEZ9(H5|0cD_4^(31pR?rQ^|I4Z; zbDq)0h2$rq7)VEW8>Y9HrkAL<3+qUDGhK-96c;X4biw0p%^SJ;&rEa*fnLw`NbFIP zUlPYjq9!$e-BKJhbcN)f3CqsCrhb?vO^L>+rxDUuJXX(6W^$0AR>1-mqApXWZr=)u zq4J0M#{7la{s6B-2|9Y*o*B&&AF!?f%bsyXejdz$7zH`(C&_jJ#^VWs@UiQGk6UF0h=6wWU<|`wLQJ2SC_%9*zyvCx0VWm#Sz~8Vs=z=8 z4;|uC(NWpn10-Z>o>$bScLytKJ6G9YSX}xPNab$&C8hB!ro54sU$)5p&=%*%sQp`^ zoa8+u{c+aS7bnu-%uC_kSr8y(7ly2=9pKiFz`=gM8Zs;b8=(X5Sf0`M)-*PK2 zuv<85@lW)o9V+X5U;OxU>ECN3|G4Uy*?i>bwaAaF=gt4fxjEqU-IptU<@|3&|kIG;6y($^V9eCu`EPwZhR`Xz+%c1ul#3%pR(m%Izl_a+; zBWr5Sc;?OR{b5@s&eyV{(MY|Y)0(B`9FU861msZo)Mr=iMWJd}{YmqL8o4%hD-O=j zZKZF*i)@e}9K^M$YiViZIv}A8(y(0p_F~;Z<01JtyjcWfT8iOPMu;s16-+8awk~hy z%F3N{ww_3|4Yz8!rQi~?TYouI;b?(O{!7z23Zp7s@qA@u>X~`nY*-fu-xmeIM5j9WKkwKdh~l^(pk;!wv1?PiB9Nyq)sCl%Dh; zv5}L>clmhvnqkHV>r=tnhVi+3N5vAATIJ1B>uXL0-Y)K|N6)!Zz8M=9lO!oHJLBjy zr#v+zf_UE?fz)CFBrLEXR^`c0DGF*y3}7%BRMjM1ortq0h@hewV~2|hL1Ko-gMr$o1X<(sNV8ot!CRGqd?X?zSF;Vt zXCyBb0GJ{p0j!gNJPSuPkj)7CB7ci`1eKn&W~@5_xR_*famhloFxabYQPtaa;9X1s zyPR=F29Qs-qs^o`0Fx`y50Sn=W)a?HuGOnKkoZilXbGrtgZPp=C9hF4YK`^{*(>Y( z?)%R<{v(AF>(76e_4VT74?9NtyAsbgZkC>#x#H(>;Cr|4jear?YfiI>>Wf+z`Ku&r zUTFWF!bjGk1t*0o?nnB`TYYZ2y4&f#k4?p#SLxXkE3Df`Q*2_3MsCmd%4=rCKUtOd z#6@~Dy@=s0fdfJHkhW7>_muC?8BcF&s>Oa+Qowgg+_3st4-S+3z>iU#X!z9+A>VDBvdqTrY zic2%xFKo&FP&mISKjr+Z6DMLW8pc@vT6sv7yZuUxA8#sXKj1(@Ijls> z`qnu|1UKACw`=NtB3tQ@P$Oy~>uJC?-snG69|2R>CV;+y(*Qdo(9sv?tcF$m47+0G00;Cv9AmnubQ2K75fsQrR+5l=l6eub zR83C|cLa1WHm0o8c0mHKY==G>=kP%?4l?t}{K=E25{VBSCKM82FbRG(`Oy;Kfn#M! z^X8-_6A3}itxL8{{PU%P_ax8=)CI5tRtf$s-8n&FGPwSC?#w3hCs}jC-GQ--x44AZ zl>rRKs+_n|2dhy59#XR#+sU7=?5#tSZ>V%jCI+HW|mm5z~m>Wo1XU z0n&JP7Qy|AcJf9-vi5VHnV<}@0c%*C76tqmgD3{b7!AWXn8e|SxuTIPH0s@*0pG=1 zIud-trhRLUf;o3h4Z_XIvu4htYjBDQNdkGg!fh5eaWWvT3{3{R%UGHO2IujgHb#sO z@l)8FT||)sk&Y_jigB&e zQ5Lr4Fy$4shV^l??r?+|_7wp~QTU^U03(j%5pO{W6GZ}zwRIc>2J8(kk~*R<+>4|n ztW1HUWFjRJp(P(muwjaiz%<|%O$ME4AcEBd>RMiSF;k#Lk}dVXVbfzR)v2Q10*8D3 zr4EICvS7sH=&Amgouyg;W4Mk(C_De z8Xk(~8L@#YjvOxo~u+cD~kt<}nbg-$?TraIF8ppP=(3ph_irwYn zIQ@60>yT~)DIWq!5a6Xf5v3b-s$I{^s_7~RM&Zp9MTr3)vKBrrTlVn1WnQ~@-}DdmZgfxY z8CL%2orr9zs-mpRCaE>@I)GlwwtNh=TOhn^@A+f}93c;6P@y0mJif;K7GgL@SGvk!(K1tvvhfaDWp zs94I7+*Or9ctzs7?dk^*QiR7uoFy846Z&>f5#4}}KCE%0$t;_JB^oply*nPSm zyWlNoVD!}!H;L{Jw7U1SKDTJa2A3bxIPU`MOOh43Z4;eD(BjqS!#8|9wKCW_xHPyp zG<(;bhd<1U?+v`<@#Fh1QdJx8sdj|4+%aCSBO@h{v;XR|7Kg?${ zTSJ<26US#hl}h+{SPawib~oR!{?x6UeRFn1+JAApKud#}b15oMK<2_qf32G-R`$WC zo2zcb1&$u?^CdHsAl=79Wy)pBAj$aC&)F3vp9cEJN+q%z^!amJJLo=7qq7@~#pwzq zA-7bqqr!zgqocobvfd!=zFkjeEQJ|v*F$r8WRRh^Y`yKnqbYJSAAd4sCHr8W&CPLF zi25$*V}G)eQ)=Rx;nyzDzKimjx;}i*C~kY$m51{>6>_Imy#BJ;_oC@KJ>`1pYx7Gn zrs6Nlt(+6@bAo@SXXeVDqo*f?VZ*7YFC4Bck-(@JN`BI#{#d~&QVHGJy}EdbJ9-b= z_L=&KgnrQ)c}nRJxB_YQfIKP`M@*0@Y@q88M7A47rX*Pc;@1HKe}vR6RqbgR1PDO? z19Yqct%?Eg3L+l|5vI`?Zjb;SI@w?HkTZOVxFv%mb}@L;$RPDo_!>qHr|iAE!x|zs zYLmkQv2Bp19C9>RO8g^mj2~EpqAq8NlP2FL31eC+&Ly}D<{2ZeM4G!k1l#DK==L1L zasuZFoGajSV)k5El8pj{A&g3tk$u-BqK#rAayZ!2T>__N2Cgm7A>hUOBd;bEK*-Yg zR)WLzej8rJtx1MJh$wA!5G;UbKJso%6hP$QxE^9Ag1Ck?P{+cSVS~SShyDzsv6gmi z7-9%nA#`2Hn_$}V^U_2(pcjNjxC0fgfjjvr4E<2hpf-s$Oh#EPDb-0XQ)&m_3*9YY zstu29r5n~<#`cy87#8k@0v6s8L=v^^f=gOEj<`r+4{_a(e4>2x;Tj7{z!=IIhpZ@z z&E)j~bOxl_aw5qNRG&Y!$Xieha0_K_J}{qavjn{S1dY#WVHMlWtZC9ufSR) z*hC5}T!J5T!K8(xG2j7${#}A$aOmXs(C&5*@(l<_DC@@@-`H=G_IcIICx+5H&F=~p z^xeDZ<jErV%0+i%jlGQz%08oJcb56ZFHq6+mi3v|3-_#IBSn+nZ&)o(eb?@lE@uK< zz9^;0zC$tOjJZ!#lR;PSol)>uME}&V=8@Z$f$!twdo=qW$tOI>PS-yYku28sa7akz z8SSu&kvQ#r{$|xZ6hDn&y_X4$)YywkeXV+@vS~XyM=p0_szv{FFk8aC(b*urDjKgE zHs$SHvVQAT+UIF)4ZVV2c7HFOGr?E*^sPAG8mqS6%3ZgrX$Pu{5a<=nt&rFoflDmX$+OlUs(GOYcPIr)sG$H+wHmU7FJ$xo@nm>@<-4|=HnPwt2<-r8qzOT zQde$XclF_<-HzSilIG6^RnEL_emZY+RNrN`b8ta>+0P|2jP_Hk$)xlfqFly&Uu?p% zn(O+9-j3~dl-RS%D)b?SVD+6B-DwVg%pa@KDc_mbzoXIM)2pJNKXJ8^7gUBtZ!?7Vl^qoqzlJ;;^;rejoL{@GC#?qDuOUVb=n(qYTj>F#-UAf{ zqryf;t5(rjETs`Ke@8>HASgUF{p+DziqjyASfC<;6Nu|$N+q|#en(VRjEqFxXjN$9$#n~B=JI+ESY6(t{tJ@P zniM1JVw0O~dHk6NG*Y{n$t<7UI{I4{vRpS#6l7fQ3T|{i7clt8z1ek#;GmcN zDBENeF*+y>nw{n0%35pa9<7b4zTI4*TEQc=3vt?q9gH!}N%sYb3iwU=Avn%Ay-&(d zK3G~W9=rn3SIo=Gz&FR@JWDl(&G7%{oJ_vhkY-?{(e$;rk&U(e#f3TEKb!TuP(I8& zuzhS(M93dQ3DO!rAI{sA@^!9!O?~73_rVYEnzJU~w2b|_^6Jr}>K9dPUxc5oL^%I^ zce&F*ApzdtQ$_NX5!%Dad11RK(x%Lm@N~oEqmqqw6T*Tg*|2u{$;iq{yKtJaSiZTr z=yzemYLw(tEabIGpneq=%qT|ep&7ai3lk+gU~?(g)|jjxk%eFtoKK8ZaB_f%B=L35 zQcQ}kS_PcdJPGZ8$6O+ZB@5FfQW!#_G2ZZuVU)-I@}#{yz{UWJv6pA$L|x93PXuv= z1ZZ)O&*?(zqzwLR`D~a*W7QIj6R`icD1Zq`h43yzv5-6x8sevsf64R8DloP%mi!m3 z;q}BZfS+(HxKnIfL1_b=iR2f@Byvc=L`|ep=5TVV_MsQA?f^p_-zw6Yc;^QKZK}#z zD5_BmcbP^tn)zTCL?;z;g%MLG+T%s%jpicQ=z(8Ac5-8{F&~9n35Sp5ux)oMfkf@{dG75I6% zSgV8X`A*naNf!LD@Jk8Q;tj7QbogvC?;Aq;K89hVQDNxnD#;vd0!1$|r)UE#@%bj_ zX|_c^$~xg5lIG*`-n?x8AwGcdpcmn)v4dp{BOq8RX^W$Ck*}acl((QZ6irGjGBV?P zx+sCVT;8&Fsq3Bx#Kc_+7FPXjBuXb7dv*Thy&a>Co zCa#bC9;d$Y!lsEWM_;-9aCdje#f070ukA8<{pR^hx8_IJ_dZoxd8LN?^#+%3Te~*( z@~PW77B*TJ>F$A=gN6lkQ28MXBjv-bw^CV$)|y)dmmO8CIIt}s+ByH>XnaXSsDu8|C`yZ*0$V<}H=-CnQlWrE-y zai0{XI7AnPej0Y|(i>Al$SGdlKj9KB+Q83G!}oM>eQMRj7Wvt=l)pN&jh)YuIeJ&-jR1!WN%;gJPvQXNp$jkhEhsJ!`yIGf3&S z%XwObf4&W^#ecGgw&gTU+_)yJFy4HpzF7I)Oxv2>=9li8g@3p(f0iS4as7h}-O*3h z{d0FL{@DE=%mQi0URBTUE4V5eeKNFE{mOP$*`1q~p&6@gl8&9)fDfxKzXoYh*as&M(1OATYH|tmNOHwAWZRkY;5P}a-)J~EB0|(ft zonk(h0Fv1A$|AED$+~dM!&@t$Fb$Ip*^!c_99;XFGc{_Gi*Z$fR#6L|mnCB8Juo11 zvu1g&Q3N{eKtiHj9>jJ%&hHWl+8AwnQmbK~$%UP!9%zeuhni}Wa9M+;5iyIUSO zV}8{zMPN(kbftU>lPNRi3#>&pIp$+4asA;86+{+EQ6~VpzJ2Gi%b{B9&^$)?O>^{k zkt#3V^*Nf@{z%o`n5u76pgJ7;Bxnf|6qHqh*!H5RgN?&#%nurrBHJCsCQ9uhbhLbF z3HZqDDs&LiNqm!1xH9IJqg-x6jrHlRtDjwG|N7VL)`F0#$<2b3=k~q1mFFR6|IVzU zKQVq!`LBOk$3T3R__=pOi=6VYlb6POB2KQQRt~1f8AJean}E2sE#c{2t0@vb(5YZ@ zw`hEsT?J2!s-Qe5z%&6eqGKobmI3h~=_^k=PMSO6pJ~P>&u8tRM)_b*pK~e2NYr5V zsH|trOVr+>pqlo?+)l|)A=ys1*C<_x z%l^)Ng~Ou7b#5t3+361R=zUg;Npwn80!O{wN;^yM$T4H#NFR;4mj>>h_Qe8a6AGWt zw~K^JzSMLzk$GoA6H6Rm59>A$jG9znw;8FW`IPtW(DP3aqej|Fg(31$SVC;zq-PIJ zJEIuh;d;bA12Hul0DVyK0N;q@7m3ae|6&vv;>O+`V;l1W6N-^0$YV5cgauS$P9>15 zSqMCZVhWQcMq-{s>A(Bg|1$&QrD%s5+RyxXq|30Lf+_W1pC@U;jS_=`t_8?Q+KPF& zNd{+yrL+VRQ3$zGQ_5Pbj9`WVX`X+M&1uV*BOga*(tpQl4gn|`2!JudD(fQqfw*m4 z{(YP-{{|HD?hW9LS)Ql)q`cFpC1pQ^2-gci)4;=9BX^-cade-K%hu0<-uRSgvLT_@s|BQABMJrhRsxVc-N%~+_dIe=SXbhC* znm3GME1(WZ6R;EJQ+g4FM@;KM?}Qnc;<~>D4j4rH`M${r;+hxKB!j^cP=^SoK8ZrK z)$-%=wGc`*;z*=t#0xwtCn-U_+Rh;ye!nbpFvhGVW7NbV;sbm%4%db?&?4ffk?Zo| zI2mS1fvj;q2P1hf%r7`mfD5l`&LDa93d9Jwn=ecxX(iOKsOZX{GT(AUnIJK=fN>x3EL4is5O_U2X-(SXo@( z#kjEN%9=*QIn$^7od+a)i>g(EXmwsTW@=3{5ejaj28R2K3rh7w^@_G>{`RZ>4k@7w z+h0q*E_5iGHvRe6ZIxSNj7=AA@`+kCM2cp=->^op^UY|wheg8WdeyGT3N`JMlkGcb z(|e2Ueh^F?)|;so30D!>xGlMr6ZSNkF7{u50(g<${L3wi z{d=-=lMJ`}eAH6V4fMlCcc=NEtH?%oVsRi;asK)p;p4QxHtS!5Uz=Y&9sK;9!}!el z7i$lkxVJm>qU`M7R@X;g*Sv0evvH-{`JXqNYPH8Uzvjg(q*0=hwEJ5;CE-7_eo^5i0!>Rc{Dc!7W( z9V(30Df(qio#8QhpI4>JW4U8uvyN z7j(5Ovc>hdErfl>i2;!@b@7!rqQeiUr3j@KVh5wD85ETJh2+5ntF=xbuum8xSt^DT zDTK)4(jz=!A~QaCq^{pdJCN#CAVPC*c=;who*QsDPhBfm&Ejp_rtMF@$-UP40U4=bzs=g!#ldKV@wHeH(^gwRfzfke1Ayc22bMW?hRR zLD{ki4T$(Tj0!)9p%lC*dKh(lh!f`qYoA&yiy(8AsZV#9(^fi+Bb|~I!o!$={YS%1 zBbPLVg@wWNk(otxQBHGCR$ruhBruCkUiP*!@s~Ln`sCnw+d+XxmQQYd9j!c=@g#Di z)>LHbH8*5+7H-({)3NyAj?zYUb? zAWJCIK9_ng$v*ew;Bfx~W#`>t-f9sM0~zXzMoH2W3f&ds7sWR93{dYnaEkGiYO^N1 zi$x%mkhUqSA28ifEZd6bvvmqnu&`WHR(hD-8N@`BZLpR>Gf>L2E!_{s9X70=KxZ^m zTv5fg^*LQ`om5>jOyqF9w18A1DKI=j7j!P3BEoNACyb%f0+7nXX7VXj|H`Uxkoj-Z z4S{+XSwHg^P*H@#?@AJp$iz*&rO5gr9p)wxyZry&Xh0Jj#|7Q3{u33E-}BIKASM_S(z zO_Ybp_`$$n;O&vlh$h&Pcv-+;vUxsT5+=BSZgIOZ$d5*~R=9a-A;~5K^)xtobbBfy zIYV{8OQ&B(wkx9BV87>cn3M2ORa~aCwTye|rI74A93*PuxY{YxY34JN1?}eL8$_h} z`X33`o$i?`TYgg|O8M~pUV7TkmQ#pJ^3qt+bF6>^W|WMr_?5R^MzRnNeuN|P?#Up} zBuGH$KJ?J--VnyqLs#uJS*YuLiDM$1uTX)peNmo?{+oX1B5x(VCLlMu;+=$Q!6c`S zkLnQZ+a54vz4YV-lTk7?8~1UDC8{~qpqWX!bZXxjmV%P+?qMtPBdZ}P(?pe*1VLzS zVQY7<(QS_BaYg*KDKvoo@HH4k1B&#Uz6LO+Y(wFU?@3}cIGOrW{vBr5px@#lk*lfGZDjoE*qSz z3!)__Vm<0^yxONmlW}(Qm)#z+wd_Qzp=cAY{@>Fm2Yu?i1a9dAZ-0{Fx_lRwylPo)D`F-0;B?BGfvn#FB+T<$=BKelBD2-7un0*!YV2HZ^ zMalQH=~l^tcj6!J{8D=Hhv$V3<9{>1TpxZ>v^&&nh0d)P)>7Z4ohjX1A+!INs(Eo^ zXYHKWW>(GkEv3xmm#%JaaNlmP8YXUdF|XJ((_Dz-r=xj(wI$0V;dIjxcA;~gxUQA7 zld#=)ZXPGHsgmvj%APut?yL0%Rl_yznQw4)??8WJtj_7i>UV9C@)<>=)U zr)mpp^-baxPcjZ^e^i`*oaZ}Z(Uq>R+8}!9lX3spi^{9_7(aI&6My}6?CZV7^w8a! zUs+xs)%y=BZhw1FvAK_Gx~XpV@6dZm>mo5u8Hiz;C1?y2k(j}f>s2f;P6lPMQj99p z$EqMfa4C8M*RJM_RJ5L47hKE0p3K4A(ZD5!rkzAttr2)T_jaZ%H^C4qU(BnJL1R|N zvKO(zae)$>Fg*F3O9K94s4{fjB!D180IB{1C5i&m19=dM&QefV6LYVXI5qtCecVMX zDPI#>$CGz);O`CWlN&Qcf)okp!6|>I3WJDOcwcIru+EeI{H`L(JH)q{r(OymQ9kI| z9TF7aX;Sc&f=%F{2MUL9kUFEG`2-i0G|*>*d?#aTy=TYiqpR98xpYxct>T@&n^D;-)rYY*7LO7gn^TC5?QS#y1qc=rc=5>slLloCG=9I`}`z5pT$ux&y+@;@^}>Q z^QbR2E=CT*q&Q32CEgUZB!u@wvSO{swuELG+g!32utJuRswa{7dYk50s&`?(fIG*@z{q5C1HpHno{sj!P^J#0=K*bivPh* z!fyZD6HJ!EV>}q;GtT8%ElAHi-#gxVu+&a0+11vjF*rU`d-Q1V&Ce!XbXSr{z=$_1BBXX1Jp!+ zdD1O-(r25d@Ov>Ka(O+-4x2|3F05g1D zgpS<%=75g2k>R!A6MoTLS6hwNtcUy`9Cza+ixNeQKGK#K(;wU+j~;2iREq zb1{l&OLGxXVPkMWAJ3m!I)%_xZ871(2K{ucZY5N=5HUEBpU!MIGBR1!koSP<4(lZ(j60LiTsJgiGT?w&Lm}%0eyTz znNmP1&x%Ag*U0#nq=NW%V~3(JHw}AkJpi`J`OdWNbX3+p&>g^D+1E;keJwz7TZVIA z82ZrYLer}P7s|QY?(GxQsd}rTc~Y}DlVdT(e7NC{+xzA<&)F1Iqutg)z?aKY9fbT%2l;oO?aJ>vZ6guP|Gab zPJO|%T4^-Q)>kaMV$%s4dTGxnCZUz_5T|KW2Hb|w@-A< zZ}~+?=Df+M!a|)-w?iG%n<+9Bvo&;t^N(oq7GwX7CeIItzAzAYaODMYo^(|A*(H69`Dz8)80c9OzaEnMesByB0ryIU$I>&Ygq~hH@ zw=tQtc1bn~aesHtiy8=6bJ-K2GL5$bq|#@4X)o1JThwmm{-pOU-7ok4`z^V()Y-j% zJ^Ah1#eZI%GJW_sywp-v`Z|3pJu2HnnznC$=7ahZ^qaR|y>w4wT;g}9vprMdgPTls zH_@9Egs209NbtbgzIHO)J~*D#&4t0glUm8Bi>sYZl1LM(g&D_XDo+3Fe_S;|F={?6 zOCQ@DnVaX^<){dpK4}KCl8BOjzN|^P@LsWeCrf{)puw&ORpMLPQ3QfA+0_R=+Q@1h zJ(0uQE#0y$<(kmf8;c)<=>UDLpmD5PJEr0(q>sc_s_U>j*u;0oaZs)cQ^t3Z6C zKpmdlrnv}`#Lw#Kt~n)q6bmCJ@0ozjCx|dtxWF^zmwMp;gT=g&wTBST9lVqaq!88- zffE7PKAw7+060MNKOZ7sN!H7}4eeXTc#A)#-89_r zWXY3p#Z@tqo~-FuhWKfk~I z&B^R<6{5bbi|kkV?vG7*A$oQu*Pk^y@M18#df=^BbvxtL z$ifwM#i(5lAAi5VEgJMR^Rx7N%-U;lV`oF>iu^c%Up_Ef71dTs?7fm>^OE_9R%2_- z-RNL63fb=wl8x*NldjVf<3q)+anG>lKvH`px0R0asGzr8lqBds<>Ve2iMZ|hs4zKJ$5>9s?TY~%t z?P(LT{b3s3S%lgE!m#r~9n&~S-XPm?`Nu|5Ng)a0tp<{LVLpP|z z-WnLqiz4M6aRNZ)tvA-$DDuG^j!dkcvL0n&>?GoP2IJsp4rk&gnb=#@*ssAY%pfvXOqeA%reDg2e%4quzx(``>v;ND4j~iy^)V{TS zx4UdKasM)(%6HA8NztLv1@F@h4}X0YKEn4)^VIUNeFrn0+}tpmOIh)GIvdVmb)BWh z5uP2|=$VoT-dz4pLbtr+7ma+8K965hJ?^DwQ(jUi3JyKXqgDs!X&E%?r$lh&o_KRN z)^=^C*V?8WT|IJv?k;9^^sIVX%Qr#)czl_tU!Al1$6B)!?_(8}jIV@LD>MH3A3mpt zvi}H|nE8Zn0jF;5Y53ibwcYWz^X$w@G*88K2;kxrHvq*4wz>g$n=jARpJr3hiis50 zE=bG-8mr<0>ototZa)^J3kw+!$eFv_2AlF2J)~7Sz-}#Hum2oI49dLxP(`^b4L&_9 z&E#vBC=7@?f6jI3&p8(}+{-BzrykdszbBVc${*Z7zYd}9)T05lsH)Ek)VSM~N;R(E ztl!4U{qn=Apzi4MgN^-<<5tRkz4!IuVnDdQ@HqjO zl706!p^tvUFOuz_F9eMSDl+38g^r$9Rxq?a^YFoMk^+l(-LO9d)|=;D9u>AO&alr- zDyzv&E|O0P;|hl>>80~}>%s>L3=#}|r+hBSDm7Til9e>3=~4MgrW2vm4OeN}Da^8n zL@N!DPu0xeQqzGHZCP_6~TOFx3mRp`tdA=ibDn9zUKVrQ~v_?3mCx8AO7~jwzUr zIZ=^!ggv1jvwKMnq#KL_`x}mUFm1f{0MkNQ^Cgp6@P(K;xAHKwX zU$ys};Rw(9kAYA3Nc`ryI5OqDYsC+ecWo1$Z~9(& z^J6YUQCDw}L-D9j=& zq51~tLW8@;jG5iCw$H*#LVNY8PZ+xUlmn!h9`!V#wL%nc%MH z7!K4xPJ-zKz=;Nd2jDPHX*EK82CSC^Kp`*f7=zqdbF}(d9WnQARJ;xSY0IR@u9~mqU;NZNJwN=T+>|Ax9)IcDvHM*GZ2eRl zzTf-3cHPS6Jp011I9?t)5z?Xb-Nvuei%LER&e6O0P2D@RXY#Lq-}Sgc^?-Bprj%A|Q-LI!vS-rG%0eXok{F%Eul zuz?eNSZ=Nl;$V2MPr_5eQI|yV@gt=ypHpE#p8~sed2x1&QP{PUU~51Q5&;Wa`~Y23 zY?iUy7ps95UJ~d61oX^s+=18-a5DXJKKBYg*BY}?=gcA)Ls9Sv^JuXr@xHRK zi%NlQNI}Em!_rzI`}Vtv*BfiAiD!=6WbtC>?r_`B_E)gV_`{s`27_j2hx)s&+9E^ z9W)uw>MdWaImYFxWhAxPC05`6c$H=^wAmefYKAK%q9`x(ip=G)`sM|Z3rIheh)XO* zfFM{}!jsjyOwE?O_Jo=P)k|UC1`}k%s)AX|5 zp?7y9Pd+O>>Q=Q?DLwuw;v(W)b9?lnbVB058MOn^dy1dGy`e2%wzziR!8gF}Oc>?a zw!BFj(-)xv&W20+cI;-|J6cqZ(He_X{sum*O&UIYX%;O4R>@p#)c&4>HsQi6NaFYKyc2(JFFe=Ex)_VxL^ z=Fk4W7NCFpy0_h*+AS}-On$t&SoLT`<+XwUMfdmt=+5O@#%}D%c`vQWRknb`T&CNw~6Jc=;nJ&OP zYw?p188ColLxbI10ww~$7wImV6@~hO`&K~y0+$W6Oe1dI(B4XzK|gI(N`Klavg+wz zTtQW&ivqwR3P<47wBWFdu|sQjtYnBjhkry^ZJM9c*mp+YS#suXo^BJuZ|ZfDk9PD1WwY!qCv9ehA{5; z3On0lw}&!3nq$=@PZ>tfuKY2fsoKf+>Pn9Ps|QcUW`dfpX}z7Z+!H9Lmk?m_x^ouD zp)T6%AET@}*HkqI*ir8wcI~)@W$#6o1zL2XOWCoe-3oxlN#b&nlZA#(!3JR z$Os)FeEf!WAro|lZjeAua9F!jp~wJH55MITgp&XLGhC)40s{irjC1^->E9XwMt5)v z?Cl`{$haTILI9@j@U;-g2uv@KgP)P%X8zB+&RDwd2NeJU0$H$wg)+y|I?e?AX|TM8 z1Qmp*;k&T{cX^vi92$`t5Qz>2#Sh<}g*V+9a$;$Oez;5k^&oE~58DYm;y{@N3piv> z25sO=BDgT#jSP4T`SOlia0fQG@j<8xxV{_^?B&s=gW-}@@r!Honsqpj3MvtNT|c3v z=NXnh%pNZuf~w;$qpGMjibUf;94z#Nr-vE}-SRWIqpanUnRiG>U2{Cf7hZb`1CpT2 zxkcdYaq1uH8@B=#u@|zIaWjJ3XYS74x%6(g!Ug9h{9eyLcY?;mQl!mmRqmF3`Isx$ z<*k#Ou@5wCefA zgb35PG1hy7xr!n6d9l|OWIO5P1<_fgI2lxx!h+y)He{FTB8$Ak17azHTTnG62>mMF zbQth?Hnz@4(Bx&If|=qzE8DpRkT~QZ7Q7Q<+=03d*={FFIQQouNwI2VO^}K#)zP;z zWu7CNf$Zj66b`tsVwaQsV};04ANlED0B%48S(c2z&LGh)YM-TYjKf++13{0y#MAJu z3>9FdgL+e7(XBeD$QU7147V~MELt6KkEJXPD=Gca>KmDgE^Iwp7CtswLX2!Slf`zr8GkKRK!CkwU&e5cyeZszA>Rj`=lS-Dw(xc zEG3i?-+C2XtIo4;b?YtG3*WY%8JG~=P{N_5JiGe!`cx~Oe~mDI zXZO)rDpLhr@ZFct<^ms|6mhR-BZkeo_H8vn7M73ueXh%oamDm;Oe@;gK8u#qZa1Mn z$bI*>*Aqi$#Xr=h!=g^b1Q`rIey_NB-SAgyty2Lcd6%_n_lTP8O}(y74?#N@i)!4r znKmH0F1ANs0?*wTziZiOs$_UpM0V(b5>syNiC&`J)tqY%w)Au3n;SjuUmxyVyVP;( zd73qiwRH1}Ucz0Ug2>FzlXJXcyWMI=nBr5L8*f)X-h8wdb=s4p)mTbdf4oVEo-G!p zd0CIAc%?|{#he_)58BY85*t-WsPI0SQb#<+0we>MBc7xjEat|uCR{0n=Q2BN%^4)t zAYtc*T7dVVT)F7^4RS8$Qb=tof0xGj<6^n>jlwi(7csw{E}a^}b@yj&{AFu9B<1Iz zDX2Mp1M|+hVd=qvk}oXx4sP?!PJg28`{OpsHH zX#c4>A>56{HJ%)ob=LrYF$fRQm;y;>wTLiT9t5cL;y3{jx)3fro~UR&`9VUosP7%yCJU( z`~`r-Adu(4vyg`Ki4*BY66R@vNty9(iwX?-)HAK-14xZoGY}$#Y$^!FiM@o?{I~s) zqz7Z;5gs(3U4HpWt-RYBd>vXrOS}(iRIjlXe7fo98u62PM|#tu!H52wRq3+J=&LJi zFDmbP^zmgJNhKIb37+LB`^Kt5moC7Lt1quih^Jvyo9~)qO$}i8pAucbGE}*&C`Ofy zu{srPI+M84<)Xc^8ekJhq;$G?HlC}`R=?q~izIX}sd^db!1#-6~6sXi+GS;&g&R*APjEVupKZW!uG#$4X>r zt0_swA1~`f%XTm$!-e_aksssR`^J(9!23b_-i&U2d0z1aSHi4u7*UG!A=arb+vpiz zaP~2i{@fz@L;73_JlLwO=usY!5Cj{MFYGs!-CxM~n2L{ZSXsVHCjeuX>=>TT#f@c$ zsx5XR)R?$kCzIXMRN>);Jyb1_X4^ageP_us1e|q9HMXkGk}2L}&dB9^6PxPO>J+fi zZzMtuvmS$*4X!r?xrqWRJtGV0bV>14ZVShiu0rv zPZln7`2;O7aXGofZkv*gT%Ddab^BJA^t?mZ=KU54mr=c!eQr!P37nZP_?51{rr95I zzC(vE>rrXdlen6g^G55B#w^L#t@ovcKZSmj15!)%f#*Foj*f>kv;4i~abl5dB~`ML z@%&>|g0Zq=y~&rcKuTG!|Cy1zjr_|jt~^8K9*Sbc&3DS8P|6blMlo_IxA^y}XnAT~eR=I_&Bwias*fMc^oEere^a&68X= z&Kx9SGU|gn?i)c_Nx}YJ`Cp2M0u8m~l_bWBpgpT7;<78UAn;;>jgAXaG4%}bBBX%z z5Vkz9lweNbu!Upa7#cPLZNY9>j|?a3pgM7cpN`S+676pS=f5hC4@(`49E=e)frp6% zlFKv3GSH?Xmez2)7YVX@z5d`yfVVh#<^imOjv955$p9(w2j>u-151Za;y34CoTGW2 zEekIxU;p`hu!7~1_Qrm`guK7}4sKs==gPC+Ma|ML<;8UEHf-`~rS7GIshD z^8pO&_QPP8w3M#5+8vzLEjx{6(!NziLpzxU246Ci_1y*@n`IJ)1X(W8cR@VheNl#2 zY@NLE$Kv?T@9S&*g!|tjb*lAHPyR@aXbnk^Gx)lC@VQsxPKIrA=wk2Sb%B_ap@f5l z6Fo;|g?u08prE$5Ctkk;BN$dJ_W91SlDO@^-hP`%+7q2>7Z*^#v^i8!swm0qBnL@X z3booFFeUl}Fqlx4P8F^!mCrZgapU{Z!29QbASs%HO2d;J+j&4+I9IIp@~0ZqKpOym zRH)Zw&i?<4k!wf>yr zVyhDiE`8VP)N{Ek-lip#4mNWuE6CTe^#+<_U}Oe#CSg6W3uZNu;&g#*z0hykX=zU4 zB{)AfPtg#UFbjb`)z7KqXWC||x;1h9BE)f)E)phHISwwf2*be8fHb*! zH-{CwMqrB{Z@vg?ceu87X+pk;BSC)n0UErr0wQL9rt;2w!Nl4DpJ<27%`21ll+~YV zo|gDv`47{<4@UdEekr{FOUNf6<(}?Eo!j-Jgj2_}Rb!{m?sq!7rzJ^n;oG?@jf=uc z;ZMhQ>nxQx2zvT{o|;7ud@)?H4$HvR`pKlpNrYhdbnnAb0Ag?=I#j3j>n3=;wVgeF6@B;5`P|?ij>;PXr%%j1- zAPe$@)GO8*150UAA2`wZ=h%@|!Z3sa)alfabyp-}4wa9t!@N-L1uf;>d^7} z7lj9Y9``YS`$Shi)b5f8yy+nHE|(s%52w4kUb8U+0!bn{UXvXBJIrD~D>Zm(`e+$*R;_ux*OdRHR;83tz?T1MU z0C#<7dQ@y%;o)k9MMXU82CyUup|5{BYx`y5!E8L+_s69Egu-OunUf}jY2X$iZ+g0WdVd^mUx8xv%ZJfPe4Tm?UbKKLA(w&eMd zsRgj%0&JlEDN+rxCGbah3nc+?Q}#GCG{+&M4aV3b0S5sB_~LCzXjj4|oVz%VXPz+g z96A~#z}K!zwGbJ!?s6=a>2ApNP`ljCaWcya`%pkf`|%KsSD?>(8UdFYyV@aVT|dH*nB0=iL; z#9E$pXqj6K^J6!EY8rp>Hx+UEgD0ikP;2Ph|MrdD5Ali>Oz^Gq5AH4rckg$MESkC6 zr%h69>~{Crh1$can&eZ@bVnvQCl!4o1FIhUJUS8agTjZKaGAz~ z^=)camiro4g}4eOMovMtXcnhxLCn^H>kx%SFpd<$@`47}eaMCh{%fYeMV$pA85-f$ zL<)HE!XSkB?0|Gm281GS7Edy;>r5OW zPRQ;ERz%3+BVCc)&|l(T&_6p}&k2CD?5>5e>iQ#(4{H-9UI;hvAn>+`L z{bbiVg6YMH{Ts}B-Lsg!v5n%YGX*A*ICds+>#M!7lf-Y`W%j&S{nbZCGMzJ-F5$$% z(YnBHiTyTNMN!zdUfJb^#>!eZw{8}=1zazb3oqikr_oS6ceRSW#?pE|aL#9c`>ZK* zmzEFdkVU&se1Wz}ZkV{bUivM9Rb)yY=Q6vYHU%$4;Ec@5r7uc=l&D8!gfcN#Z76s8 zuyHDVbbn{BvdJ#h{D{$EO8NDTJCQSJ&#l;_V1G2?9M5uDXo_@qW0Sa4_QBV6C1}w4 zY3y#Pp?oPr6HyH&>6m+5b!%pi>f{4j{CGqkigDq8UMmhruF!J`Zw;|4`*j9qczext z+ji2;eR+Amfv&8^5&kQRyb1N4)s)yk9^EHu^}-#6aPow$S?h`2iKl-oV0w0GKUaMB zJ%Up@&6{;0L}wvkV_k2T+HR9HIkDc%b5tX>SX(b*3ddhAa;ONrok|xes<}QlBbjC? zT-Q3#z@t?IJ_q1JH;CFf^QWO)r3lF|$1oL{M>*zn4mC)(E1Mk_uFJFII(8+O>~DHH zAofk`#KEcV_CFtu-_X@BjTPVMvWzYH-+q4LIG^^>nTv7>B)v@KbzXNS*7D$`n;KjK z&p%zQul}?VX>!=`LEwXa;!KmF2|;w7(>8G|F zsKzM;XWfE|$E94f@H8Fvbuy6NQHPf$D8+$|rf#zU1=XX-Uei_(Z(ey5&tDU%3Erlc z{BQAKJEjPDNELBY3-0h}th^%u? zZl>*LW?%Y#;rbw1^CA3W)%c%#f>S|h+1G~?kA_&^Nci54?&-e}JNPl z?+y%}S&tHvZxwRJgXfa%^s0{%}gU~fUsaD1m3VrFO5a{QX zg3hmhCjqd;n1Q%H3@@O!`K3Qno=I+Wut25{NZbx_It)?)SqHULv$-Zd8PecUfP)0z z4_X6!>bYt*Pub!Ax)Kro4LJH_7)b=AK_wj_<$+DH5-tSY0C;D>2}PM~&_<0g)xrPR z%MV{H4Wqyv#RjbV=n-;{EMi^rY=#?~VZQ+vzRmtaT~I^{_*ieSP|Be=5kjrw9GeT2 zFsiQ>FhXaQRoW)pQQB}|pkWabZ(|$U%MvCLSWMTx+#+$#K26PJN{$I*NbkRF37xMlXTbg*T9!YO;kJarQEb)742aT+%y$QD0Sdw#NK+Tf$mkEq$GIQ zAUV3>tWWLphLeB{-wj;sNVhj49DWyYAwm?<1d@zV!hgC0-UrAUcNpFv zCDDj@KyHCVJ)%o4GUEM+yOQx?pP;@Yjs>CZi36Oa%vcmNB;bI&wbS}@Xqh=PFoTa~5{XpAfBC!b#Qlpa+!O3k7-kE2`xv@_f|(MG2vCNuXK<5uRc-@i}at7P#zBuQsUM)6ZG?sg1|o6j968P3xB#? z>ts9j5oQn8C&u!`Y0o+WGOb>plCBOM{gwvN8WmBktj|AAiC-1tytx`k{s6tk<)%3c z%C0XqQr2m&@n;WbZX9rKEZQF17!;b-{U4tZ&X9LoG$CR<=fKe?EGXrQG3)N7=wYA! z#x}woOP0nAR~IIXwZ&7B7J{jL3=hVwgH0kk=eXe7L(SZK2Ioq89B;qAYc#8GpfsfN z_-)(V%FeA%ddX2*2NjhJ&;RLh3a`ULOcVjS=d99t9jhM0g#Mb^(mYVLuK*4}o`qhe&o6Betq$=>kd~ zb`oG-0FWRUCZKHwsVTKnfxtvfKcN^1wlo+h!OU=w8(IcQ@at_nPcn>}lIunxCIQ=y zo9m_i4O-oRpvUQ;2EkW}cwJx|g~TV|`(O+yB!UD4OyRK{DzPSWHA$R59bUH)T5xhx zlIcKJ-8!-0S#EjBt5=YzvaIUMZ3+8)Bt7L_xjv7zE_QX`8Wi2`>_T_vLMU>6;~y5UbJ4vkjAtK?UBXHOktj`t;b^mDSy~Tl?3_ z3W{WCC}ZVn17f?v9`SykHTiS)xf-s767v}LyVxUW`fLi(Q`ziu(KkyYipSbaF^%HR`Amzdt&?^#9oD&-+s1KzAA>`QYd=YC*_X#vwsd#GwYll> zd6c*xG2Ta1PY-NbS~+V+um=fuWeC#;8tk;%o$wjLVt5-7*lL?;4LderNXkdN@%D8F zgEru(sC)`BhTD;W41WHNQTn{PS^pGiN zXOtTP9aHs|Y-&2f!R|M`B@4xeyhu?+!`lF>&|VewEGWIzO?{JE@GO;jJ%z28Q17vx z;(4?9fG8u)Ut%k~;b z%u(q@+Q2ULC#3`iskUY{r7+x)vvPLc;DZ=}=>%|S=TLCk%cD;~dIENnx(ova%oSk4 z0N17f>1pPb;sl}|Y&4>rCEyAH&}3kafJi#z9EC;tL^b-|BDCPDqIWfUj=>NRX#!gW z!ofWOSx6&zC5DFRRTy)aO3)hs_XiMLq?64PCH&CcRAPujeIto)09;fUdjd4ANx+x` zq4y!ATmTDYs5Jfd$0J-nOcV?=o-?jFpG3o}H;mrl?vO75$+%j)d%FpDJ;mFX!vSNP zypePeGq6IC?S?`@bY`H7RMZ z575O$Ql5$l6!zZ)Jjm=t>aigG{o2WoMVL0NG%+p10}(%C*QMkA?n|b&3^Mb4?Th$o zc_m@exW$R0{~_AG*i1=bLfo=!q0}|v6Ni;GhbP1rff(dyC^H;n?JXy)CWn$hRvKyd zt-GGIuy@m2R9iFf?vMikxuK%pz#AhkuWza9J}c>74<}*EE*9uaYre}*lD#XAu~4Up zc)RziyLura!@R|2s`GtD^DsBpu;Z8b^<>=Dc>aVa~^B zdo?mH7Uw9))8b(6m`zr%K;>Hn!ZE=H0TBF7j#R?t04_ga zHIWY&sPYWS@PFe6h)OaLm=Ie893FCj!|l+KF@xxdCqM&bgcd=WESMt0izR75?4tfZ zMFD64WM-k_P5;;N2!FjGNB-ToNW#6r_yjb?Z{dLWD|!mR5NX4hn5b_3e*f?J3H&d> zu(El*-Y*>mYtcom)GzRd+G8xH=Yz%lYVH3gz(F(>bK+gbx68Ahr zBkUVnd}ZOne2={B!};h@rtYTPdHEdqP_yKP;_FpC_uXgJI!jH0;AAo7J8U;`-C>GQ zmNc~S>A{H`PR#;m{w%ntNxL2RWB0StgAz~x<+h|*e~!9iF`hZPZ>7{Xo=sZ!i>=(- zd#Zo?T7-z^e*7|Oz7&jnTuxV8%KoHk{8j! zCiyn*&*f~^310Aoixyjg5~8|2%qni&xe9=~R~0%!C#N}7{~bDA z@b_~oztE6aCE?e-3HYIXWfx1!kLq_=jootHbocy`^klVoaR0~!+^n6$Kyl5a#93ax zs^^iXcL`dCZSK9oVQama)5o-hmhYP>R#L>8VOJsmTIa(u&?}zY+N?FJW z>;e~nj9PaHvw_4Tddo4RdeGqP;)hHRk?i5Z-#n|xB1y9bls^p`MV@G>e$IsRKWGn* zW;=o=yCVro#AAW*3i_dnrX7`;0=5U>9>B+Bd65>{kxEtM7jRQRWRsBW0q|ev=WHaX z4*v{A;s6ka_yyQE!4{zy+DPFUXaF;&hc6i@3W14;gM#6&fRyQ$bRQ;9F#^m+~HrQiL3{ zq?=faN_xSHyfeX8%SkmxcS7V;D2`@aB|v|ZnCfq}@Im|x!A`xEa75vqON;p`50w2p zYx|fIaGbxscyu)ChQz?PL910yA71r(q13!E&SCv0+V&Ni?}t89A>0^6fFR?uvqxV} z5Q}A&7hda$?me3j>3xaxI%+b@*KBAdE{a;yQq;#(r!_G4_@Rgw$<;^HMoGjeoZ`5J zAvsQH(Wa1}?L275D-;{?5f!t;BBpm0TqR{wq>6}+IodvsMmS8w_0NV}Y=ULpzMtgI z4UbC_p@e|CDcW8vrl)Kf(p99+7QO3@=0eLf|&3rC3PtWv?`* z)KCBN#I5Xo!8SqTD_16WA8esLOlq9J8+X36YUQlD;1W zVQ&pnRIES#L{F!7GzaT>VECq`e)>6~>#QPIIpp7CO?FPYqMnmqxYx4etM&Nx#hE;> zl00+EfX%HP6PCCN_u?A`Si#n;>S1>QYEeZK?Ffk%@lgD(Q?Vg187HmP1 zcmu2u%cLE{Jkh!C2{DaEKI0T*)D4zO;pAz+{;@C;6}2)9Y<^h=YBmYt9ncKv2{B9L z7y&wiOfy2FgJU1v1U{8!e>gc|P4T;82lT{n6KYNp*!%F&Az8wtPQ>8pxCNJc;Neg1 zTusek(@R7ycG&^3AOOxjAR_YOQCn!yEoDY$0J1GXf#d$)^*Ri+6`^_#sw$C{d=iD* z7QrNl6HJMYJgtg_%#=Xe`qVslNSR99E$T-TDOa*wG!sK9fgBIGqJxC~3VK71!m@2j zbs8A51acFSq7{WQgDWSB47|*yZ+Fy7dQp=;=*S;XKcOM>KcQbm+wtWFD{(LIDJNck z-pA+qSLXQ7G(BfN^R&~cabMqIG7@8j)TC1l6~DCv%#y3XuIrf7hf}jj64x3I)T(R< z2<^;LhBT&c)RL$OP13?h#8>hGyFwg$NQQ(R2##Q-Zgrd4`&=^rO!k#PD%2E{JnydrVA(h=a+lTWNEh{E2 zTMtfhpQFC|he`8i#<$~A6Ow5zWT$fq@DBANzXi>l&{Ofm<+uw~*Yrc@g7(N3*OF^* z%o4g)u(KGAd2w5UILpQ`+m7IK{zz)URO&gN#}3LNAR^XL&4wbFD99!&_>pY}6WcIY zHrXYZAN9jj$vaA>zKjP2;TmoScgH!FW&-LKCmj5+_eK_4a3&)>KSoj#&IIHrN0ili zGCZWvM-G`NX#C)V%levsX9oPx&hlFn{C)>AKtLt|j6?q-qk0?^2@C;1T`($HVeJk} z2;>=p95hfLv%>IK!XID^fn+O@^%^8VQjqNgybIZHKx_jXe!tsLRRO&Ovj*6^L@?~U zzm>r%@&%zd01+6li-XQg1hv3W2@w26l9+xem;^2U=MFHD_^o{*AS4gt3dE2JAYvOb zI4b~-w1>HHK-v9@TJ_M3x+N-n)x*5S%5tH2CO}wZ+(5#?bI5%G77wjg#bc?8 z(Hvz31BsDE4;1=jh0|-aVKA0!^H$1v!B@2T^T^DCZv<4HMySKzfe+-=Y6{7&9-0pn z`#89pyibLmdROeEad34fU^{pWbC!)#_0jZ9&>-8#V3&sBCp$f_ZfNQ?&8T>vh?Vq6 ze%kWz@!TV(C+Y^;Y(Y8EXVo>r{5y`A*;S;)<`7S|t!`LaDcRs-CX8L4OlMY-%>-!DB)3{sNNa=UgNGQ>AMQx%lTK!!ne^<8y-$GC8J5;=w~>n3 zc%*>`$Vu#L{B%{CmuR$yxc(H9sN~@pSjk54M|fzq(_{oNk$4A!Nn*6|0la|+jREr6 zZHiC7^K-;Z!G@Hn7s4(C(GrZ5CSsSs7r7xcrgwR~ZSIf_0pt=yKDlwOhy|0ME?3b- zJoA-4c5=jv&1E@E#3tgdlUl**BfZ)vZ=q!sY>1)I_*ucn%S_K`Ud6mE1>U8EUSF?Q zfpG`+n_Aih^l&TIP?v}@FnSnaCFMDfZhdVJf%e({(fYve5(1&3;%L4<{Yt&;<{ntO z)^O}-F`X(tCEp&e`^}_xs8G-=Ld$dDq?kl{mz!9nBlIeQz0YpZlcCy`Y%lgeyV z53}BDGOvBrml8r4^>t z1rw*+-NB3PZk>s^>EGR&fwRJRy5hpq@i+f2lXrd@NTwP!b5vy?JnP)~bs-u1s%>U- zmB;i8U$0s3ux_#|ZI8YJ+ju5Ul?@L|)f%f6n@oSI=s*K6$yHLg@;IlBpb3S`RIA0> z)f4AqO4n)-_>2h&>aXEkL#d;Fv~|jLX=8@cu#W3iM+6nc-xTezvUoXAFZf5B6^XYk zGaWyE!2x2Lud;N$$_SKQ3w{0LKA(OE)2PIS)Yl~$q5SgGC8 zDRD|Tr-RS5o3LP2TLDg$l&54Rx4-wJk_g>Sb)Cc`_<8-C=LJeTXX8$`-#rVRct3}l zr5l;{zmBb%iF27H8p)f*>;z2yQLOV^l*R05`N^ddlg!TpW-s5C^WEk?I$GRukv8L! zCw4H?<<5Q$&##jo)DEKQdQ2lHJoO2de2p$f0sE(7>czvOmM6kKFH01>xKP}BzXF@! zXE{a;@%`-N@=~W}>btMhw9$f4NX1MxHnTi z`#$4Fj{m#A_!NzHDOoQ$O+464Eq>)!vsf;ETI*_s7qM)_*H^v4Jjf92PDz|Tc1frn z&>tW)vV@bni&Qd}z%K#O&t{q>c@9<>;Bs)BhHVLw=FA5m6JoQdg_Z&L9GFokBaj{N zq%=Ui8n{;wNTN?tCfo*F_$2lw$2kSiZ!o4rx&V}YGPpfaNCho&oi1_$xhEhY!`K9X zi@gB*UxV9$Aq8EfIVSF9&^O@dec#F_mk~XL%R)AT73U6FPdJny4>}AZ>2>P$Tm*9r z*RTvpv*O-X6DNaSl!rNn>e~S5CNoB?^*m$(ONDp{!XX!A`Ghs0YO<;^V*rdw79qPR zSSxpqv)UyeW=)UY6uK2hGZ~-@_4+7EUob8jA?ESP4+L_%VA~7~i#1B4@23)OPE7Ya z-bXzaqQ76DKB7VU%Koc!{www)1zI(|1cF@@ZppC{P}@eJjq}oxc|6t zLXVY})oQunqgM|s-gQc3Ch$HzD3&#tH;dP&jHyr!KiRGi(=)ifv_Iv)kxQtGhV%_E z9q_~HKAC%<9bkh9s1QtyLdGoO&OgB(^3udJ3T78$<08Nl5$O};hiYP&MKBl+bL>d; zqYwlGzypzK0a7*rs|8qofYZSpj`bwqG9cmsh8n{E%@F|1f_y$hGa$DC#UtIrAGw#~ zzboBq7&8Opc90OrU<1)3{|9e^MCfn*0XM>iMEAd)2??eQFl2{2MsgwyxdPuGOd2rA za};JEB*w9Zf}IC^%pBND;id#F2o=K^0zPO)Iv1fS%7gc!Cl#nroRMmX!vfjrs3N8A zybOb?5i(c<=~2K0%@zG_D>3~;)RT6t92~A!GGt@?c@#?W3?hDE`=yAlj&5~+&rjsA zvwZ6H*7yuZ_J-Gq%glT`;abH9o>_L6|F!-P(^#K|m)BbJ`HsLe+vnRm-wNW+$FAQP z(@ncHE_P#FCs81pL+5a0&96J_ASL#l%o zIZshpgXKOk*K_io7$;|_X^8D*Ea}{J%Ki>~8;{cQ$>Y29NLQR#n znd>kt1Pq0WXksO1dmcCUvD9K$6pPae60TVL09IFWYV(-kbkvu%)!mPdKHR5NN+jgE zgqAOsoyR{O`E31Jsp#vVP%ACu8>C5t(qK+@I_k5MmV(3?HF3doA(<__JWqNyxB*1+ zIC)>z<&P#V$oJ&flQH5^qPRMdvGj0gz+h{VlXW}cY6?#p=s0;i&^*g*>m@u3j~J(W zyKq50=E_= zZxqGf7KR31ph^z`-!0WP)mnWGOc=TVr#FVKA#qj2!O2MPf)j%V>}KFsAXWAnc0hro z@K(CT*e7|xPgguI(528tcH!^sYI7xVW%V}{9YkFsi1vvSUe@=^&3`V9+-a}!^$H}o zmq6h2a&|A!J=z}}Y1W5o`hz8@e!=V>XoC_A@!_~DIr3ltM^ z^e4ORkqBqsBWj*X&p#JbEGW4qKcY`3uUlGm8VX9@KXN3wl-WXN1o`E@-F@>q&>0mF zQTm)*%x=*?>PYa?ph3s1e3zZuLc8~ZpztRwr)`lr-F0={{~A|M{=1*21(yS-^5)_$ z(4GgMW#PGsAxmL*K`^|3Qkn4xvpEq%qprH z?IT=yOYOp{xCpGF>T%s=O_n`UR)@HgFnzjn+Pt>J}soj>CZ6%F~ml zTdUhxS+}?}(>{F2vE5GCwQSLU)+HPte$i#V`Ri9r9n#*Nj=^f|`YYmBXL?-x(b9YR za-F==9n2s7wku}h+_uj|>$HdTZf{V*Ysv(% zbW1{ekDB?e@=)&w5*^(U+Qg4}+LAc$)Vg`L#E`zu;o22KkPO*YFIL~+DbG@-v33g} zI*SiHEq0s?-!2!Z4|vHDse8U4aB?QBi$!)UAaK30aYn-A{?VD(=IOYT=*>^kwM(DU z)EA#$UH5D%%=NyP{&e#nrhPoe55C_!U*KYY?2Y0Joc_cw!rr5Q>}0ChI?uYFB;X$h z{`l3qlA@nGDXla9mzj0H-))5utd5z1 zi~j(&iXVgu3Zx0&2#iID+|cYw>ZH#|Abbh|GTK{C&dSr$JXPU@C3(8P29g9pz$b#Y zfi2j&4%P`h2z_Lzv=|84e;z)l{;A(I-gOa3M?=F_SxI5w0_ApK6Y1q&`**N`96O@x zHeE@+(I!xi)LVI-5kvuoMHjqb;)6Ij7yt_}b)+-$t8f-dWl%xEz>5xSWkIiiW^#j~ za}p>Zq(=!BLXOWRz2o#ry$9R6FEVw*z_m};R>q!fJ~p#LbGG14Xjir_G;*k z!DS6IY-Br;_65MWkFzTD2PP5YM^kP{67s8Nxm|OoN^t5z_Lbq8nOax-ceGxvX9PpJ z3I~3le1*uL#}|FU6IW5>a|BM z|A?u5dhFQal)78%1a)W|x}*Q_L?|Dgka>+u@oKDe-)`twNKQ@-Z`F1QN27w}flt8= zrH*<7jbX4@c_0D10{j5XlW?=_it{Xd>>(?t9>%#FF2&{Nivo}%kh`Z1oB?9#Me;Uq zZ10^}n$2*P#!%SPIgi*N%0lf9bjU z>z?D z544+?18fN+Pm3xgWqtT>kb{IBSg1fMMdCxA)ZVtxK3_^_S@P0sQpVGC5VH#2P*2Gq zpDBaE1N=ehK5*Fz$A}6{zCaW(ich@+1(Ci!P#1_F3R!*R!nLmkWIqt|CGsC4Zx~>U ze>KQ&*#dUJTY#}-C>OYTVdxs5Pfj5l7e-u^Az7d~7?CXyY68iCXc=VJ@_Ufd#h^?h zgEqu=$N)#$z~3x5x18Zmst}eP3`M3VNT3xW!^roC7)k&APJvq8?6FI*hr;eN&JPcX zJ|t(5getR#KcaH-0DGNcgs`m`;srDmNSPDG+9HUJG?fY^H_+1%<=bi@dRxOz8;qff ziZ-7I92|^V^JkSK8kezh64AA}hTUz!6|71FE9;N!9@u{QTFAy!dyYPwGWz)Ksa4JB zFNGVO^MRftrzdj`$i$TytfxJ8+OcGhM|Neco;3;&^X3)i}#zgOdN| z*50chBs8fkQ|!wd;Mmv%$Hvze8r3qQ;^_w;x@u$VB4z~4#j{leVb8JyJhM@>)jGLT zJ-SoYO&QD9!F&@b+D7Q0t5CQBKU;>K$6qI)HhxqO;7OrzRQAN3O;_71GCS9{1IEJ6 zpV(Ff?`T~4dFf*}_Bj5U+5T8qGULI1Mu-~PrvF+`q~ley`y*)Oaw*vDS2Kyvw3pW5 zPm9eUo=Z=f$Ralt%dA`4uw(5Ln^Y)4s5}jp1=J-CSFhxXK`8C>_v(VzDw-t~Iw_Jg z0DdA{7!VRB=w1S*8r1Z_Fp)0UxdlccI9cD~?2TAj5XAyGDg;mElE+@=mTI%i0zfPP z*!ZQx#3K_HEr<~VIF-PmFv$!#o~ZG9( zTgo6}Y7-!&3SdnjTsQ%>+AGF&LBxYQ#N8L&D%V_r6-XH?h*{z3o_Vay#50`VJimUm zeVm(BL4PdsQlYwzkM!QoWlp{hjmpABA~UhuN6bK6Pp##V?Q52eDhQ02skcszJGPFq z`Cahy(=n13cP=GdjkC8_YoE90R2?YdirOfcy}arw$-`^DB*VjrOhGF88!G&%FZ~-6 z2O9gKg>~42nPlR7UDUYB5z0KYoTkgN^@nHzQf!9&6g)qDHdkD3O-!X*LF^K1lybG0 zIGbq}&p<0gecvP%MY77&U1y1i7OM#o?#b}ARB;LCNlr58@O03nYd4>mu&YTNl)l=M z`3G10+>H>Y9D@0soIJf$k<+`+mG%wmrUuf`50i|oDa@V1hNH6>&**CJOIf;P*UO(l z2k~h+N9x%xVi0f5Mm+z(e#zx(43Ew1Ez0R=k zbWu4_%W~#|qE1W5K4yaWwd%PO?@md$@D9>7o6qYj$5y8FUUE0Q{b2Gy@Zh)Jk$3YO zMQ85(Ay(V46%nylaJ%?=OG05pi0boK9JaR7#4^6?ySZW^Qlu3q49&M_kS)%lvk+<- zA{Q_xO2&%I1LlXf2pkTQ}uOy18a7Q8jRR(Ga;_V*{nl-qyu>!tA>{tO; z&&CvsqLC)_=qG)iu)R_#ZB?E#JA%bm&<^seX>u5%I^~I6Hh$TLC;TMBM!X0&ci-DDtR8Wb9}R)wKq$&qHf&(wO)L$ecGGC4VY{c{CvKj z*Tp1P!!l1VZK}lWmCSw!PDAt^`}#PUhNHU_3N{Q}XChoWHO}mBnk24|(P|jbs>F+V zHX$OW&BH@@8Q>28p54C>VX)&y+dha86DxMv4-B_HMpv$H+g9Gi?@7qUkXP3&Pz#bQ((P)=Ip*!2Fkk`3Cf=Y` zAp!w%u|u*P*#^e2HG;dxvw0x3Rah?n-w{EAa9a^w63GeYDPy#958-T3 z{`(U{fiRQ`-V8Xa1i-Q)mJslRGUUv^ITwVd2{$_B&9+QMnMo*z`27K!hyb?Is}w{HlQI;)J?1 zM*Up92WgnCfi#??IxY>&jI4~O-43!#HI%%wVS>xd#g5qq#X7UftyAko85#ifd8hNv z{8;VABiYF_W1`rw_r(jH-P=FQXRr&0tG{TjZ(BKST=_Iveo&ULeS^GsK4D|uxLAb9 z#W>YN?aoDf4hMH%n7UowmMHQEPgR2N)A;(gOCAMo%2pG0#gK>%7M&AVW*&+xqR?$~ z#hi6$CZ+L^L?((1q(!ypciwR>;Un83!FB&l6v3Kc*GO70wX(Dm2K5qjkQ^cCIZ6r7 zBDdrZbq`f~24W-FQ9GjjuF*jfNS_E&d<{k2utogO=mVa#PP(v5J+w{wAXROJ1f&G- zwm$$cQgn`C?UJGAZ3=V@kQpEhF~o%0%?@Y`M=7=Yu3|pYKvj$;qwW;t~sxJbC7-+2Qt?cQ!oqENM&Mt&u?s zP5btxQ+=mgC&P7(JL?ACwF5K5`;9Nr$u7H>GD)0pf5-pB@<*4rckjT+6Ama3$v}Q2 zC8<{UH@<ecEmJVks~%KljV)n++) zmZdzNBJNN%Eiu~E2dpmHF@{H&E-X|=!gk87_?THm+gChmt(u}72E`)St)|ZQ-cHTh z#Og+od<2#7eZ{KJk?4gUmUPk3Sypqb8<;+ zkbLgXmW1}OZuzzs2c!?DSS(tyXd2vm!tHjoAvPkDRbQa~oh`pY=|!GJ{7f-cveoE) zkZndoIi}Rk+eri~V0FxNu~^D|jW8LitE6wBtLVUI_qc4~wa1?7y$T*f-vE5VnOQj> zH=&giX+Hg{Qpy#k#6t7?dv&Rk+aI2MFM5RgYAY>{W?~mF9zu_i zzq=UJ&I6J=ki~>MvznUKzyVdtom-vQ(6*l9b-Cg47hm2gRHNu#4*GpR|Iy3cmo-;X zi$66Hp!IrKy*H;gouaWfM4?GXtk)TJ5|W(a00n_dbvK}gvgyI<2eb_Y8t~w9h3F1| zMv)RtK$XQk(^xutd3X~2?9j+ahRwMGdU9{8_>t6Ska1{ z(yAy!UDR^E6N770&L5bUcMVp2B0r+ozeZQ&@6wKrVO!lZ@c+0v6L=^W_w8paF_xsU zt7DI`#8{%#vBwy)ja^yBGT9YTPRG6{#x{crF?J!#Sd!h?T98BwLi;InTF$BSd+%pD zzyJSz-%ouqOlCAQ&2v5XeO=#+H-VyDX5L+#X>&)_0gMC`-kTE~+mBzHq>L3DNZ5F4 zv5arzSAzHWw^1%JNgwV^+|-{x!QK`Bwn%KT%Pi*vrtV8Xag-)jd3Up;$?ajv@O*I1 zI@>fMFIsPdJfJh}|EH+9v||M=4maSETW)Td`9$eV;ZOtjaG;C1B(Z%I1CAXq{x;Kf zoAzdcX2`+b*QDd_I4eT!*wOH$94inZRjqUsDMZcUeF- zKZw5;tR^se0kbWp7m3V}@T{2(0uY124kC>Zt{3al*u(!qLw?R}e(IO1qp3;Oem1oV#z0P*}zT0+ZpS#WKd*Y$o;djyHHOJGh;{KkNmtz0Ejd}K@eDC zSk^gb2f3l9^>}B}eR$r;@NUSugxt3K6sil9sV4=^@XJF#bAzlqP*aeoaCVyvZ(Wi7 zP2w1pl5R0EJhVW{%6ZaxfUAyFb(a@+fsZ=&;-r>b>w{-LyF8ew2R*Ml$$q-cK5^Ew zEyWlV&{yr$uwxDHZFTqDh`avHOl{DqNb6)#$QGF+w#Rbwns4vhJ&OO>n+5N!%8q_d z<1#iVZT0F8t2Vg3Ub}*>jdxfEi;MRa{$pt;wp-kYknAN-15wQNwdOLL9h6L|b!(1W z_Qdz{Rh0(wzD-c!9!eSTg0||A7BIeOsF1wyPY^2L!|;aR!c}}ljK;yzC;*M?@T?LX zEJNmXrnV;Pjnd8(JxTDU?N!^ir|dtwv<>QeE;le69872>v7e$!8l8K0PVVqdrOvuy znv`s;j_Y2KdZKv!$eE%tvG*m7c^7`DyZbndQrC}qoHZ<9&J~KQDua*HHfppTBbTkv zslfHNP~>ZYT(eob(%vjD*Z8(*!{i>S&_f(jX2P25z#fw)2n9Jdmxj9dlrvP3tC?N! z%WV*XJlYLo5Ud6tD^dcM4fHV>y#ODK0%00ZFt}hJSU>?~oE_r2ZRiFHfBa2E>*UF( z)6s|``lyi#Az>k*lhVsyiRcu5L!r*w6GyyL`YB| zcBRv;WsE=%#JyGe%U?MhCtCe2`rf=(q{3Vf&SCjeh!O)Meo3!ytmfFUgEkKgcePCY zarMA&*0-#VX($O}#P*-tcKaQpB33TePIFWBGKq0orYk$zWfGx!$W{Z&!-Z-q8TB`% zh)*<$t7Hl{5)tLYHs0V=@O0+pOH|;}p2h^n@SejTkt1XXn4$v?IC)SD;$pcz+GQlP z$U1rfbe+q{){C?s%(TyxL#fq`uBZj=VxRgwSsJOnSsH1+DOCG|B1W*!PG;H&di+p7 zCA6*P!yFqAp2+#&dE=gU(y3v$^?~y-{^Qj-sWdNvI8q{MV%L&5q6ykvM^oET=#HH)s4o)jKx}LywoD>v@y>ce-tGvnZ=WE5o`N#C9{Kn1Z zRpm6RF}+HF^HB!SV5P=vNMi}^44p}JkmQSIIo3KT)ktrZG`4&q zezcx6VAVq!32`jJpPSCU5b4TYvE%`QM3-Bd-s|$k7sQCuD<6gApi2>*a|I6*!giev zt9iF`IIQ~J#L3c^A9Vg9^*zZukhLOk&h6C9-U}gz0>&j@&Rm>1>zVJYeK+mojlI$L zM`X8Wtn35iTW9)JwVzKUzLB8f=Qqbic#ZP!>3IGgnOkyQYI)_spQ+1%)dQqE@r;*>d1*gVWsKn z>m!>lxzdZ1-k&C4Kz$10-|M@pvY!k&_lG05E^~dx=9;}0N%u34 z$hzaE)HbD3J6o1!??`Ji#Fmm0G0bKWRp;pvm!4$ijWzL>`!83t-~P$aOX)jk+pf`V zuhlr3pME4=BKBU%x$BLl{@HOsjL9f5M#`~0q@Xx`yI9jYkDY%O?>{(HzB1vh9z_o? zDKk)*;WW&CbW&2qUQXjBnuxzhmq)~1?65g^VcNlgr&4DskN6HPN%y9r{f63`(MD(z z$->82fK)q8&I+86wA{c;jpRl>ma~Y&acHZ_q0o>(2AKs~2X;vyR^;%2N;!Vb38k7zhLzScfYGp};Z%DdRLRSWh9>uvQ(j5JT#MYglrq$u}@s7mhk( z4{S!&d@Z0xv0{E0cI*5qj>7~iN9gAU7QrZ#+L3#JbuX@$;=$!o0vc!y20Bo*VBtyO ziM9aujjl%hN~E_+!W3$+MR-Sny_9rEL2!WloHW%uT<;c7*`8(glw8G+ou^T-vI^p% zpORm_tK4_Udizx=IDMBTbNIL?^u*2jQ@?lY=(jR*nvCp$`(f%1PVGlK zNyja*TUJt=7JU5j>>>Ty=bA4?&t?)PdGyi#m=`v|_*-Fld|<~N68xiE!=>P8CTUox zjbZy{t#bjprjpr+q=!;JDunAbWDG$4$`g=;4PI*v4GC(HmLgWiX)JNOrtq(oMn$-v z_aG9oDYHY25X^#-9mVg;O%G`|9?L)eRLGVnYd30k@c#1G?I07QB5rMb*x^TUb74M> zJ9^8{52fNf2(;wWy0MRTqE*7TmKjU8x7RrI8Mx_4iLCWs_5KN*iMcCcatEkL3k!oY zbI}pk58qq5SEA4(U;OK(GdO>(3)Y7M(lvOeJ{M4IY(4KvVREe%&QJQkYc;3TxpiBf zl`h6d0+>}j!e4MEm(;S32c}wvgI-4=Lkz_30~ikRFoiq1VJxH+&CvliER=1REWnLn zNzl?7Bx~FV8Xb0(Fw9$p^$!q3xTAGhxN~S-gMCpkaL*H9e+0ik?)U&N070Ix1P=J@ z{;N*FJ`65naP(r|K_469eEBJXSi&ar&VPX@|ErE{SrZiX%Tu!easU84Rsj*My+KW8jCfjE2`THsYo;B@4n31*>)MMpL&RS9{P0@@Fi z>9%en@6#h{it_(Hro17Wq`Ei!?3rZO&zCp!)w#itQ<`L{dUe0R%?{He{qfu9H?+S@ zUzR5+f1VlJ&wb_a^{Iwym)v(hx@PtLdgepNcAq<{)2;iHq%H(){$(}wk7?$jlEs&A zez?hZny&{g{Bvc)Q2FDg>UMYT$fu`=4J~%5a`~LpD&`}U=sC%lVWxdm!ce7()MSQ~ zErXB8lR!jxF~NL981{oy?{GU2jP0b__M+TJRV+tj!aShypd%2bN3ioMY&)n)ke=oC z?Yi&OU5J{K-f6mMy>DzwqgHy;)X)5hM@!eh?`8cqpRQTvMeDpT6^t3KlN-)oJ+1lL zTr}jC^EJ$=D`wY*rw^%3?xUI>WFu1tReRgZYtXO&-l@bB*m)){2LbjBg-PR>5 zZ6npWzE-&SEMi{A-x7czEvuW=GzF?!fz(`G;~-lC7aPb40-%<(n|z=Y0#fJT(TSM_ zqxs^f@hWiN0e+Slo;pA@nn?Ak=Tm(I^B=^QgXD=2jif>3QcD2b2^B$_JmcshkX4Y0 z(26%ePQcnnMF4H_qljjLc6CMZUG-cp;?S|9&Ey>LCB!4__Y_;i$Ak2$BP12!V(ySk zUlMkO6fhw!Fg-baZaTmdSg4L`u8x0`F7)jG{otG%XCn1s{Pe(Ce z8TzYsNuOCfT?BhW*wHS&){)L-1P<(BlJu; zaIH`)MfaW4y!>bxo@ZGigxfz(ds;cNS%TE@h*~i)iKFv0zkcd+ZYrWUwqm~ThFynd z>vZ{no7N@6!rhr^s%%RyN?c`0Bkm))Io())`RZ}ujw%O?n~2?IoRHNzv&gI3;az0q zqI|K%S3x}6bBs_@-bW3bA@i#(G6Wi!V3&I&B4559>^7Zivk2ybypOW8kLh%C>OQi; z7hwQ@kWga!dfcRpOq0Ug-0O>Xjl@mYmIeO|Q+;DDYRrGc|BhYGaAEcxqdRhaTjMDO zA6m=bmK_d$#YnpO-uh66HvoU~v`U;Qo zUX#A=*5T^ly+%`d){C2S0)=45QET{C^!%hfDV+QLbKRxicuH^0MaQ)n9G(8pE}G(H zDgKd*Qcwj?z)fBq?a zuC?Xu7rci!&zNT~^^mye8@;3EZb!jaUcGg`W;T9#7uP%2jDxx{pFR&Onws^O9(!=0 zq~yR$U6*r74RsHLRleiHF^nXv+I6s z-x{CU>A(N}v7f=?6ncpuC&Xx`)C`^xUdoW1mRvC8OYcUmUD`zLe_ejr*3R6Q=6)pb zeTrj_g$yBEHm3~>J{NtdCf(ql2GKN?*utlG zAm|mkr&e<`Ke^9+TH^eCGY=~L?;rX%-m4wT5WNpfxHa=D=E~TBlIWb`hH8I93K&~J1#5{!tn!vP=0 z34~fW$A-C4OGBG?R?NTFQwPiu8f;sTDl7{UgAW5@iDW$chBRk0ig4hd;eygDF9fhI zG;d2qOJoG`g&I2DgyLmq5w26Z|J>S=tfXrpW05m1pCWRS57_i}Ugj2> z?jA~EMQH1uj!|96=)u?sIc~eywt&mp>22n+3&M42=3)pv5KC;Yik{E}W7KYxd$V$f zG6V_N<2&VN9nM}7jH0`W=}ZQn`r=Y;T*fdj@yknm^UOE%q1E(@4^zGxQeG{^(K`|h5un|!}GykDX1SfqTnMkbfjWYzkB!nU8; zM7p76Zb&9iOVL1DZ~{J0A2muS%R)IxxfHc%x<$&@&w}fR2QqVJ{LZrW?=qAF*Fm8m zni@8#w#b9v7O9<*OyaXeWCr5fdYf$NN$G%x2E>)C6_iFnrV3sWZFm^X=r)gl#{(Jr zpx=oU!^{YL>Iha2`N+&If}rHq_?ZvM-4`FH+OECu!94P z9Av*cz+;4rsaR?ZF)Aa5X4XE<7Z2i#bs2I{O3`3m6z-$IT_BUD6t|HEfHMF}HVi?_ zf~bP+8m#BshfuvxN((k3xFPTk!6Z|Obq53h>jJ=jJrO!-lhvn$7ZJyrc2j~!E_a6? zq0B_cPFeUYR;J#b;N78?-w^S#D1X^nvr3vR*|3;by^1TM>f0$xX65x>Tm)di6&u<| zj@+J_yk7W7bks8Y{uen&M@bnxJO68-)uDqApILq{wfJg~_n~v$f54Ib3ZInu)SJ_) z|C~1Q$EbpMYIc?S9;5u;?bm_Hi|=iKYu$L*%)RMfR7)+Z zPH7A4X^qWQ&Y11wG8 zN@OmUAdz6OB(H+>=pjLE0yn)bg*-oOfygKr zOQJe8s$o^i>6t)T%`}FQ(jmlsoSln|m8K8v2Hw1$5ed47s{b`M-{AEkdcd{t5yX)v zLA97IMJ7hfkc4#81Jr@Z$*Q&`#rxsoNdr(x#7aphLJ3n>r&)#d8mU$T6F78M z3iz2XOUmU2XP40~->|1$w&56l5O+KF6|?sXQ`Dhh-22bsd>u?rg3}#A@xtuofzLT| zJjc`(r}mrXZI!Ie66d{*YsCcn&|WY|%END^VBcusYoSj^9oyJ|tkRr0hw3XLh)=@f zQwGCln|U+gJ1m^-5~!xlTmzX#pg4co%Yv3;vddpsb{NGcyP^MDJSooM zoino-`LUQ1RODf~96_T_Ed)l145*hkMXr3<^0V;2lH(?|ICCg!!Xca@wQg}zC|`;! zWkJ?FCEw%O#-X)_55={KXDA4jWvR>en-3(~+8lHb?_a-KmAey0Wz_uqm7{?^FJWTpYw4a?*axW+GXD!a{HM+~Py#=Yz=VAdBZk-t zn>B7^dEV|uJEtksUOwx8Nloq6USmr=$^23Sw=<#HYFEyo?D_C&T%jKWM-CriTk`Dp zYw`PBf*&{JwDFHgG`M#1c$=%hyP{=+c0(rL=(Y3vlCP2bY~@W>#jhv}O{1Nd3cjSo z+PxI8kzcN8)(a7Mak(DO{`phJ9_D;YB8Y|;v9GafX~%w*Up@12D=qO%v^f-`Yuy|y zPCp{3XKk=Ey=2>)lKzX}lV9SB0+)N=-9DXiF#Dc3M*~_g@ND|~t9zUuoO{ej^mtn) z(!j63r4*?})VpObr@0QMpxRL<2NiHM|Lbgq2o`i^HND8wG}9TE8)|<9GRgYm;833M zkU>V4CZ}K4Zr*cVjji6RAvroxSrTFJXAX7loHxEKf*z;-A~D~Jdyrt^AjkGoDxok; zDE&#%6-S8)CepB4zsS4c%x5#~(=;uR0h2iZR(+}>+XkAL74^X81v)QO?!bIVL@GJA zDw3{%fW7#=O-`^lC7DkE)$7&K#ic@$ad|(tn3{?sQEE>3} zbDdh1py~&Ld_K*?_yz6MmSj;;$%pb=MclNp5O)KOyH%+r{8v3F3 zy>-X!`SO^)<9)w`2FubLZ}<;8cD~?Ffm-+gpQZYlWFmFn`>u^ddF5vgL zp{ZJh$K+4MnGLs`+HH_X%9Bs;O6}74y8&vr4)|Zarb?Edb=I_u)>|vKSd?Ona*wlx zvT-U;X2lt!O$sM^lz0T3o(QT(DCC;t=h-`EFpD_q^7Fj#lWSv3P}1C_4_jDb8oST} zXOJ|$SK9s^4-gAr#OMTC0t{mhMmfo&u&*^~<5YuLQl8VcVMFXuCrENh6B`p@1D3$@ zC?~SubMP2J)B-kl;0<~#jcBFjQFR#7HMoEy2~jZn!5a%7VUG)qlRy1NeOPc%KV;dE zxIwf0m!XSHn7~uRI>a|4H-Y5KSv`Q3AheKy15oVZ;dBxSyTcACAN>=__sklWN(1;F zh92da1>$mJjk9E&{Stt(__Iog)dT^$3_RZ~orOsLJ{bXI(gfv!fYgPdQ$yQ8DpUd~ zLEx(y)qTWBDLIEhZni&R7V1v7k*SPTKGr_x$}FFz@`ks)G4wA9C_dnGT~V&L`Bz8_7P8 zn3~QByVzLra(yKdBNcKzq6ccpyVcqvRPiPk`HjT$Q4gheX2MRoKFCWk63^Rbqif;5 z`eEjSVgwGK^@0{3kw%nRoqWz6d)-2VIdn}wfr9z)DYj|je9?Ey>DbZdK;PwfZ9<(z z*4c4cxsjF0e#~M;B}-9grwwJeuK|5M6wTo!)1t~QYZAztxI#boRAY8_R>MKU1*r*v z=2oF1Ag4P*GAvp4DnZ41Z+x=q_|q0r++S_Hq%P%c8@E!T79Tv_!OP0G^f zW!HHjFC?0suTcvdxEs8IM2~!pMut10uGHCiv4HJIbde)SC2x>T!`AVLIN}|c*u@0; z9bw=UmxVlB@TS7{jfQjpqzt2-vt?kxhphdE)E6JLSg58>cg|WTOh-zIr#jbognJVj z(|A?Y{`kk@QMN#0cj&>Q_tpmmf77XXsx}>jl}OZ*?sKyESfc#IIxELm|L|xg!&pmG{@(*-VuO9-oPF-q2yI5Gk^5`QIc9 zf3%SG7X7~cV}5>|;bBzuBYs?0#8}=)Ef?CyD!4;T*_8S#RaKuD4~*}pfjXG&tiSEE zqo>P5-n4&f)GTFO%x+V7yvEEWv`?AP#LJ~t=~H&Tc~ucIPg@)g$a9RJ0pdNy0SLc7 zmfC-l$W#hZg~aV3&)dl*UrvhSvoxAeM0=6qDIPB33bQkbR5y3|lr}?ondLau+) zr-4C>P^umg%5&9`<~yjc#+RdESrFdjc2|)?{v>h@rEAVd~5xj#56JDij*0LqB~5hjM_MJu>ON+S()XVAMf)BW9r-fNSo6<)@_e>k zzmQMW()ZEfl-|uy162Yg!=pdIcW5ENLL|8}IQ(_f*i_kW#iH4gywVxrzy*37OyPoX38wm4rV_L+Q_YGA!mOoFK3=ScBIZDzc$!@ zubbo7P1QDelVocSTqOgCJqdlkV& z%?p#=3B5d6VtU})gU`|Q&*zRjF;IMF9kt8LE;VetR&Vj8*;;?VoORWv-!HsT-!>aO zYqn&+{C2tN#bu3e`wVv4&$XYrDb<8}ThM#5<~ zIdH1MsG0mEQae{PdC)o$ooIkrlx}Q!w3|0LqEbXE#v^^r*`@9Z@41MZtMQ{PX8n44 zt`2pVWsAU`!~vu=Y_JAY+3w9%MbpGlBXxOss_3L<2aO>kRmPQmpGkF*Kn04ry~$0g zNnT;@ypw}8Oa)GWp$Pkg8$tha1BV5(-F}2>+(Wgq_6Fo0V&Vp#4|FB+`w~3i{2K!F zP+9;22|P(CSjKt#z#A$s{*dD&m{gF+JU^JKS!br;M24GKTyo|M(e9Nm38m7n5@g6d zq@q%~iZx(>s~-dWr~$E#BIzCUGvylZj;M(Cqukhcxk?@auHx7>DTbtvokb)8tsu#- zq0}CyX3x!B7M1YU$Y>QfU_t-kr5z%#=a(s8kvi*)({Da;^xFEh zW|`8rZ{lJuQFsXH%=EuTLBHu+TV#yy#tn8`YE`M>>N zo-F?md--(GsRvL0+zq5(#S)u6;n&Ty!m>EL-jEJbpIj%#I&2R~YIJ0W*gPiD*$b}_ zzC7JC@m35CjYQL~q_bPAh#U%U|9Vy;)`RI2@cDwH(?BrKwCj2oR)bAvm-;YvbB484 z)2N1_(M>uhbaOWh&^e%3@Ud8fOZEIHe;F}U*pnbqs5>O+bopaNteDU(G_W?;z)vql zB&_G&7pt!wD9RXhwD~5F**_AO>WUgkRj6H#+>P^bBOTcDEup;W$E}BF2BfBjkEr)L zKT>4W;)3{ zTb>d%IhBu*%8EZc%JG&0rhI}-zYL!+X6WCTN_;>?ll=w(X>$0L7y~ zt|#Hij}gW~8}PbB%Ifh896&WtID;@2g1v1QM{^O@jx(>q$|?yBt%fXkFO&_igwh0p z_Jd9VcU0SIFk(kCBf88@@KEETB4?j6A~3Nw;3RBS6$+JeHnRBUOl73P`Qp`=qkNTeq8!Ulc)X z{$d*|=dn+A;AJx@5kcCo=pN|hbqvY6A)}eTwqUGg&VfzH1Wi=WIjS-+imhkVGsLO^ z$c3%XRwA;aZ_<^|OWF7F@eqjvx%FI_n|4_Weh{vMpqNq>vuMDsHd6jD43O5z%nab{ z6y3MOhdX#-r;<6A%Js;){R~6*XnbCXgM@WYnuNPURPA_*tYczCQc9nt`qiVW{4Er* z5DI-$~3PJ5}tWagXSW;lhiH-h7s_T*i4Hq|csl;a@j6BXDZ}rs~3I z=#4cSccFK!>z4k@s*xP`L%%8;SOf&SY{_`clSrZ&s@GLWij*f_GPxxuHR;AiREo1I z4Q*CEJ3}C6P~24)=Y(v9EEB1e_`E48vN2V7ROJVDrG43l&ypm(AeBHu$#^j-)0|P6 zG?9V576k*-9Aj*Rj9|XZu~YK?>P8`Xhk~}vdlt^haaC~m7ZX#Lxz{j1B)_-p*uN>t z;MH+KXYbracz&@0iw_>GU38Hd@P4j0{gq)g{qU$}ojN4S$N~cmHrmB}WC6Wd&ty)h zL7xY#tu*#y_{Z{_%>*;_Nu~X`?=b`#Sds`!UbCIt*SJHCZxDCC5OzfSfn zkY?w+eOs|Zzh(S#pXPGbl5^Ztsx~F}$?5zS{lY7}DEo|q0n^`O#Aq_F+_fUe@9DA1 zr;eKEcZP(M)gHcy+DbedR3-Y1c4zkU?-!Y$uY@Kr+m40^Mn#mCh36bUDctbhl5|pf z?pHrizFTwE69p4EYaQRGqc-+uDG!EfAEYdL37IJ!e=I#x{QZlx zZu1LpDSCu>I7k}b6$^3Ke4n^bfF=!^sq-d2uvF~!Qm;@fl*6@mGvwFaL`0v@>a zS@6E~?vzz&w!ym_1LT+eUUMSbA{@@@Q%};g@_58JSd9=z2C0d) zY0?q8;}Z3QNkgGI@_6^`<6@C=DMBp?%X_%Pauoi#ic?gpi>ew{focj7)7nWt5!%>n#*K>UShIzV6ltm- zPCyfF>POOWiv$pY0&gVt!9XHVgvbf};1QpbHQo<}junkcYCcf=#~KnCKXK_waauue zc8&T%qlgI`awUw4`BBJPl_86{QBY7wrvh4n4s8fPh^Y>eFbd+A=_B@dSIs#xF>_u% zB_22<6=YOCBvLw|5@U~?v4YKm*k>;Z=d#EUNjMsX>}XQL6ag58{TTVR!c$U>&%&%? zXu(b?qO`!CLKtZe-%wdYi?9Dfw| zB7|N+hNW83uy}q}zFyYLJ^KHI!U_E$qg9WtKs~J)>wn7 z&njR*u0Z}CGI9EMA&0nxV#wgjL`>X$y5OlH!M()l{Li`$z9F6X*a!A(Fivv&x!Qwj zNKiUp?G?L2oRL?b_;PeVVrx{QPHtfp9 zzz53-cGO#w8z=T3WM(F1X7ZJnhN6m{0d(oi#|f}$IH<;xkRxaqJSK|+gIFiex4anq?(Wb{FQer^nT$-)!pfuAbH=r|O3ENR8h^pLgT? zylwR=&;|&{H#jIfzLVV1F_UbMRAeSI_s(|1E=`e;Ux-o7qJ(Dd&lh=BhoSR^W_kH6 zv1hJWW{Ria6e^7f8*y{5Crbx~8o`rAK~B=dQNoRh$a9VKzv7=MhYA100wJ{s zMj&$>q2&OYxnsb#-3*EaGHU=mM&IBLMo;3rbVptIsL`bZpj2JK zcvdDd9ZFG2NPyBv0Q@LpD2d)#>DGW9lCt_$q?FsvFPD?Xwy)O%}St zE@^_-(8{J>yGfGgOm#=SJGYb>>)&+7HlHw&HA{{X8T=skp(rgZOGTW4IjANOcaTg- zrMjEuPA`hw|Kn+!0+(gnxU6x8g6RHj)e(v$oh66N3}_M#@|CU;RS^o?R+nY(H$G(6 zy5d*xJty*%%~V~);~QlM7G>*O)~)yni;>7Yt#se7YWE%BjQ+c`??Q4t9l!A)cnOXli0m>7;zaDUG0v~(%a8Kyn zuHr|Z7t@@q18$s?|Lt4%cumgXtcA}bZIPsK7tV6KewRpx@+s=fktD*8I^6@}R}6mK zJ?3`w!QM=zNk)^D5?NG{S4dExJ6shlU^*r1f?=$pUAnLRcmFlX-}6h6*O`{B%!{W+78X}kQN$0|eqVBPJLH!cEg0NA zwK=kmsxSqJGoU$L|77uR|u2Ngdq}cgQSyN{N#k^Hf{GlZ6TTn?Xt$(ml7s>3yLUkHkJt;*-ffU zP4@)QG;|3Q7iHc^c=-r|6C0`_0EF1SqUwZHMRckUK?5Ur%%LumCIX)fH6eOw5iKJ1 zQxE}(2r9ClU|6+2US>a=sZn$1CT2Ye8N4=Nt^(7O!u+WEgiH?!fWC01&N`~KX2L$h z8@)J@0$g4;Sl1vB-CYQ|YPDz-omI@K)UM~2sVrcY03#zwa;~|Ych>=Lamo9uoWm2C zO&%Px_%ubiPpt^GhF7jSS!CMIRLt!snKEu=Da;+>Bk+ea^ee*Zf5-N%EjWx-zrr5f z$SpSB=_$J;)%dGLU*RU_?Noi?o`md_qiVXL(qH&>-yO;H*R|*+M3Nrdk)cLnHrPX2 z287bQ^Wg!OPm#qLp+hKqPTo!pnQHZ`P8p+aLr~G$*IMQF<`JgdECLxiRu;{nx$R?7 zQA3>NCHUs3T}fvNEoRLQK95QsVg)xU`GuH+QNL@M`19VAzL^p^C>gQs&t5wC{pq(> z;rcdr#?5JC9j)(1e7&opkp+GJ9dsDm3{nl_b zUHJT^d!E}FmW#uj@(v!(XA5ciwqphNA`+iaU*P?Ux_K=7H4JY#E8`+%IF5E27JW@t zDWlt%8l%mbT_e)+!D@qQB?QhM8MVCajII%dZC%?%!Nv%Dl2n2!Cqw@P?A87U)cn6P$@xF`ksTW;7x`Pr z*y+FfxBq9$hO}X4_R507fl(9>{9G*KHnOr~-5>H3Y$;yOMz@aU6V~h?l!*1G=+@Bqvg$A&&>@J=y}hRXmz0tI%xOY*zeg zq5sC^wwTeLtNy11|N5|Tr!{kPulRqNERXp5J2(NHefdtBwchuQN4OjgVA12rQF7O6L6*j z&N(=f@gyM3&QNSeOS163PD@@Z?`o?*(q(DEE|zI$I=|0f=j-RJG+MhBdtcP9JvXAk zcGE`RE<8%?-LLw;YR2^iZ{|jn*G=v4^1G?_DeR~>ow1|(*V|n@rf$V&9P{ez0pqK8 zl@;35ouxMo0qIk@VI=U{!y~;wDsLTp#l|>qbyLSlXW6I1$F)b9S*AWECKfn_aVAE< z(QRQJAwS3x>q!QFrlhLZj@8cpI1b$6xP<+Cw3y+9^`1shL;GCB*c0dO3#CPUCAGfL%+ z2W17xHK@&2hu=fMMgkBTy_hXU8a{_w2%{JU4O+D$dWG~>nX;2z0`>)_9_-a< z85_v35dsc~EKW$Jaq4EMG&u0|lB{*UQ&C^E|zG%8V$KB$&0v7sI3 z^v`2i!!x+{v7`9Ulht`-{N})Hxp%6&Ex~1G=FWk~`bHDuc?T&+q{=fRZ)oe|Nv>OJ zD4G!6>TMofw{ndknsMlwx>pi~?*-*r>`n6DLR_cCkKPukUVpR|?o)&F?Va_W3bxrk z;+OE)hdI8vEDvQ+;*#v@@+!JY?R+=$WMU{;#mjP*LcWhqx~*#Zjf{t$Dx7>>ST)

^Zc2s*Hh_PpQ1EKR{gIWB< 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
&v+$WUU-{*=v}F3^cAqHewCcNUi`Hx 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%0AGK5VIp75bNpN{)@@wAAYKC$!TcHiq)yh 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 literal 0 HcmV?d00001 diff --git a/img/back2.jpg b/img/back2.jpg new file mode 100755 index 0000000000000000000000000000000000000000..d68f5b0c3550f3ef3f93b6237e3bf868f910f3cd GIT binary patch literal 628846 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 z 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%0A$O`|L4bd`cH}97nk|45JyAO5A56&}%f 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%0Dt-0r9TEPK6{Hlup z(cw#aE)eElb5##rKdlAYcX#|DZmafQZXYo#Q5ldZz!Lvw4NSa=-n!AkmxaZ3qlC}A zM{l|+F4`@0wJQqC);(($pSZrP#{UHzvtU z@%1H5pxj)@JT*-77jCm(I@Ip+GQ{~H9Avt;2sWr1rg_U{w+OnY`{4?OP$1Dy`R0Z& z%HkRe^N4CGXkMqnzm24^#iB7Usm&Mi3M_R3w`ekJ;l^FPVm*nIQ&1B0*YGIk6a%R! zcKpLQA6zD+uVHZ`BCTqs`t6g=y8Vi!`%c?;oEsQkmty!69|v-r1j7uu`>bGFKPT~i z4P*Pz*y+;L0`Y!bGm>7FZ;%2z>SV_i1H!5#Pr?A18j5vzlo#-F=o-$BaAI&g@U5 zy^`Nj)o0z%$buhx__Z{AXZf>Re)*n$Tmos5kvNwTwe9-aG>Uxc{}|86lIajvg?nd7 z`#1luE{p}jL;pGNH!S~N0ak2~`650{8$(z`4Nti#h?f8F^lQa%oI-3+aaC{xUG7UD z;1{Gb|AHR&1xuQTE6Olq)Pr9501!7eM|%K)|7}sK$A*VL{0FLUx&SKP~k5ZL*6ZS9AS z3w;uHbC-U#>K6-3GkVXUF=RKbH)2UC!L9nD6lch1H|=H$Uklcxn<@ z(SPpWTnX+n{8p)D=b2Kg8!MM%uREz>k?ni~QOTh2Wy~3eecQb%S5A7IEK@yuyuUTS z`_34fsT3!l}S@a-_2u;kJj5l93GU z-RL5`_0IxDS8+4N@a<+7_|aB@bKGoBKZ+6@wvvKd@l>(bJ%sge$Kh8KlSPHB@9~tZ zh$*4rM&beUy1I7gXJ!X!k!5=;&z$awoa3?HIevC2m#qG72H7k#|TqG<}^C~q+Hrtk9}ztuw49CY(Q?r7{b9`QOGp^ zctjY$Yt6`-DCD%BGS%|0Ta}vC<`TRaYBjZJWDkkvCnKV?WaBNbo z9aXXJCt}~wS8s2cgEY;X;27!HX(}ny_Es{=9mDvYf7f;TQJ8gnXe5O`%R`v9r70FE z+Zl&VLCVctbqd&Vb(4G~NR>sw*BK=V?=vJSfAWY_y|eSqy?9i0{8Z9-@prI?0}PSm zso`q2P0Ys{YZg&9rR3QKu0*VsXk*V-A?NI~hKPyM^>4Pd&n}QRu8VhuJ9({c(r#0h zwi*gU^`cLrYcEB*NLM_s7nZ$#)r()@0YM?V!an_6++a-Y=B$2y*E^a$hmQGKVi9~x zK&cGy6Y{1q`H8}jaBEvKt%#v77l2~4)Upb(b2VJ zwQkrS4fB1i(^y%u5NC4TpUz!eM`pwexh_sE=s*wLc9N7>?!%T=nIPBWCD5&A@#MTi zOM&BNHFN4n`eUuWVa!5vuT!anj&XW|Ltu(#H|->}XGq{u!z7<`3mQq-62fa2NTOCD zQn0#2eJCc+XbQ+zK!`%VL!y&$SAUM#Mt3CK9hZ-^<CLozC6?hFN4nkNq1 z&M>|13zF0F@fsYui>ee?hhby98HE~+LRdtA+mD%w&>F)*kVBq`;>u6X)iWvUe3o`- zZt+TyXla~7qO)$yuy@Kxp+Lc3g@|SHck>G= zbb}n7KZC?A`s)_c?F7vqY;K}UC!Qb&sQs@R@X(mHsn>t|-pLTFI2jqC_;-r?=NVS% zShCF#0|hsqpkIgat$KzyhQ2(*KbO=;@&Dg+%AR3yI2F z-(Oclm``YPG~vd#lq}ZMUC= zEKA{2%Ghk+oD@5JZRp`?s6(G+t7Q5UWh|PuyuC`P>zb6RAx}zeC zWY5S1FlGc~Q0o)Z+FxI2K&lll;)+wo`)=TbH7{iiL0lzg+NK>0)n;6XY*FZu`{RP% zH}&d9&vlfd#keRv8`LjX67L#n&giwGU8ur%N~&T98kWael_*HN!tmr5@vvoCr=`^E z@28_M|B78c%9F}uxzj3RFoRBzyr#v-e9TKWq&0R7(iHi2_+Ud3mde zuoM^fj=a@NcEDb1rDHyewCOtO?h%BL->0uee-iZN@wyzn??@6Kz3qCBRr_{M+D)qQ z=!c4>=8KLUcVw=7yQVNacis-yGM8I=uCA+QRw4gtpZ|2rdjY}F1B)lGCb-HKl0$ye z;zu`fT*ci8PV-vC@FPudh8-e)L*MlQ9muGd)i7>*L~l*4zC+T)BF#xgC#PpppmvGI z00>7iMF$51@pf#$t3@!Iv0!d>87u0ZPbF-n^TN|cg1c?7N88nL>9#BDO;}_Iy_K1a z&GcW9sgbxBmgN-*%IlXyHr}d$VLv74+x9{KMTYZ)cvzR`^_>1&4@btsKcBjL_3=?` zMUUyTc-z8^LGfm#F=*n2*FKR;*A&<<&Ry*IGNAl4(|Z z*O{NP^q$)&4|wSgPXVFicd*R+pOIcFR>0@d3VaO4b^^hWAnFDfjhVB+BiT&)?(3fX z0?N#$dY>~MZH_uL8-oibKjq1o6g=L>14JNk>w&SvzcshF-i?IuHeo(RwaNgB+Ru_N zP=#?glV$y>hz#2c@R&}G$FK=`%V)MY?V@L*?p13maqIf%px9{Nz+$=IFxksNv-^4* zHxT*t2TFWp;ZC)c^W_&KgAP8F{D7;+mM*!uijS~bTY zXURbCnP&gkU)km=X&UUC4Qn3FUJROxC~>J;RXLK9s%PIrbZm`3$&iqO6x)}TE98-2 zAM2T$WuaAajykW?ODK+ag+!B_uY8(H0^7jf03w#pu0qLxF9OEYH5_NPq76AwVK6YB zGOR`-#;{EzZ42b5VD(qK0>tw$rl=@rEu{THuDInMRk|6+C!{QK|8Nl)=ZZrW5w5_# zE!>dAj!zxq*Oq0JCt&!?2EJFGZxA`pd!KN`z@-fK=Bmu@Ir&#wDbG=*BbI{K6iZz* z{xIR$Bv%TWbi;oX@FRAMb7-d?BLuJeXG0AQ81I=3n zk1P>1uH91dN?{0E6ud0xI*moG5@{-n@+ftBurFv1c<`su_1*PR5_#^?!{rgFP@U^C zWW-*kJCIy$lc+ja(H68d>$21mA&_N@fj;Wu62gCt`lKAHrx=5-)D%LpEWgZ>wVKX=I(Q1cA)o-tEo^nSIM;P4On|-Qo)*C(X!XzQ(VXE=^mFiuz=XIw? z9Gfk|7ueD7K@$So-OdS@s*3_jl@|g<{rBTpwoWI^CnKblh+cwc;=86vTUfOiQNHTN zg+4vXSxwKeA!UIGm@W}>NFnefVO$<7Ft91-Vrr*ufqLqHUYHHAVUudz0LJ^uC}yK# zhc>N<NteCaE`}g8) z?>6)9U4kA<56A zZf++Nk1{G%KdwvYU?l5Uo+_N-O=c>e!*%#|#gC}X#SwdgV^mE9+>Kiw0DUPGMYP91 z(|4aZ%7PEG0w@F-=7Vi0PDmi8v_rJtX^m>&Q~KbP-@#4+n|&@!sC+0j>js8xAST9meBQ3~w7!e;5|Dc+|32n7K^l}BJ~ z+ST(!Li4+TOkcP@ZG-K`25F;!pj*>HPIfD8CqeS_vNN^6yGTyW2y{;y4rGUCSWf>_ zV6iO*(T|Aq`UBCf)8kFqZNS^%4X1nQ3eu}7PM+)fS`zrgoPt5+UzW%>hx6x50C}IK9z3;Bn1__aJl|fK&mAF_e*n~`_q*9lm3vzZJ zyXlS5jPKzTX^@Na_jNq@uOUo^zX7=ICy;5pe@)EOK!P8)Ll(#?6&hdq5bes7-+eg;A6odMWbNS@z%dA5)7r9#U<*)PmfZb z3qCd{z{_lXr9oy}GpJWRp$Z2;LP8I_f)69@Ie2_SwYQxTo1L-wgIb@f7l8*nkc7&0E{=P|9ApuYfZLJ!9DwicO=MTgUM$ z(PX8=iX5bhyR8DZ3cYMp_lJOu$U|BbnWOGkBV#GilDw`hn3t7u-jujQ7I`XxR#xX< z+?fj=mk+e=#O1kd*QN{|pH$UZfYZ0F)b*}SDZ7BF6;Y2rF-M)8Cu)s%B=n*1afTop zMxl-Uaw-Z;=Wg396*voAn~%iT>pR(HC%-{lOHQhpJ5O|b``M3S5qWKL z$nT-zgTLslzjP1WPaK=d9GRiytrJt}v8nf`atn=oV^S3Sk>eha78Sbd9`e$mg#Bfd zw|r#s#L~0zL$OtM9Lr1|Pao})?Sj4c&YHJQi496egN)6K% zIpKUa^?I`V_KP>~NHX^HwhTXhFba8@(62~wy87!~#`cMa7D<-Im!)s{|Dbc<*9^q+ z`lYSR&2(3mR!#f*K_bv^j3c&AZG+4G04u?bZ15^ehS2VChRaoDssa;8>J zE%n!fN5jAWp90wcV)=OpCkVvXhV8|1DC~79>7Np&z?@!YA5yc zYm5Mkp;#39Kg#;U+XX*Cdop4l7D)yB!coe`x8V{=B6RbnAHJ9QQBNJ;(pO90ZhcV9v$-wC$kDkkGN44)U%( z-mAJp@1&VJ=QOaFx*4mPXLB!|jpv4kHCti9?(iXpQn0!Tv0bHRDcknUwLIZP@%>m&!twqcGW2)rbeS%f~@3HAl-^$j&8<9t6uAopLN!<5z-B zP2o_uQ|YCyg~p)u)%+)jZ+aVWlR(dPNnA6St>xhGvTk8vWq}M@ZhgzQ8y+}-dL(bk@U~vy5>{bj7?kLoh;Uz$Q4513+$6C z6T3zxrGs6YHnPkL^1?PnicIpO+WCr=L+-v9Vew5-FHXB6@}I#shU+mdkw!Wp17 zWBFEIK5!-@A9U}yqW?;0n&A}F_ddc+%N2_ElnWk>02j$>xUtg$%+ogr)|5{g(ADh0 z!6Q!wy9RO*zO6u~xTkqZ+#ypOpqa7REyYeFASR-i1GG^J+vy0%|P+;k~{nN9cg z&>K-ns-4457VXl75>AS|&?>g&jxMa;KX*`A_B6xKo^?M3T39q45u}Y6`|$Nrmr3;a z)cVFk+q-^HfR$rCD~GgU1rDaBl|rWN6s70bw$nmQh#7vAr?=E}*-pw*ISbAJ;b;qM z+t&**RErtw)=ZJ!Y(vR##rC@H>CzMG^&^KATzQ=|xMKuocAQJNAp)R=IBCo)m4(;7#Ur7spMS`OOV{ zpedw?cp;HxFHBWzF3Owpfc*DMg&)ATY2_miuLk!D&2XD&?UZU@B7TDgp||pKJFdFc zkZWYP-7K*n-b6Xl_EES}XFz-QdM z-AaC%F;V=)t(aTXj8e!%u>{bwx^ z@Vj2Wm`wQbYG*ZTce5}RSuXU|Ex#fM@#UHp?77a&{p(@B&cCoaH^zzJi11fkDKk3R zIqqt@RGr-;Uoxv!pkr`eSKK_A7PGO*Rqvn%wn{;v*G88zWL(>qp+XiWHKU8p+c*S( z6*pS51TCV63p)7`l_C1|%7SAmOD>YS5?qy(WkjfAlv2|&4AoS_u^_(c4adl^l=Lqd zj~2y|B=dvd@**k?S<^FhL%y7e1NEaui?mA3Evej0-O~4n8v>9FGb_gi65=E13 zY(taVG%`Pe{$Yh*^t0mfQJwWfCS>ML(UAA&U;oeu|0}kA@2PIH+TF7WWt#(?-H!sU z$~B!;^(-tjjyJR8)l7S)&6NlD)*kzF#kND18pEj%QE)pt zO;0nnz%t8E2=}hQ@*m_eh;2H|9%!m4lwx;J6o4dLAl?W8P+tmxBeq%mj@qI+rXB*q zkHKN=O#!ty7KBV-9_i!FwUYC~04M_QlCMsOa)J499Ux>Pe^M=SathLppEmrLu5~zF zwxfSgqw0D7z1?TVyJe0}*Y5BA8$KvLy{IG~RIclAQ&EF{^erIx6};Eqs<&K}_vPa{ zG?cJ?gxb`_g-#~6BUlYembHRgO0?{ILdf7Sebn+_^0bS`Ts3Wfq?QY?h0h&xSehll4d;ySIfQQFmcE?Q7jDeGHh;~d z_T`mvbnFeA`i&^DSo=b_NkY`ZZ;`Q%6MpmJg4nR!r7zcCA+S#Uy0E`R+HSs5`_yYN zUN)(eI8j6%FCt%}aoeu_BpMzbg+-*`k;w?KBIK91X$Brbf6RLmnRS~e#uA)mJpEMV z(`2R~rb1A}hAk~nyZmt^m`(Kqt4IB=H4w*bQW7;1yh%I`{D>a8VCvPo6&ms7Q}`Ra(z_YFg8jr5y#%UcDj{tQ&W(kgr?4_Ni&w zJSDxz>MO9Hzbj~#Q1O|qcvZ)KuFB-WUV(j`(QMw9T{X$bG>kw^C5=h_*OC8AM48P0 zVwwNP4?y)vCj4jf$F0BjfpS)#}@7a7^u4 zt|r>s`^F>Dk*U+aD9Ex488-lV)T8mC6YpZm^O2o@i`V~CdNtHP-aYYk=dSL-*2mYX ztfh1Dw{5@epTc^&fzs=$?DxvWI(@3>XQ%czc5_Z)tOl1#+C(xKr%v^FsD@@T1VQ8j zz=vQaLj3rY4yjCfu+k25Fz_ndUey7{bD-O*oh#s@LyvB!>LCsoa*n7}Vx~X%^eFC? zpD<66VWQ5@^mKZ#&W(b$k1%_l{jeUQ^J9}{JVz^em^8vGaXC*qf{WW=2=Z8eNnl74TP9-SyN^2!LZ=MR7&sB6_dW$Tja$b6jVdeMj%1^#fy~ zNp&f~*DF>6D*HZGR<8tZ^g!xtL16hLB-~jSD`?CZEtQ3m6!f8wi5Ls-NtN=@TnPJ# zo>m4&i=3|>&II$ZK0OJCxY4XI-)__oeU0w3l#WZ{SfwThO_w2jF}6k?6V%^;9ju=9Gh3qrEeYR+4YqWqEq7-N1i>-Kip^}?^2)NRi0?bn}`^J@p8pMpN=MzK7RQk z=Zd6gJ4=OjYCcCKltLJ71(tm91lw?nYVVnPVFF|u!Ggio1~p)#ABgd6kD#xC0eh>_ z_!gM~a#OfK9{Ic>4Am%L%qjQ;I&x*fA70@ZSY`QarjxtgRNR|BNxE#k)0Vx+smlj* z#Tz`+xpV}?$yH}oPel= z%7+6#Jbb130!-&pFNPApQz=pvI~extipD&a4PeXHx%!Fn58;uWd@jS-jAt0h?LG9c zOu@YZ%K-rHQg+*Ry?rvVOj>go&mLC~`8r;$we9-&aK;S|IObclx@K=Ek%djw<#l?4 z3M}}bIl|y4+fa2?{FlM_MK`c#RDX5Q;NCnG{;4;?L9@%Llm-U(_4&i4ceBm*C?{%5 zX%dex3{TDqlSg&Ctl~xCPhxAE!kQG4NpheEYxT!7H$bE~0%WsdsYF5ZCe&^gWSuk$ zf%p}3hJ+BZaSqpARCPs{D)KPA0tdfn8&)+icp`ysa6+a|hl5H*sMl&yEpVy`_r7?J zfSa+lkk1SZ2VT19f5!b|A-t@+pPj}0PVpAYRCRR3DT7K>`ey)_lM4cFC zPRjG!5XtDxS(GGESovbEpw=!MR#SSK%kvV;X=65q;Udm#Aic*R$m;}O_dhq`SQb{t zh{vaix2@qQ)tY)B+yVOs|3Vwz_S~fA`lrzvDF(e$AQqbK>MW&!sIBW)5OpD^84G-; zd0D0Dn9)Y51Y4jBu`ACU&BMWrfSJ$(y`1Q5nYXgZ(Mwv4OzzN3O{!yMe8_jER}Xtb z&;n{a6-NWAna9R$&C>8QXPf7i2LgP5@<%_@IFB?jt@{x$yG$(Yng3vso7&91bo`Y= zPWkTq?^eeyHWhNNp5C4`8`NvC&uedr4Ukr7+@@E*7hkph(LJZs_a>$XK^7a~O3T@M ze;rFobjwmPUnTY>VvE-M~ec^&ysYs?tdLYn7IW( z(MsM00?Iz`*q#t!3%k=Z{!4C@8{2uzw48YeSLHE#Is^0fJ&4;{s+xrJ^LnkqI)dU# zp7~TV7W2JlWuqX?zi~R#jyRfn1gR+u+j*=l5>55TOSRJSxW@Y~8N$FM)t>UecoiZm zFzzAHYJ5B_+^~8(wU7!|g>$)eD#O*&GwQjEqYx;?!cRv>S46xxlpqf9DL!ynGZY`VeZ>u?wO-b5$- zdT#HUC@sy6E31c&CIp0rM%4pPLl#k59wf%9{38Pk)s=9CX3A<^3HdbcWa}DQl;14m zf}_cL+gG*^#^N08xQ(o!T|L>7fLfjc<@242v*T^e+_^36e*bBkar2vDG80vZ%f}P` zn4NfaL4k?7`MU9&?d_yvdHdm!lBddz$`AG?`ks>8c$|=!^@krKw1`jrQyZs@e=%4# zP*Jdl9lgya^4nV)d&+2<0>#Vk|KFp)aBv?U!VxSb|EF#LryeVd7dQma?cwA9d!+&b zWB0t+dTP2dvVx!w!F*Nx2yBg@$p8~H&}2U+H?`kWs^Pdly0^IaxQ72})Ub(wh#1+V$9*Cw5wzkt7+F{s2Z z!?@`eN(j2Ad*Sk3E!`Alxko|&kFxg+YdUSaex(Yb3}6zf3?;NsB%ub-p_dSl7D@<6 z2_;gbiKsY3s1k~_KtfXxLX$|Z>L3sxG!;~uj&wyu0d)qZJ^RA@-tTd|pZ2rAsK}v* z82-7=Yn^NTR!>~7J0=!trdUD;l{uyOlY;D9qJ8}^(S~u z&)M@ohJof~ca!inY_S(Yn$Lq}-L83{MOcA!*@OQfpg5l4;nsf62Hn^iD}lPpf*$qgSar+El}$uc2k>B-J5+&@leWSQGX`y;UAEj6$54nS z>hefIvBFcS6TTiiTEfIwB>WWaUODNr3n^)MukpbU3AZGmh(m17#h3Zjl42VUPTPb$%LMXZ!C5t&dl%ZEN#rsIT$?qxxuh-+&L2E z=7*@;4C^eEs+mst#tEc-iCps~Y$sR}ZJkSB7N)Qjn*NyFDXQJ7$-V@ElZ4a&c0rv8 z?*|nZ{~9NWN01Bad-j^w(iL4uX%er|L#@?}aCL#isajud{>>;xpU}_fgU<%O{GKZ& z|4vsK-?%IEL)C-FB0OP%spq-wrww;69uB(et@@vLmpOB;U$)2p{q|V2O+p}Qaz)^2`?o zvdH7@WhmHe0(UgKsP-FJ(Mw8Aa-vgkt#b|PjChm{V@LNz*0b^%>*_44eBh(+jA+xJ zb@wa$T0K6(+2L6ugcf}Iy#OjUeT9?h=>0U1OTh4rmO!Kzj2J;7>|}uL99f_3xd4&j zr{jH`Y|ZP**tPEZ>S|dVO(m{E6$NDJMqynIM~&sp#xZ5_lXOA+3nm0R2YRC4Cv9A8 zR7Yz>HQmIYYF`01dyt{2VS5P!=bDE9**%yIdqJ~Uf%EmtysjSq;=^Ai51!ttj)<+6 zqf`gRUdLy?*Xw@D*^d80DvkYI@iDqri*dGjUN1Jwx1Lz3~1BcQkGoill=kO z`x?oqS=!lgN?DPQROsGuL!3}7s1XQCm0ZuvN6Mi*XAhf8waNe`;%D`6>Us;A8g<+l z)g`KNf#}^=32t2kKdP)$#)V>GFL%H}0Ju&HI~(#oHo&sY*qau@rr3;K!fl14*J~czzF~9a@=ZduA|ca{_MAd@KE}-hI(=?A|~A=k#>{cw(??hF9yJ z!>$(?vm$CbZ$oQ8nMCUuLCZUNOK}_k>1r+IPtCf_>7LN|%1+CNLQR8)ngWBm@(;!E zu7g1Zu2_o?l!BcPIt^wpuyfHSOuh zm$2c~wxIN3J#b4k!P~P{(2OYlVb|WyO$?T|`j^8lp z_Hq5P!37Ua%@VSPhabjYTI#zu@Twzu;r{D5t%$1GTc=H4btGM&e`>}qn7kG>vTG`y zynmtiRF#gv$M@TAq3`2$^=WXac5`A?6a7m>xkbzs%t;vsxeX;ES*1v1Pk%x&AI=A| z1r)bPMIqhhv2(y&Kd@Qw64{lOAp{&R_yzk&0RnHIQs5yaosM&K2hJ}`!>Z*GH$6#2RsM0C#o#$>nb1zx(&}4Q zpa|*R@6Z#4<)@v)`ab@ntd}a*f3LE-f#1Xy4na5f8bi#Tw4M0sQD}#ShlE6V@KQ^hn*sb3= zZb@nQRc9Zu(MINOdhN4!=%NWtx3m+avgz%?kY~bJRhju@7B00718x2$1q(}dxtaSE z&;U{~#1Lz}E)ToD$<2aWd9&^c9D`IssM$KlkoqqF=$2Nrq4lQ`LeUG#NmM-9dW51u zFKa)Fm|5$)+Y@i{X@=z-dfVr^Wc$u?t#eIbOXbTbOir;`kmZPaZ~w_h0nR`sBTq{b zuW#$`_Z73rTish^;%K$nNmb~dzkNoj2R$RM$}N*lRIVkuAfimDz4M?~#E4)2WtPzmvkh|!zY z6A~{;P2&K&YEy;~()Enx5TZc)K#qs_YLBX8UqDx^X{KRMSAEQ~MI8Ilp>PQYo^pnG z@6+=u4ws{XO!ZAaGYB_OjClQWtAYoTGyY+`dV`UAtdIYkj9&>)uoCMy`tixXvEPKo z@<4fb@b6L4epfF3o@IVyH%)lVsJJnE4W8pPEHC8K*l{rR7t8u{#oxv|Pq(Yuua~jH zHbnU?X{Mp(BD{BAZBZI)=peUbRlHHkf9C%W_N!0W$B5dE0}GY^qm_Z^<9|;K&=LFq z@@yCu3gqDbxL*PSNZcQN48g26G+_i*oXz)f!j-;&r~iFiYaKpz@Po09yDtEL2=43c zUS43?4-mC1K;I|}#2-nphCrGq<1smw7)5SFbrbo8uz)AweAv83@e05X1%XtrH21Ws z*!Yx2NyT00@^8Z9m9OX)rWE6Ct*HuuhSHWP6k1YKs>iRvVuDisalRoW75;S>K2_+7 zdv>v7{?Xie@4!(Qc58GVqn%juAwlNK)8B}5=@;d4H!ct4To=)i=ep$7k;V+~n33+# za}+S9R=y0a8i@ST^^%dl4x9;K zi`f*v`E`dT_S_P!F)_Kt&#*8|J*bcyuQrlMD)#<_@053GfO}6^XP#tQ75hMP6#W^A zXRT`{+ELwsF(Z04%l*nTlrfmill-vN-PS*XVtZ!g!W`=s1P@$g9HKLfX0}gTF|u`7 zq2t>9ket%&cJ0C*B7~bey02i0$yUfoix!k-REgw|? ztUodFc{lfFD>~oWzZhFTdNLg|e|x@)S?(z>)D^IIM%T_Za@qXB&&_9051?x3)91{` zb?k`QG27#3_(wxR*b`$7(!M!}hXb1<{Ox!{D2Er`ZguRM%*Fc{PC)OjM`s}mNa7Q5 z9iT8V8~YLR60o-a^K@AUS_Q)clcY*o6ac+8FE_+yBE{+BTwSQ4GRd z%?a|L?_=qQs|Wx5%6oDJqfPqwsA|c(5zSl-eQ;L5%Fr9yXn&oQf>-E%qY~~n&)o{3 z;2%-=b61g8fav~0bAe0KQFIZ1ZMP69Daj+_G)OlF+_5&dfji&W1cWfMtw+Y{veX7%YX28N@${Rjc|zEdK|t@xCT?zJ{I*BHaE7~-&%T6^>n zrNqeQ{UbvBgzyVT@~!KUkIEjAgNt+$O{62`z1-VFdRqFV=L1DsG)Sc%DuiP%FF~$D zwn;o`=v=1%e1rf~s~qSZb&6XUi6yAw4eGP=#_P=TLtJ=69A)JQhNVQJk0aSdj(9kc9p8D4SsCtR$)r8Os`etr#9ct zIkRA5e(|Y|M=@3RbFQ>MbDU$>%|aS_nOx(d`7MfxipuWK{`5)~V^) z7R7ul3;r>NYSAK$?fD?7T~X9)94-Q+o z%A&2UsCJiXd7wGZ?dZW&PqbHj&Vp&4c*6rtw(OUfR!Z8X6b+*+yMiSOK(2--z}nVm zv_tTf&?Li}kv@@$1>{4K5Sx5K6!y~vR1R*^0o58Cu3`3< zm%;`|vlz}$tbljic^%V+bV^smQ8#HJqEM6L-K6JwhTZ}q6O3p8p?a(qT!Ln1kE(=e zRVjkV1T6CUI!k{S7-?FDSGxv<)Z8Bl*%)Di3IOTz<|bo0q2zPxz>w{)b)iK!@xr~M3?-+>F-VgZ^ento6?vALf5?&irz z5>%N-xS5f$)dQ|f@Th}{c1>j*TAfSA5IV`=MZBK%-|tFmckf~D6u!>wMv*A zK~oAba(!_1#+PZ4EE`4lY-w5i>MC|m^AR`x3Qo}fM=qi8!yhk(=n1D{{L_$%Rkd!H zgelbJVBr^&DsTxnYaaGSNjZAZRmR5W!w(nNN(yOP^MYA;txUDB)}u?a+vkuoYi}^G zuY~wk)$hv7Uy6P9;0bJzr$D5V)nFO$DcE>hRs9*g!b~RoiCMGP; zq<2vHQP30FtYV3)d<&0)&da71OI+bwz>QTK{h*4NGfjz?cZm(jgCb4>s;q0?nfe$- ze9%cH(kLf3XXK=WamjR;%4nhLL$jpf(dH&#?8niJSA(Nsfgx>N)|Yrn)?ej=yfr7g zs0~Ys%4h|?H3);S)??mhM|5ykxtya!8sfO90pI`fXS;DQ9rlg!i>j=K(42n=|Gj`aZR=O`QTi)AdHn2OR(IEa5MjNHl`MSkRnIZ_ zPz4wE__B1ca;mSqzdhYAd9xVzZJM4XV{>9$M??iOt||qCOY4r-RH#Zx{_g=|AeEA+ zRRtb^|C}oSjRSOpOYZSAl6YJ|@L%PviRf9zMWKVtfHz1?4)W+8e=52zz%hk|Z2JC0 zeP8xdyD467eXwKQ?!Ol3mLcPMAm8!%pY1?4^7D3G3xb89?eWrZr+IE!)8kBLozDG8 z!N>so2+Y5>PlCHq8h&wK#o5xPL(!x+2NkA<6n6%McT~Na4$96J7_oiVrTHrTP9~2` zsp&y|{w<7f({pW_0BD)tX0~3N)F;jc`!aUEdS9@+dfxso#4X|VK`Xc4U*B%Ho_ITA zr_n`^HdFNar9%ug)D!-K8KLL&B5Yzx(OKEx!0>&8rq@P65BxR1VNN)RXGu|tg@;eq z@u(^-DrH^)9_mHzYz#6KS|Jb$^;MPTD;ee_5syP5c>rFLhnK{UE37ST->F*Uq0f#BpYg$mZL!RoSe7_`Y3JbVyupNg`lCa zXQ|^`#VXy1u?@A1N)}A`1A^v| zL~YGNxT7p7~G|^xjJVQfGUCAM182ecRtK zc6+3I^q$kf%%@b}-YrSV*+*gdmHCOwn8^Lp^(UWHlbj7OePjM=-tw-=Mwk+zyAxxq zHc5%d=&vN9?85xxP8O}A-=!5E+wT@xg0wE`KhoT_RSig*5+0rvd9PmfRMI9mbtcgM zA~7I7^PlH2Itxay7;af$YL!udE;1lYiM(kyhGs~ra-C~;W@MKPI_djYZB1Qk!_>Z< z`S_O_tZabnx|f^WS^OM3wNrIlREp`=&ByO5_VH<5acK>%>1=E_Gk?+L?o={Ou?-{~ zFQP37c-CQ2^P2g@vac!~X4k|~ye`In0@%>|^-YuUE&=!>7-7-!o*IHB1xtbrwd?@u z<&uI}Lx#}PRSl0bsiiN!jM2!(;A!>(Yyg@|pEhz|J`6{0Qj@w9Bvw;!~A{Vb{f zXWHB_deX`F zXxPBusIQZg_eBbW?a4rqoVbG^4ruSIOG+pE4$E!jQKF&uFo-@mO{WDIQ5GuJv;fzh zhxz{JqMN`La@I<%=4H7O)irlOx8h<)i~$?ZuXP;caYtlvWq47F1du6#;C>b~)GVxp z_Y&g1DXq#UdCDE$lxY#7Er218+@FD8W}bH)-TqZKp`5NJk-!SrGBu73l4<#9OE%zs z-BV(An@H%6FW$v@YQO(#7y6^ZtYK{JZmrAxd0}6PiVRlBP@j(_VM8Y;2VwekipSM4 z1d1_|cN$HO<+DUkbF~WGv_-Y8HS+c?7lmPiYT5<5 z>lQ#2XyRk668UnM*N{h8SIB&5>s1=wXm9;1f zdmccl_Q`9G%gt+x{`VJWv{^jdVZvW;yVY^t_KlvtcVEu^DbNhC?f6Bn=jhct-MuVd z^nTROA3|sE+vUA@ue#q#*eI5JpHR@?SnA0}TrA&SRvZII-6QXznpg9R=+U}AMCdbU zY@KmeN%f6cn_6-LT?UaENtnzbrE%9Sx_Go5Pwf%uiuT_)yapK_vmjXupa`wXARB*t zzWHWoix+iYQu;1L9JhR1ZcsG2Fe)C>(4JUF@A^juV@oIcy#SmQ7nsk$)opH2Gl9PcF+5M`^fzCEjN<5kFSdTjIM)V$q%^N zz#_B1Eg4>Fvie#o?k`M2_Flie?23w>J6a8}Fy0(RoOZ2}WWGBWmPAc`t!LBnH1%V` z^1S50)n8`TUf`GSV$OzzE~wSbe@JIbHt+m7jN|fT#UFKz>EQOVqovPw??oS_nK|?0 z^s851fA_dyP-te`OeOElhqe`- zzUfycRz|^TRvHf$2wyTsZC66SI4wQ=Y* z_q#Kti?rvP&np)<(pVh*A}?0RsBwons4aggLzSMz8I+iYP^dX_1wk&m#lk}WTf7+n zY>50{YggcF12&ykpa!^)`a?1Vrp=@k5*PL+8E-eRH>NfWHhVn1JLHx)$(%wIl)l`;kl5CTep?hk+}ZaL703Alw$+AO%! zs^wny5Ekq#by0~(Knu=L2GziI!Q}2q+GvTyJt#>#qmL!))XLP1NIe8`mNvy0V4%$$ zLp~spvpr671-YU^+#-rX_+5?U$?RW=V-;D21uI{tF#~^RSB9w<=OM5?o>o2(%AUIn zQI=1sEc3B_eL~57Ty_p{dyZemHEw`S>#*tPWTtny3g!Cig-2X@xv>f%{eMi|2Uj zS>|@r8N?yeP)=3#P%2f~1p4ihi3ydz7(-|34YyOl^u zeDc=tSX@AGLtY{0gT&qkwVdyPc~}s$?LGBB7h==I0W;}FOwG@{_lbO-ouDKA&o#@9 z9*yuIm%dfU)M7w3IGt}u<2WBe#1$w7q&H^Jo)%dWNkUpAd)lnE@Y zExxLiscz>~uqLP2VqYHKg`9EhB+Q#Wly3WSHAk)2uk&)Q$*T;b?0BT&XrUL=d-(Z^ zQUSoMghCxA&|J}4B}9;+b(u!*Z-W4rDRbJ}8-WyfCNoehUtgtdsa>fH@+ zr=XS(z=7umy~!>PhQeO3p-ey*s+X#35%x9(2_t(&7hx@cOI9@;WCAcDh?uhADt`Q8 zFI^#MoS>IqTbo9G%e=C}`c{tCJS(Y?j;RG@(Yv&3Yo-0nY4VFb*Pv@nhqp2>9z^@G zN+Q3U^}8w&U%e~fea%Uc`J?(9JjuRyo|VwrtD3WFKGpVNiXDARFS` zHBll~8>a<%5GbD^`?tms@Mkj+(%{ z4fCQfGbFdjVI{fgPW@a}hYHJqv;@yqiIXEBGK~q76~cFg=|K_zhW1_e@MRiuYEhQ2j)jZnts$_bb!^H5MB%$RstYXO3>D7N zg{^}J6X6?h%3f(RE}JvM778hkYEa^j=-j-rYzF^!T4J(9e@3)L^L1h&Rm+f#2fGiNowmBwdeBy^@TN~3o^d0 ziFYsxVs$e8NSmgP=bNdLBfZ2^Sj9$Lb7aBdWamuxayCs{6Y)iKLLB$$5uc@N>1RH>tl-Bhw zV4hNmaGa0iQ~$aH+#n!tGa^whV2Fu;x_L4@7<_SwZkr)pK?;NtQ#3A=?M6~u1WJ7Si@nv9Ft$mUlN20g<$*uI3mFZ_<9-C^i zv5%RT>OPzo=aqZ$Ucc5kQN$tVMb`xbv*e$(-gf&{3q6{T*m=L~RbCXpQ!e-R+v)0H z&dP{c+?TM#YeNB$o{MoiP15=coT8PBDRT|FVU&f;P~b87$9JqtUmpS`@X>gErg+G@fAE(n-H`0Yb620g8$?`=HSnudLZ0DY_?v^+ zEDefpefG}U|AJR5`l@Y9i$3kdx7n=o4P0-#Wm3IWb~y%3ZwwFbyziLFc{T05ouIVp z>{;aeZMyFy+(sw9jK3&7x<4R7JrolVfkbQ;c=#q41bIk-J22n>7RT00^_4oNYJ>gk z_kC?QMH@_uUc!Qr<)EJv{B8gt@mM?*tbhJ}Fa(j(UYxr#4~*-WpmvPQ^_7>ZLw+6N z{P(Z$Tsw9lw=@-c_HBfJr!<34cs{ww)(*-z4oq13Wq5HF;14@uK2RVnDfOKc&)dm> zGy=EL~swvw&3T`Zo#3Fm zyV!=$RvRxl#T(c0W1jslUclOb1`*%4G=$ERMaD5ZFpZ4 zwbzvbUC<(v+hV=EU=gMI#}u|j<6|%0138yQQR{Z~zDytms>b|2ebu%{|m)s&}g`h2V`!!;Ch3a$#NZZbCZ-mBS+FS;3d z6T4l8R+dy1cVOulZ+j?g2al8W%PCe1dhNEA6Fa-f*V@j0xs;`7-PKazembH;YPLXs z@~67TD=O>1P5;jct$XE}IQ~wG$NL&S&BDqy~B!!|I}6|L|W3jaC&$a=a*L`M4C zFUd*Skuuhr?;jmL({7)uLA((BT)em2V?yeQvbmL`)428_-L(uowIqV^GRWk`&-35`fcS~ zq!k4jkSrq;0IB6Au6dcHVfa9}jaOnxsvyVBG{mN8Z;3=omDMKldv>j*X!zkeGeqWP zyhu%=W3&O_mu@M_b-^~sb+Pk$_)s@3$el_W5UqG6;gKH!pgOB3Iw7>O@gT{leB(~? z$FFWnn}2zn3;$y0Uh(P-PixXYf2#yZ1oF-e9P?zPR};%^xD97O*? zAjNGs^T3|42)%dAn8@#IzO$ml@pHCrR&n-p%gfyqAiK~J-K$P?x zUJwxEx+14VgJ~9O4cRDdk&H%_*ZjtUXC}v_oht`f5qf2p@5(;RnsSX(#ym{8ZSgV5 z@2tN#`^4|F*L=$!4jaaP$hl5`8U%yPl%8OdVQ(N^ca!m#$rMjdy5~#1-=#pQ-U6Z} z^AP3%Apu4lq^+_@j-R)dYaOkY(A4QAAkeKUt-z8EYT45yHsv9I>vQRY1ax&RT7sXD zugQ6eZC^*z!Lpa^>z<8w0bkEYL9ZN^rGs$hOwY6#rA@C&)L-j|M;E(YdzGbc2yS7n zo;c@IxOmX|;I+}kvFe$Idh;iiA9uWy{@yBHVI~;A4z*<5VUn_>?ql@T4K3)dk&e2x zi4*X^kT?zZU{J^g@rywdM!4+V8zP0vxTui71qdpui+i@xF;NgZAr&-=1Ya+62X9gL`c}A``&J|dp<2e4mUAF;I^6Zl zxV|tHTCR+&fhUbKEJr3q2^8c_lWAfK47~brFj%8BVOXs<|u3H=NO3v zg{tLOv6zfXW1rX+rOe-3u-jSgo%gFx&;50TX~MJ=#_8JTJh?v;L7e}aV@prUHHi}o0@4^7j~to#lmEXhq|Q}5>+Tt-+IQKr)v-vUmUK0)n#0;pfI54)W$p7LStbW z1)X^)CiJ1LR_Zoktscpz4h)AKI$EhL?&h0u8eKtvjJpb`=8Z!okwFaZ3FW(|J5&nn zY^x3=v{N)*O)I&@F<^xa|n zh-ws33O0p&978b@gt}6D6kn}KFtXyoS~5^q!W zQrZlr{t_Z$zoL7z@oWGsqvBwhtE`z9KNs$#gy3E7V%TwuO|J=Q8A&QN3Ode<{Q5X}#l1%MZ(hfmjH zI#f2&mLQg!UZeq*V0Y7SH`QHE14)rA)zY)%iUnCkaoL0k5OE%2dHS@}k^~91O74I; zuix$F98A(`($jr@sYK9tseKU}9rZ=^)YVphXz!fD)+^^~m%1AdJ$7m5FW)5f7~x73 zwnd4FTkBPNQ9pT*YFHOL1NfE#pWyy*`f`eWJ}2^d*fZ|kwhsbf%fB#SI}_V?4XIV> z@pfv4e&ly9%|#z|*Z@63uqtU)Ct_1lwVc`3V8Nul2T$7|_H7UV&m zT{&eLTLfaDF7}(UR(@sa;*NuZ&fil|g};4hw=7B9>mTP;VNF*bl&5359xt)pIxU_4 zZ1%!z+Fs?iw6_(NGLl!btmLkjm0$e#V!d_P&dhd2=U+>k&&Pk9JBW&y{z2m>am+Jv zLV$^r^S!dNivu}lFUH?!80M*Y9;)*8THXi-GcW>4IC>kYvp?e;g*xd~(0;wvDgGx} zN}L8-swq}_lOb-C**k}Knc6KjH&;Pv+ zZUq5V&oQLrE=3bed!9QKeE{I`0}~$To^^*Y!N;#rPDbEkHu*W5JP_byLT?2hI?cm9 zUZV(?QH01SD*1AZrZ(oG5HF3wOE=Hot7($XDs}c(e<*9+x#<--_rAmZ!CcVww^Qd26|}=Z#(`Vm?ew%HhDjO8k(lH{ADvVF=o%jIRZWT+K{{)Tw=Vt z-W-XxC~#L6VLqpm(sZ+ear8)tO;Zum9Asoc85DYvTp)xYHR-oilJ(zLYC5grhXSk; z*l^HyVZ+)uUJAp&A$HI0eNZN-m4Bl}hIaMHggAM@0K7^AOoG5c+%h!8jsbLaU4kSR z(J+Gqo8$yw(TX$&rb8uQD>8cLrsxq zQ+FHAr`Il=6rAsrdY-$xksc5-#0)!o5PTXf<_-1{YpGKi;TX`(!~9TovsGKEuQIVT zwNm-C>dHp3)UzlGt_M{go?Db*GS5Xr*VlIdPYVmwf!42eJVt=iUDL*>NWeF}{$5^v z3`W<*Yb)1dF(fWW!DC`_+DDr=mkUXaiw&~;4OyD#dGRln_zNblrLR^$mzA9U`J%tp($k%( zw|}nLtpAyESMEvn)mQfjAFm8MrWNM>mVl91Ck4mn_j|{Z%6c49vt>6&;Cn50KqJqG zx-En2iv~rTLKMVHOElh`wXTb4Sf6MwD&(&L1}Ba4e%2I#QXz2;^`j09dx znQ1xiykV8sJOJ3|ma(C)J7pCjb9Cn}=o-={%6Pu^`uU73Be4EKQKO+PBO{APMH0Anp&AoCd|3(;+N~(;T#QNx zXa$Z1DJ0J-Aa=$Abdhy)^DZSD&!+C@!0xB0s;f=$#8^0#u?N&ur$&<8hf8m=4LkES zdR>Bg`gAlShSx`rF-Ed%p_uyM;KO)YJ@)!wXi=@K*1}PQ)KVu`i3i1 z(v2bG6$f5Vr9LTm=3>j`q1MbHyc>O9b#G3C%;r0f5PK*p6c#-`H)%Y z_?U?;YA@}Z6Yy@bN-f~lu#426VG`99&&TG#He?(V#%OK2ZjyUr0O7-sA=ugiPg}0X zL`S{rX|!Qiwgryr`t-gsz&^w22I3JVMav`Ka`x_|#G&%~I*W(@UTSqgr&av+WZsuj zq5SqA!q*9(?Aa+tYNd2pal}FwFHe=~ty3+}Nmq3m;{5Ux2EyGNIhw(&rS_ZQDxz^? z5H}fNmJOd;e{v1eMx$VzKoy+zBuw)A{SK053J57H2g}L|0)tI)i|%XpUbKWvtB2=m zc#jCUwdJaNgM-I%&U6j56KqghnLyx;fPnQATU!nv3X7%zk2uqI$qk^c%ET$!?vVRD ze4RodO5$PNG*Pr9#u$-zRoV{DLC!~UlnsuyKX)a8Y@P~p%C++M7Uka({VR%}M%6(0 z$!|5UI~kfl4u0js2;{_HRiTb~GD+eVd9_JVv}I_cookV0?Zd>00@|ooGUO$|y0kW8 zyJbnWE}8i)-RO}4ZUa7B53J>qq%f9t(})8TXhYXdoD`?1Uo~-8gTf5-IbtbPksdEx z<&|M*>&6DC{opFJ_s}0I(aP~>=(;*76)s4)^k>8hhDXM#d;hg2plRNatcktjr9@-L zaChCs%F&ut^$IkEDsJrYsfS{}PH_cgXJA2VNGg)S+XOqY*$R1YN)kePY!LHW0^Ff@ z0|8i7fl=!b!rN(6cX_7(?Q$w2{BRx>rWwg)WPc<)j<&uVC=wgxcMvnNbD8ee(8DAN z-}O z_42g_n9}A_w>MshP2GDiVZ&3tJ*boMB(qUIlDRM~@cgi;{O@wK?8Q^(Tfe>8N*=8) zWD#5u+agK%4XB2?*svOB#V=g@)8z114)0D$JsAEeJhzshChu2GXWt*YwuOFHCQsnL zOt zf~lNeIgjMSFDLz-7>8Za3Hr~n41a9Anokpm56QBg^tmgF`@wZVp74D8jcCb9zY2V$ z&hJOngWFHw$jgkl74Zip?FKXX>hrBrnt^|2I3INkf9OPMj?UzxKg4CKT!+6kG_L#5 z!OnTrKzu@pLGw}*J}_c*5UEbcd2|r*D(@qkDGFKL6FSA?;}W=_Dg`wqjc|7g!HlF8 zQo$fM_rDtC*8g9@%@0`ebl6@|=aU5^AR5OTS}>*QFtt(=ftlP7=i}Z)olh7l2cudv zomsI#P4XLutzFhttzc>%4|(&@`K05mZc7_szKH^cJYS)WyjEaj1GCyi`tksM`8cO> z?4|$@=7Uc&Y5G{oP@7J42n@t5mg2R&~r$I-;O)I&IpkfjugQqwYE~gOzWr0Vu z_BEha^x~r6eZm44Z~{I1$0Dewcm^0*pRti)h|)bxkKxu;{BwYk^8?c7eW@B3lB2|f zfS^HcSONG|&MnPiT(NKRctuVjrf1rx@9L>{@edws`B3$h`7%xhV55E? z?2&YaS{Gz2w<38JGG`CDx$$vT@ygP1`=v$+6_vANe)&qH-2#7Zj|e6XTt!`V5{+!n zNsJW;dOp2q(^HO?o6hyhASYL;fO_nWNJswsJc`fpRb^n1bJx@y(qc_k7;@=|n&$hXS?+8SLL z`{!nQlh8;=JFT`tO|>~p!43YBe#$Y8^w`FT01_1K-QID01q3IMoG9ZVT0&OQLx3b^ zNcAh0t%;GElN46u^N?$U^Tl9Dq)hq(S|Li&p;JjK)uQP}=FHfwiZapjQeO-pKTVm; zC|;eN-#ofLleHfCE@A7-?k`5U7k`S}j=4I0&-rt5F{Loi|0~nhW^`g)^^Jm@TT0*~ zhtvwuBZbsnVDR}aN8?8#A&sIoNKQ(#wy1fXvKYWvSgVf}!NAOnZKltegYhIMT#`h=$WU!MlQ-u;tT z4wg9U?qhnUJ`oqre`#m8eDK;tx}FXW+Jrs@yn}U$1{`4Sxkd_3?5eZNhq)FCf`~xP z5O{l@ESV^x9oI>XBg8uB;Gy!{_dE*5g7O6~9;##-MH|G`gIWXemf`$1NL)mbfV(O~ z#h0+2F^SS3I-kfa@mP1u%_Gw$>X7YBYZbc!jvFjEGFwovyT1lHeYI2gzBAxQFKmlE zyMBqLT65tpNAmVQ94e=Td$My>0)Oo?yJoxoXPs1)SO-;0JuIicTmsUg~#ols7 zLaxFAD1ZhL?w63|a@8zk1~e5GDi*jKOUN*J2k5Om{Kx@%O$XSJVSS}}VK|ouL8C3g zeAMxsLVI(2GN6;F8(|9Jt73U&{7-A09V)!*FUNu#Ns<(J{l#9Rd?UC4&SE4-p&_HV zshH1W0?>H5sLkr^Y`j#vza-r7Q3+c_t^mAC-FMHCXe|bvHQ?HW2VLH-n#B?78N)J@ zebTDDwc_MLZBlFD;kIZTJqN2gxO#2-u2$4krluX?Le?Q@=d6X!m+={0s|8}spOiqU z^MXNose&hsXJTr1@3`%F88};Gz0jY_tQ(Y|TaQPJBtlr;AWgwnN{_0`P>o&+&~k9O z%sNu-5c{Z+!`%tj-lRE|K?>^J;VR)obx(h~n`D4uQQ?pk;T07g>_G69@%42F5#akP zG9edJ7G%8S6cj_y0g8Buex!>yR(Bp@4jW^f(afZs7eU306+!lN-SA$K`mDg4f|`&8 z5yiF=j$7|o-`stnLPf}A_e9BkAzU`W#;!m^;4mxSyi;H+SG*vyL23{r36br$&nq~p zf6VyxSMiz?TbCm+5AIIO+~>Pk`D8wDT}R%*{p~H(d*+ouzs~#5$cI*}%GRy&`Yo#N%heaJx)IxdK*9%^@=CS71MGb1k zb5Q=@gM8w7^*9#xW%&lq5X4XXy3g)|CaHw6d8c_(Z!_D@hfse-fK~ugm7TNHDx&CatYtb94kAFiaVV1c= zD(}{0%!v;{05(_EQ2`qr=dLc$9OB@9nZupI`{9wZy>-aN_Kra8EfmpA%j=tzM4gvP zxTpJyf)D>nvSkxQ7?g{|bxbZ8o+vT>T*|AykI^LH&7G}jo=R!TWVbU#TzOq`d3$rI zpMA$qLXv2(G?G2jym|iOYIpb?y_s=AnASY3nfa7yqnqJ*b3s|fn$|e^n8ZBQ&mqk1 zofyK8(Q5*v@IE$Z6}*tUvV*Ir;2SmjuH>IFI7Yhp=>x$S!g^O;?=55{<3#0SSiFo| zBo{hgCSg)VHGyse6Lr_n<1)z5leeKM`HNa9sII<8c{_&m{62NpsQP)3)io$*-(<(a z)r~F~Y~T3%o4D(**n7sm+hdAEsMd%kpiOX6|V zv)Kx(d+4v2TiSYsryT@GcO2l&LP@E6<+n{9#VTwg0M3csgEaJCMURs`> zzNRa8_5GYsp5L4P={|BwtXiWAqWY-fQOF3)xO0lE|7D?Rnr<;xIY+Kzkw%T@H1z)} zX1D;5w4cp{I3sDxS=D|;YA}rz7Fkw9`de7W2U4AX)$-mN^@}%@ zhzWxhk)|8VWGplb&G+JF#r)P0L^qZQWr7?g36C)E~W-^PqqBN;})#JUG}@6 z#x$hHNG+4WrjR^vth7<*y=(rvL-nJcrVh}7w{S%w)W=6H z2cEahX{TGNe_3YSHvc%S|A&y&J3c+yq+cmnPgda1&`wms(dKRSY0FjGqF8snSOK)9 zD9q7;O}3czmObI3raI%))&a2&42g1wL_PmLMzN&s+#ky97bcTg-k?9By}o`CeCZx+ zqL-|%x0gsq4`?hmZpqZR#C)v&P}}P> zCOx3*SI#W^qZ$gaIwRG9)K>YK7t9GjfNam;a|y6&w@RX2C$}9B5zB~TeM`Nope+gWPK{g z8DJw*5|uB5eIo__36r4c7%@f^^CInvz?qglWb)$=h0;+;1eRHZ^VTgH@~ZqEom;L5*&{($ z&_NTZUH6d4mGGl}m(Bej_0PgBcB`WEwy)#9BtP6{U^87@ z@BjO^ON3)2w$=&~qr@(4?HZwkn4v~u&lsi0>9L|_D>UC>9;;{oL zguFf}Dj)9Wnr|ggRlgFdQNHlT@PpUWuaj;lgW_f9+~5C{4hvxttvUGz)lbCVY{RZs zkA3A7w-0!WC~n-TrB@+8&crqh#bYe&&WT$)hRnFxa~rE`O7*Dr`A&?I9X7<52m{O1 z|J-$5O?&Eup^iXq2R=X#kB79HI$Hm32&$x;V>GcEY9ObVAOm)fc@zAJhT}jIastw= zu=9@7+Z9-kUBYc9Xzs=1*aOGMdQ^cxRRF^7Ccp$X<54VeGg&qD<@d(3>3P9apL3i0 z0&W}9T}NJlxAf`N?(bad@t16mNI}yL<|5*|pA>IH5C?txOwrUcjy~!4^S_yO^VeTf zK*3}a!ypt{0jqW)Y~m}GDNP0F2uMFzIe?(ZG9Q}(Ggo14vsK~1jS!n417l|8(b>Ez z<8DDA&FTW~&P|+gP9gteg53b0xx#=kS0gE=0=Ra0$&pvJL7o8+RPMoaOubmy-NHRXAZ9zVkBW`r`HE)jFW9tUgvQUdm*7YV9zwH=0V zG>s#`hgfX2>BAV#VoRgGarFBNbI1igPydi{vo3&*7Y*&w*fNnKxg+GJNI)&1AzUUP zA=cBO4u1Bwxo{O^b}2(t4rga-3OKG>c~$&z@DUZ-!-^v%nFVEO1crjD33Y55_7FkU z!$%!OxAIE6^W<=wX`5R(r%=a$NjVoour6o35ry@-e6oTR_HJw zoIPS9h0~#iea{m8BiNJjHsxC*m)-Jp`mC$&0&^X!Z; z>ZFGmx{(vIp^1)~5f+r)oC!9uV~$n!q$iZOaQG#t9RK1_XKIMgozM*}?GYVOFW+3J zhn9IkZQ9LBAybY4#3_2Wgl+rM2ioyx*4&w0sBCfJyUQO+P1nHZD;F{f#%77Oj5`-H z+Bd@tv`RdG?Eb3zWD@re>ZRGKndUDd|EV~4{`yZdclYK0LH$~DYUb|s4-1psp+_ly zkJLmJ|Gb;Wbwi=WxITkqBP>}ZgaE|eps^*=ZMn_b4y*P#$QNR^PMCwN`dzTKiK3Rq zMOS1lfj*DCJC#noGk5$55^H6H8P2xZjw$>}rxl)KN<*b9{z)>2J%@ILcrPDyj++Ej zK(W6otcI4~8Spf~*-tOjPRqW+b?co+$JDDYk0W4iCs z|CGJ{O&&D;8#-4Non{o=$qy>czZ^&gBz4BIVc{FiL309F9_okdlGubb|SX?F`D2v+L`B%My)8s!oba& z8zYpWmn$8@Ds|-tqx^q>&!J*2l znj0}oQQ=n44{j5Gf9cx!lgeRjWhAlw+k>jYnRh>-*7CPSoFx0wKSU6_{`k`&!Tx1k zsEEUyl&|6I8iVd@g)2tsh`3vZ?-{UZd`dIK6TdsES3g(A2trC9vw&y3)p$|SkakmF z{sHi|2%64&)&T>2q@oSvML@NL4QPzk@9q5|_SA6PZ&$yGagF`d?N6 zozf+&-s3mWESysZIPtqZoadm7LQzKybdBf`RVX2vX zLjh&|2v<$Rx9{rO8h+=*W;CY$_!ZhdHJGr7Hjmf5`HohTlz8B)(@dpBy3<_w?per% zMKX>EJm(aaUk^Jkyy^MixM(rZ!(SQm+Q{#u>cG#}pDfP5C}kJO74n@*4f<96M7rz* zK=3R7lU05r>h7+TZLGtN-|7z7mkbKdDTzzY2w%508%-rxFFKHkFlbT})}YJz*vX z#V&V7eVwtlx6pEF4P5=iQf|z@z7|v_Xho^ecMl?2-X`jSQ-}oUS-u95Gv8)I5_<-U zaj|-!jKz;-+7^%i&M%_8ur`KmTU94FcqfI!k9orZZp8)23y(Ud8NLD+2v?wwrZ=S9 z()~!Ri(fYM5)X~ngp1x>)Omp|stYW>e=B19XOZ0d^9t__4bl}p-Ko}%+Xhz$Obxs- zgr+VdtmjG6UatEr1c4$myZP<9`04HdS*LS?7x8Rd^aMO0OSt3s2#Fis)I{Z|j02l# z=i^ldGI`K+P~UlX<8)DmTQO;Ko#$ye01)}{!aYGj{8h~u1w--A|_h3QCbU z{aq(P&%(2x`7JS0I$dRJ4)h(yYc1_e`=m|ups~N$@`ub58O3%3M{1wq(NQnUI@m`; zK*%e~s!0AQ67Ozc_R3sQT~ut->7v=Q_%!4zlj;nwt$of#?PqrfB_GCrT@A30sTk4S z(@jb$G*m#s*fv$lS?=T6xlhK39-0(K#+AKTwWclwpmIzM2*U-QZ-#iDdMJPp{Lu?j zE!M%h$I*S@Dv?F-T)<}>2Ror$xCJP$?T3K7)-9AWo9m;%zS3p`yW5-rd8Y5x3TiXK zS-RtxlfO*^bE0lj0-}Ck-O}4Vvl}vv?%EKm_D4ig3A$l28+r>{yQ4-!1!twMDOTQ% zNPUh=s(to&=1pCBUd~yvspgzT$%6`+gk1i=dw$pl^F|RJ0|7eN zNy?@|fUiZ@pM`|ZLhqpGasWGsFH(J-{5jL5~xxld$vIf)|5q3Jh7y-KeRQvCCc3r zmfZVd^2A-mr`!{tPi;z=6S0{h*lo0*)zxUu62QRV0wKnn%v`+sAbyQMOr?qoc3aD%LK&8$f(7TJNd8Ct3^koQ(i!bt zcJ}D5DHWKvinEfWd>S{y-E2`$$1D#OX4tsS6B~ar^sx&?s5X+m+s&<4cxDe>t|uwe z!0=Gv=$9Bz3x(?4Hca76xHfq3F&*9{si2e7gY85;G~(3m^k0{c24@(aHr+LeDby}@ zH6}6^cL`Lp;`plMT$n<730>Ql-V!1$T)#O?lGA8>lgA2YWkE+mb4kLj$rC(j)g~up zKAx6%)vjP6xPwEI27@du0=Ng1YR!R`pjNtFO9c6sQ$>b z{r=)R=^pnpwukh&wXMSG3GF?5iYC zmVWqdHKk}NLRnZ>PQLs*`UF}e!PpJ;`BIh1^rNsP~OC6V&H(LKU z`h3mB-I*HOvx4VSRXu;By2tX}%+WaN)39$$wpZjHJ*po*kWz&I)e7#79k@Md=y-z; z#mYUd;#aTctnPI_iN!v0Fz(T+C@#^ED9F4%sB7HKuPQ66t1f^dy2^)ytbhd^*wP}I z!FEQ4iyiCo3}HWTxC2ejT^9P@x(Hx4<1X}G3i5_}>W+gKay&d?xGj?b#GF(>>DNQ! z&urpw@Zl*{<$%wv6+ig?^DIJ(o-SNdtgf`Dvn2_UGAEJ@!ARmqA@`M zeSp=>AlSz~3KKW%L$B_7U3VVqje&Ndy{-!p+~!S#E6N6H@9*{Ku(=J{Y=V&Tm^Wh1 z?;y9(>k(%p%6}$SSYE4?ASa^~=>$;2J}eEDuxbyTQZrIN0*yIU=#U zk8r~~5n65dGDUI#8OOzTCWBh!lknaha3OVS0bS_5kNu^rL=UUPL@3a3_t+W}Id|7f zn)l5gqj7kJw}uxr*1Ix#_r3XR&frhqdgna#P3rl+Px{}pey`QHTuNRlutW|Itw%~g)o_rptr1-r>4yy;N_3&jMAiyhvOid6?hYo`l%!)eOL;RJ(X z51-hg)Ty#{m79JCEDbAKmGK0WIcb@W{J4j$p+L2#C#Q>-n0sNigKmpm@6IE+U2RSX z>0*-KcRiWWpS*ZYU9W8Sf*`c;l`ZZ~{V%e~$>R_8g_h2^DGK-)vTb>i;+wqtR)5vz zttm8~(n-ErLqKGj|8h%0tp7Ce$RzEl#e(@QohA`!JyPng z!n!h!#ewPR%MATXF(Wst-J>gNzlQwbho{86z3`N_cEjYPn`*VAPL;=^&UO24nARhg zu1Cg(^&t%B0$4v;Z@qPZ;h=0^g}_VmGnEkOpJh?ipX!c6?2 z*v5$mXiL5*LxE{0qsjWxzipHwVzh*K@<9EZ0v~?M*gh(bFc|&BS*D_v)XF z=OwSl#h+)Buk}Tgf3Y-ZcshO4z#pRBY9jkI#8?qeOH$JJaYg#93MY{_<%RiowBbfrEAVEoK`?{#Y*zq^sG3v_C|oa$?a;RZKQA6~Pedye zBE3zX+4`5$62v@~EHlRonOi~bLggYv3GmWi&QVs$6`8$nWtLOn_3FCuaPQB|aw^w{ zWICOH>3NMV_p1;xKVMlDetULYo`1{QvcH^*bzU#Quj@&M$`HXxN_z^C$AsH9b#26&;cbJa{2+Q{crO zcH6YHRT`c}QQ3wx9(qS$D;bp;PRRTVxTPTBv3`JvTrdy;QJ!OM@1o+NR~L zikPPY+26DIqW+3e0?{>9y!01weWbv(rM83oIH?dT3%$q32%>{t- z^E1UccI3hb+?D~7?2?~I2ihOtn48)`jCVn=Ehudh_{;s>3miPn3BEcqSojjbF-+W@ zMe6e3mTYr)n*lr^Pv62rvCW!h);d2KkPUV@ftJCQ|Q=Zgvl z@l|~GRwy0tT-Wq+f2GY|c>j{fA||NrG1}5jqzgKn6rZa^J`-OgXMcev zWhE;E_QoY(w#0(jawg)tY>}Dqn1+MRJuetEGE4BX1XxA{h@NgwaA@M9MdCxNeL|4W7P}=G@E`jC5(&SjREA~ z1YlO_{rj_F4PI@cJofV==hzbrIe*W6v|-I{VxQ5^ zu}=G`$N|})(GRSl;)(pw^{2Mz4pg6?C@p@?C4m6l2gHH4E>Pa6X z^RjoqC`o=4GY01)>^jECJS*cNsmFGXTrYfdtItEb-emG(WV@W!xh+@svp6%o1;1$N zO2yXxE&Au>%)L=-r=$%w%XY|AGl5Ve!H7BG8echsWuMC#HAAMdkx@Y}s=*QW} z)N8;(#W~Aq48$h=WoCQN*39jF&U5`FmXxvfGxU=*9SLn6Rr7?=d zKT~+8b)FM`is=52{n_c9X2KY|tSGFWf6~GddzSyEoPMuZRgkzxo!{lO+6tOYJ81tD^%0;q7 zC)MS-JU~deKGEx4mZ%0KGSajuHi4ykf~WLeg<;`w02mz6B2)Q{R^Tc6-ib2n8!ZB; z7l~fiQOihAna+5a_Azc}c$n;l%qkfuw!&4Z4FmyRj2n|K%u`Z?0$xK0b0C?<6pWos zl5P0_U!7J1fSzG4|DT4d;GiUx=sQ<*3na#(x-M;IIDy`d@a}$KJBfY z6+hv#_dLC(_#PA+D*b6m;mN2*cs2%gs0ngMl}Y(cG3bw=0v8=zAeHo$qo_ovkY5l|PxQb5D(xGS$>YkNM0^l_#ANv+g`gNus`v8O%hFFYYv(8P;9|%)|iur;v>-8Rpo&<4M5FND-d_#lVZ~}+cXa+ zjwmd%TDW* zg7yT*$#|IdWSSFmgp(7o(E`=dEu%-5O<*1y{@hl5I34q$g*;~>L3(=@KXkeIDdff3 z{yx5aDHnsPa+P2B=^tl9n-kDsE+Jhq_9vv$Der>uw=(CG1`g`KP1T(#alUkCDL(UH z<|7 zuGTnxvPt8Xv=uX}|{W9Nz&_vW1}sM`{| zBk{@U%4%o|-Io3(@pbWJwT(`jhI@s5>XNnCWJRKWqc&xCM@=jbiRoHh4Dx^{nv#9I z24cPWK}zzJAWjMG#BM7^;3vhHDI6eEV!=~e;$MJaUmzW6-24RLU}t-T&$dzOC~Fiw zx>l{zdD{8&S1KimN1dV2a<<#HJQ0{?Yg(4a-(OLDBF3Ne3*)phdotcKJJ3lqzGqEQ z)L}uTcP>*WS`Gq2Ec z0g^j&%c~7z?ajNPo-G{$5zGm;zxbPAv1;JGc2H~j35-XTxi%UbC zz(9*zkq`26Seiz2^r1gvhqeV}$tv$b8PV%Abt|aiTHg7CP7gSZNt%onYnraYS78-j zZzs7hOY){hRz_RttMRWa8rJctSGB_#+t3e=%eXhiqo%6kg%wdU1mz@A322@Z^(bSF2*R+ zG|zab%ZT;byKsw1%Mo5dY08m)03x)(aNvWhe#oP3gEpSG+7L#V)H zXlpn@Rax@RfXy1ddnIQj%!(%?1HGVrDN@>R^xnl!Dz$HVu9yhxUbykr`#wkQwPTKmDe1Juv%3+3Crr*{xC&(4 zKtIzx0-9^`j~8CP8)7$IFBq4W03_Q9vxsOE-=dJVTRF$dit4Z-1Y_Dy^-5$>+KPB` zW1x_mcAn3_kMoPZmXfgUle$Ar%xZ5DFY?6#w;5DZE73cI86KnU1z_aIq|H`i2jwvj zaOH0EIcBK=fb}uFu#Z0h)ue}r9AOpv>Y7UI$xt&3tY~E$uw>_lIF(&Xw7DSDE=kz1 z?qP-0*B6kNJ!TEh=6*NV5?Ne&udbzfMbzM_I^&ei@z?5A_M!Bxtk!oz4UmQPxc$o^Ph6vnezhwMm^=uA7v{ zZRcfRCsyIS9d7&8fJ(DIFh+%1F2k zmpLdcvkr%*-%y$5TYjkkC?6`4GigjqU>faHK4V)C+JRS#$Wf$0DJ!#rD`DD25f@bU zY`bv6lwP|r^dQ<6)-uKOwAxr=aA#&SK!72X#6o`z@UpxXTrR?YQTRl#*E_OT%J4Wq z;D1EihrB&*HNFQ=NpwyzUFe_MZ0hX=OO9Gt+hE#eQ{PdjV}H^_#rj@}%JPx0`T~;!|qG_ehnANih5+o+*{TuyL~|hL#tBJw`BjkPldmgvHvR2zQsNs&Lf=*IH$L8aq<7E%4ycQ{xj3V!dFyHrVdb91 z*{WY;?e12Hz7jothzTg)<;48m;&bEL$61BC+pbT30)~d(&-v?ZKf(lD=Tisn$#Y6K z($CXOCoWEfY4?tOPW1s_D)T#%eM!BZxp%>0rgL)D_A8;{Rb`pUUw1#qo_guH8v361 zd7a%+^6QWW!AI!5$qwtY=XJT>HhlcgVECTKOaBwWV=9%Ow|*`?SA1i)d4JVh>g=1_ zM=4zLi-PZu{^(XLDRKNSm+sbiV%H_kmk)nul>RKm&#SRFa;odR*ZK0&4^frDW8jGA z?{scD=j*R=wwv^~+fO>qpUR#*eg1Tl)8`u>AB~sk3d9YoHi$p`Z05I&K3CjzhI65Y z`D8Wa>9tGgmQ9wY(q?A^>JPptmSEKX%Qd4-yR-j9bm)a>bC<)LG2O4$z0CcYlgt+< zW4WQJJI-1G-DtU@ltDvk8Qw>H1HD!C=qAL8(DUBTK*dbKH^kC8+I20NWaum>ba+r9 z)I(RUMiH}J>`@tLuZe0cOf0NhSgO;`G=xcG^Ys_@f_?Zso_-7tICd02qTWM}wGJA0jyGw4^;GF$x*r zxdI3s@^;6r0l?rYiUB$4WAFx>jq0gIGTJ~>Fs}0<5Qq3qj~6Z$+JMU-s1g#EXr+K~ zLA-)-O&lG5-;J50;zNxIJsyF5433rDWrfM|BE!$m59!?rUFurYN|`q9(mvw>XspzI zUE$DJGAO$~#x7e%%IiordJtCB5D`siba1H;2^<>q zRpnzu1laOeI5|OVjG3L_!LzMZO2x6OR%^NSf}U8ZH^SaY z9rSZOlch*3+oW)^uSr(gGAS{f93@y-6moVJzcw5<@~tYOq=uyzvvgDDVa%7hCmSa< z0G2WB+)~YJ5mMCWKg{MscR!tJwOIb=p=#wd7i#A{#%P?vjQn6MXPPxu4HR8DCxx$^ z+rcMI-5B;TW9dmACZ9(AxFMTDSqg?`+A;}MScD4Ou~aD6cp`qq#AV6?W{L{##dsMgz$YbnA(95w`WIT)9xl*FPX4tO;svJwYRS|u+dG&}V0w`u# zR*y!uc9A^}Xlq@y=|QWqt>CdvDj^LEk=+)dm~N<|+>ED5CnUr38Q%*@gTVxJJ)*;7 zL$gBQ$7`hRpF!q8BxxpndW((hX*%0|@=7>buJPt;$B(k)a>uy9vmU3079Mo_SK7o~ z)$x$Kx4_HO-aBXC(9K#6UCJK)c1mYr^HZS4hGErL1)T};TN<}6MSO@EJ9D^ga7pVL zXzwXbZ9hMibJ2=189&jvCu8J(@7dHJ+*4r$Jfph??c2D(|P4X~Xy-weWV#Gmh z>M4(fMkYjsI>gr{Hn!{J(VllUq9%?cByL51@Rlv0$E-;=MMz+AN5#vEjc0%NGU<<$ za4UY(^H_j=+HHZImR0kQ`Rv=yji)^u^_x6N;d!24){8xF3AD_y&HS@%x^{I`Vc86O z9$EgXL~1DQcg-RXmtnZl`L-tKe{Y`nu%|&BBZNoH4Uf z%g$N430Z2!mrNb0iGd0RP=jowmww!9@}vY{cf= zjzfZUJ9C&o_dfOATkUP%$ zeW$%bUf+8esn_A$dQ@JeFZu;P>yRb<Kq*BUm@)t*gOZ zgwfnhg~|a;zfjVIw}DFRWU0*%vA(yI9E_=P$CvEn!udUaM-MmIvN8I~zlwBO8QxY) z;pFitr_JvkWDgZ9qi&NN6WtP+iaQ*AWylOhTR6K`Go~=8v2S3ZEwP1zu-X~Vf>=3> zV%#Ga=(6O!D19f{ifg>Skk0MWq8q$HUd_r`npa__eg{6>-Gl~5!B9QnBhh9_{ zg~G38cO`_yUZvr$1PN-UvPi*@sb~rkLY-<#8}`+0()Tp4R-lIr%5w{Osu8eV4Uqm7 z*ueE4O?KCh^vKE232K~#fRBE6Wnd-~4-)y+lpCFrIoZSH&ePXHJ&UX#l+~fs_{-!3To#tiW%0ovBJSI-=SYTS0zoN8#wKl7Ef< zfz5sVpa`VXJNP&ct9t@OdifJaSl%#DzzfFCUOZ5G8*)+4%UE&r`W8yAzDF>acV#Qp zg?$AbrL{q4k@iiChy0WD(3R2@vzqI1Qa*Zf)-P#uH)V%r=Yvf?Vh4LhO>n>luz#+8 ze}sRS?(iBvla&}Iu_Bump{W5H-~FbuIGw-#Pm19{2{NbL4;=;ec;;@ zqtg=3uKNFxy-u2T-8n&&d|G$#(wg=L@Afk^e-xQ?+0?PsqfLDJWxTLsD<7SZDb@hnQ@9=1*QGp?-52wJVKt2I)^Ys&d|!=m)bu?GF{wardysBUn<+d$X6AScc9_CdSv|4jIjnv5o; z-|XJKYJ}?f(?D#KYIa5>Jj7zu^K+J8_N_eM8y=cT=_Qv!y)WE)d(xZ0k~TI?d1BW3 zzA)HiT@sNx$IuBF61fmwIZUlx3hs(HIeXH zIEX!1)GdCV9%^OiIb&Omarc=TF? z9B0w}n=?Qm{7?vOGlX1^f^M8rRqHHim(Md)$)5+TF;J8k zp>JhN#beX>cB`8Q;-6KG?4c6Y>GnTsKbL9FeH|VeMo2kX` zA7jBXD@XH;DDl!eGDKi2AJy|wMaiddZd!}>%8)j0Xfx0dVLzX128PlBy7c0*Er7$Ntg2Ek^L)SqDJ{KJ=0+Man*=sV8-b{F^ zyjN=;xNN|eMx#24u|3d!*~Ysk3_k5ziJHZN-_W5IM7X2lKXV5u@}Xj^t!tIz`BHB! zvPcG#PYi-WNCYQYn3Tx)8f2vX=a)n;jS?qwsEzmmuw7SjBNGLXFLH< zwbpTi5bO1|h5?*gH@I@#3Tsx*HOHU4gr6HRi5oLX|NdLzgvpV@i-T&TFZG~&$|R$T zoRQS})b7j6MGkS(Z}ypnNol$W=H{|*33omB}4@&y%rKZ0g$~LLSusxd9WmpGt zCbXh?+BDG|SX*D-ZAy<+;i65L&epDmh=-@1IjhNYShd9%IkF7YFXSSBa24Q8GH5dBP^(wA5_X@!-Mq61^ zQ*pw=9hUsIDr#Ec5Y|ul&f3r;ooHWxO{?0_2L&GEbJV-ya*P&5`}GKV3ORSH!zCAm zOEm95bA&HJrdOmqLGUBauuZWwyNau!69fx(JMW0Bs3c}nP&U1H`ePVA{sLGr$;h1! zP=p!7e*L(qCyi|h^Gh>@a6arn** z;BY2W)aE{z*9m6Pyk4u9O<9mN0+b$2VJj>f^tCtv#QTZ*O?QTjQpkgij?dj{Ge zUB$kn_m)eh%%&a0ZuSnYg;)N)KJW0`sh#S>@bJVF?ni~E8#7q@yMuO)a)UFw<2r`- zYoDKKyApB+ubFIg!a$!WA*Jv*!ngc-n6=lUQZAA0CpZ1rkRxC?M4hqj#n7#R#M|1z z$wSyRw6=h0*SHl^c2|Y&*}4Ln(2Zm~oPLrL^C|5oqa7kk6+Btui!kheOxHc>1UgMc zZ$n^d(-Zr9gx+RU2SxP z?Xlahr1&Anb8eBZO$QirDHv_1=Qyw;zbGs%G87E@ZEF+O7e@4$;N$tfmKfU*7B9wW z;Kvru0vR{}u?gp4jkG;W05LzXf_NJm>FJK)10aw9dJep8bqKu@L?dY1q` zrgd#j2Gxm1@qQ?)q+$u+lc#t3ggwm<%LP4LKHCYh*Zf*IeNLLT)=_8)g7_~$CMJOk z!YPP%FwLP$=FtURt+dEvi4`wGLIExn`xrdJ4{q?pS24H?!u)8x+n5_r zTBr5Ypq}59r4#Mnd{i`@DM}b|)ph9f|A7*X(VJTd%Mn=8`E!2*`ob$hZ)R#}|A|6f zm_z5i+}TxW(O@JkNgXj+YB}((BCjkrrbF-7{{eE0hkwfqteDsuYKy0)B#*Hk?KW5)VaP9A9 zs!pk9*&P7rQhl=PUP48k0HIhr&8w$*aK>IOzs0aBXku06TDZ|FA(uC(2v7gc!Or7_ zMLso(Nf*e&63EX&U%_HJ8g3p*)IjhuDN~DD!YieZ98k<7NdYx2fvMUULFB%|UROt-8VYZxc!_wE==*t3yvl#W4jN&M# z_m(EwtCf=H7`p#?=;h?+^opjvQN;*@;A=jgs~^rVM1F0cvX|!ibX@L#Z?M5wf4TUs zFD7w6xvFPrQdb9(B3lEkc=x)ntW{|?OGC=d`hGxHh>NTR#kSYW!gRD zKA)vISb*Li)+)>?jS(vL#ngn*Z3i($$u`QJMIZ!5*bgasn(S~{w&|JU%gB&Fc{y&+ zR^map7VCL(vTNtw+b6nq6YxI`p7eGNo?TX~|7y_(?((cp-5~ET9 z=@Fx>YaT&S+{W3lSNRMRabPic6zc38 z4MbbFx7%z6suQ%KD|x<2*w6y)b-lzBNXc{@=+|z&AFAKD?Feg7 z@|*ti7=3xHbx-ie!%$G4icjDYdSg+nv@C#44AqTOnP4vh8iekQcyM*ueC>LW?heLh zY&7F>QFpf3URIB(LRo*7aAcu(R!KcuWgt={5o~lANZ&$l2*8M|ilC;0oIN?b&e4&T z)-fz*m_u2A`HojBo=S{rxl=_^q*oFj`I(tJjpEt1joQ0E@7|{7c|M&;Dmy@Vd^L7zdhA0Isy zk`HA=>v6O9{eIrx&Huw7`m7dXt#dMKDo=~(Rh{4$TU9!?H!7)`JJLVb4T&^t(u@5*K4nQs?3>oX{jBIMV#twH~JH12!c}`SM`zEwJiSDOt$UDU7u0 zAE!q^g><%T20c`rokfo%VPvUX6Fl38MXyuZ?G#zme@z}TJ3!o9nD{G9%H`}Jy%!Vs z#gUZ?j`LbQqs$QLb-QTyhml5pVgY@M{yfj__mw>xczQpn=2-;tiNy85c$1~CCfU7k zStb=DM+(*t@$ma(UR)ctQ<@oZ01PSA&kp{}12|06mEazaY!9|^&!Sui98J^Ne z$z%@r6_+rJSfHQ7-AwJYM`nwwjsow)P=W|Wd&9Xh!9Lo+?jhpvjP~EOiyd<}^Sor= zss&2n++MS2U-VtbEx>;ridd`L0X&kqf+p$z*_zVgvIj;5gw1wnx@?$i@S@4k{~H zIz-`8{&X|SO-Jkg(56Vz2{qRAXyMDt?U%m6U!n5*>lcE8x-mZc7@PaKNKp~e=3|QZ zmbTjSCzjDvf@WsOpE-xsXVdH7e>_o=KD1M?F>1=PO=~>(D=J=ODMdozLk-ka`&?rC z%n9s@b<@9;5gD2Cl24Kj`%!3MwaNA;g!l~U{JHAK{#A= znS&^@%nsSLoh{aGGO~=@Hh0^8jQF{5OGq;eU+ycMXp{V*(2t?%kun;A;tTE*&ffII zM`y2*-_ynsjTosh{y@XwGURPA6Sp`OLt8PEnAif`jnKRWNj~_9FFA-dwSgGX2UJ@r zxy@|{L2{<(Ag|gY#yY4^gV6QPT^z!jX}>C~S9j5iODeR=w8P)GeA?)PULqeaa3~C! zCw39j1zMS)BE#%y(Bj8yqS!u>mcVaDa|~@=W5-3Fr4N5*CPiKoX3f#EUIip+WUOzG zyAF~1j1vCtM(Zc;Vh8PgDXCr4+EXnZ>a_X`()F$0ry#T^V(*4Ujg9byt3XrlqdKX)nTAsDZ~ON zCAfY!MW0-?rh)t<&j5CaE^&|NE*?tDWbn{NRebq_&sUuWS8!6Z+Z_ ztTNl&0^7P9i0_6JlHD>QYxkKK8&fgHgwH1MS;3+!megPRW^Y;*ypU)eBNA?pLRc() z>$inLP0?()2jN353|ogR^G*aW(t=`f8mBaHGAc;O+63RB?|nlB3zv=3T6D@r9P|Hb!IUx@JoZsYQOJ9HZ!|3Xpb+vQi!R7EHRnGQ-1B(@A@Gcf{2c zrzf79zzD`LZBT+?UxCscQ8dUoD-F^Q>ikvi`g=aDZGK05_6@#jo{kD2-3<1f@a@Ih zD{x!Hv`TF>->>5gITNgQ};LIJgI53WGcYgI?`0*Q+7m^Sc8t z;0MS$E#q&=l00|_XiWWM@ZwG?wS(R^xW>G}M#u!gdco7S&Ut-w&e&+QgJ-pO*-1E{ zaVYHJnW`(uJ9Jxc}vAPc{= z$`5)5CvHf|a$&L4#T!y}!3Bc?l0e{IdV|c%<@QUV5jBgdNBoky?rS@Ts22$GL#(k;i8j+Vm?cqg?SPzHkM$rAudtrdyQ@5!Jb5H zdH7ggWAho_<~>bnYnqNzt@bFoZ6wVj8){qc`g%oEkEkDCH&B1I?2@SYLt*`!mn_M9 zVl8KZB@vQ6*RS{a< zTHcs`eaIu9Fj-1dTV8`~ND7Ff-b74-}Y$D~;)eHn@C6*$c`)lU;pKJ=+3>;c? z{&19hbkDATZ#q8rt5c&_MZ#Wie?0{T zEk&d`i_BxRU&ENq{M^rW!gK_s=JR{#aE!h7I-_P4eDSmSR=Nn6^HU2nMGS48Xp(?_>wHb}CNn zFiyf7qnL{>-xnh*7RZ>IVFd*H|LI!}q%^8EQ zwrs`O)8_)!o|KChp*P+7VI3z`posfhy=D}v_Ayn52%Q3(_L&H@9VmYTQYr9l+al5G0M?hh3_24)Cx~CEu-?pU7D^AXt+qiugN9K*% zYQ{;2WEnoS^HPFuvZnJ=m8yjC-KHmey*`(c2goiMYQvCZmp0 z5JyKphR-);S?IUh`EthxXXyldW^en)4c84YuJ*=0-T%d1>)>_#6gquj8#f?JK_r}*Hj~3Q2FtTeTOAx z;KEb4D55I=!gVi6q>p^hO5_w|g+NYskDNib;oR$mNRZnDbdnssxS zLQP-^Ss6dyR!(V&%+TS4>{u*Z59_xX&Yj~N*?yh-tU6oKqKoCIu2Yv^?Dy0?W*6K1 zJsGtlpHMd)8BOMh$=^!xTx`=BPZgN4@_1>m)9_K)Ha@uE`c~n{;LW=7Ek~Q67L^+T ziWE&G3OPoVmT;7gNX<;&$CDi{W4zbL^whx9Sf4H4yZAS5`P5b)xaI7cM%IhW5x-Ar ziX$2*)YhiN=@Rr3A|DlFQu_cpq?8>1l9GO;V}~Z3s?Z*Y-X=r0NrK%Lir^?n#41a9 zJBacjW$7>@jC=SDDYJ|<_GG9)D=IDP;eq^-%2MV?(v9?ZLFDFbyVdB{o9OqHzn)H3 zKpuSku{AwW%{-m>P5(nfLE>C$ymcvItkgGBue>hvelj#vhiJSbBcquiqnk1&y|;PLW=)Gj}R6Y{&ey} zZqB6inT7pd&l;*)M(+P<-TiiMys7cgGp|p5H`3Rp!VT3=cY<$t^E;X7F8C35gU8lX z$?!(-teW_;%Tog#)$Z~c34%=xn|}r~U{px=Lh`#Iii51az{YBC!J6ei)Iwj6NOa3w z&W1z#n%JOSUkoE}2YG(|$`%eHfq|a^_8i@~!B}xLlIX3fhttH{t*$d$)2t5QQf?j= z&v~GB3UAXob#H_lk2Ppy*0S4w`YB5`5+c!Bh#UhK)zT$JdmaU})yx$SxT&DKntoGw zU^r~QlQi(Dg!ge@0atnCYS3tu+9kawe@&rVn9-l_tN*0tRr)Hq744vY_M73B$x?~n z@CePpw0kON>K;1Kx1in^tiemk-@>=a{P$Cl(qJE(v_iPsHPgTdwG%N#L{iNnG!T5; ztQu8AhDA9YnDv1FEL6vCPF7Nbs{K#M$FCfpe2w(~t)X4i-E`RU z>F1G!f|=pby6Va7vkB$nU%S8S@sAZZn6ljbFb(ol*T zp6A|{>wD#S0MuJ9Hx;LvrBj5%Nl*AYD(>y_BXkDWlWt#4C*$V=xcYxxREqhVTeY9wIoPmj=>X zrGyGK+yeM^RU;iODtBtVxf5Bl6%e{j8GV(*}z6(|#ZvnoFH z4;S$ugv;gpwvHaO=v1TLd65}}VWv{Y;ZAWm4itelBIO9#ERkDQJk4vxuqbVTd;lP1 zmqA(V@P}H_#>iaN9^tNBQnRc|qP%m6<>8kT$dm_WYZ~(1felU7CERbknUYhoV0{M1 z*2(}a^>VE!ai(TJ9DgzeV)d86vFrMe_j(+81R~XbCu@bF2?q)l24r4h;5eW zMD3;rVcdsbz)L5GSm+QP++eh0F2nUd|4$@n2dL;A$0EQx54YD_40uZROH;hTnl=WQ z0Wg4#3xab^GgEKpYZa=_ObK3UYy19n9zRH2Cw{=0EWJs0`D4L9rTl&GDuTMUUalbY zWbf{|HmN8ePxI%~?Y=iZzLKncruf;@{pY&wIa(BMwU9SMKU;jwS9Zi*6S9lc4{YpQ zdO5+PV!;1ZKHf}f^FxPh#dDN$e$mA==Jy-_2w#DGWT=(q0tz$iX zgToZvSLnu5X%S9ykOo_uz8F+gj3!+pjHzW$#B)R&mgJ>MNL+Np`=Z{m=!6YH5jY*a zGe=@&koi5`ov9M4i3UZAa+-F|Q<|kd{56M6hp&O9m*8Ecyn~%jz2=CTl{$e0)@viP z6ADMdzsx4=VdF)&^*7DSf9uvQne4lxYiP3^uP%{%@X_quoc@bX3)YT1&TBp6clZ0u z%!0hGt46q1$t6WC9dI~5-CJdZT1DT#@e)~lVM?_^_`>+)lvN8$TDzb#!%}!K=O0p2Eq^)vbTJI_pwjnyL_$v~0 z!Qv)a^qB4lSO=Rt7HcmE3{wyRpw-2I_4IH`%PTK#rns1EIGHNuUECx=(avuX0=<7x zG1w8Nl7R>xXa^)M%(hU52U{zB3#3}rjF}JA1cSrV^u#7+66Nyi)=k`=XaXkcwP^{6 z-+< zPAkc81!VpJHs3WO7UXU|_C>&PB%K*<#$3IsZ``9IZ77Qb6(mgrXg6|oTM(gCA}Y+OJ@j#zQp;{v)Z#*)ybZn{06h0-p0Xpa@j9Sj*f-+g^mY1Z(qK2707 z*{lZ$6b<9%{);GBgB=f~O_gGzP3H$c+kVf!!vT*rbPE=29_9drBr0zLC zv(l#dZ$O6T{G9$!7e5EYsR;+3rxP+R8v}iTdL$8`tFFTB(QSV%7E4PV%XQZ!_suDh z)NZYbW%HzYc|ls*wC$v&jkn7rtfRCMl#)}9wCP_bzi{P^PJgD{5p|0CDLM2vEq|Ap zx47DQFP~C%eFu!fKpfeK&@_W{ApfZLUfLd!wZvZ^52xc~?S;Ld#0WVzZV-Oe^zR_? zY97%+o(L?dVBL4~^|Wp?Y$hLO7jIs0H8)<9D=qSWIXb#w^u71pb$MI8{EO(Y8*;HsB+A| zbY8A2Dkr(WJo@@hoQ%88;J~n};_DyyVT0yJ4$I*+b4roVgZExFh)b#N#Er>C7BMdc zCQalY)ghi7jz>OQd}GhW_#SOu`Z7MKVZ!*z+7wK>m9b!Z!_tiem6(8CANS7!-6 zxPSobD-g|1GN@u{R>#`*+Uk3DYCd_Hnq{e5y0;^{lU{gWW1V0HtL`<<>JIcZp#`!_ zT8pf&u{^8PA=-#-$7^g~Hcl8|R~-cH*}!=9kXB&dka8I*W9KvYQT96?f*7QOA6b@- z1LNXOU6wX^+99|2v>e@)oK$!Xd$DPVndbd zvu~uK)Mjy<(cE}f9+7{cM7YLORt1(B_j#hXwC>9vrfK(_{YA&VI1IGP9rX3|2YzOM zdSAsDQGaeYV7svOXRzA-w$~@@>LIRGQVkp%6=@4Y8{F}^SkI5jo<~jVN{go1c?t&v)cEZ;mlT& z1jyjDBVcrm#uFHqXNsO+Q42%f0nK#`62Ps&5M0$B)&(^0pr2Xos32O4J6wbg57RB+ zb<@<(1zKr9yLd8p#CU?85N0_V&V8N>a71K^Ee6G0&^V(X?B~j!6Eg+R!^bj9Dy-ps zR6Thwyhk%5@&nN!tooU2`}f;@7N)-~z1YN1wGaNdUt$mRDW%iBfWX$(ed+fp@Q%10 zAO`mdJ2o*QJGZW_#p?~t?d$=mYC>dHzg4L8_o zb`D=W=xp0m)4*cYQV4|}vg67api#9=%G{Kd*iJtPE-q2IuGXgPwkA1mxZ2BHA9ps? z+vO3Q?eXCl;^qtRte8Aw5(VVH@OGL#Pmf9z$xYBg@lXbmgx+2WZ}zg^e3 zHtR&9jzfa>$B-joOG#yykXQjtbaZ0?Tgr692xupeAf(?cnydJfz=9yAu zCE5KDip>(0&ryI42}5r)HRsN(iTQ9nP}TGGa);1dMXya47q*W_=6AaUT9-hC)ACzN zIaQlfv7CA>N;DBz~2vU8i<4p@Vbjeos@K) zQ66{|uX*Zxv*S+;e|f^gx1c0uzekZsql2)TjN|<+Pf)Eo)_V+BJoXk8?3pbwq*^;c z>*`pGt{rPrxVus|%!u5-Ti^<~) z4w-SD;*$+6zaGf7VKk^7{pY20YV@z+P6j8DPoD@~y7(xeUXeHN@s;V%X6Lns`{ybp zUfD0i<_}j~3G;tiPqfk#kT=kkQ14!PIpE{R4Ha6V^t5*P0j~0SA97N#ED?i)mC^34su$;d3TiklW?$`E%QJbI0QFFO#JX-VV=sv;cFA zcbz&wH!M&qPyRAdpW{~Bjlkw}jS`{xnvHlxjo?^!Cc0>1eMmgDNpsfOZs~y|#-*(T z?_zxsqGBjwL(+U;4aO0yR?E+|gw4g!3a?UnCUy=v0@=l{M{r)k9}do7F6lGgY^#$a zMts9^MTZEArK|@(hh6tGA9q_qdqu8oKQ_aVHbmeD!#{e0j3GiVe!Pz@EuLI(X3ei~)85Aey&=9$k=HB8DDU zHfTP8&&Q2@?5+g9a=^jf4B6s@!YG+ys*`x1C{vDN301g};CT}&cTl5-jwzPKzA+6j zT|idesI4dajU6R1HsT8J&ow0r*_1n{XYKqMX3k4^m=t1IlKZTsU%F7c{ z`@o3gxh)WL{I1BPxUI5UKj`C8_S)6xc4ejV&h;U9Se_e`vL5w#hP%}n18X-~x7r*L zM^|}i?;r#<R&$|7d(dIwi=()qy%N?oq~?r)2TA z>Rgt9Fz4!OEei;i6i3G1j!5WEoc_t;?_jo>+>ModOO=a9L6ziHB-Osw2r&;XjD42@6QvnZ#GmZ_7>Ms z(@M|V6(=L0# zSjQw)?=;3E>rJhZG`*_|T+w_OO5>?RGIKKu*53@~;88 zP1Bd8S7w*}dM-6BZ47c=QAC;>(nZ!5UMCp8Pgrj(dVT+d)so$BB5}!g6(=K2OD0v2 z<_GVlC!O5|2IBHnem_+}5YVGlBeEW$or%RzmqRr+OAhVFfgwZ3xZC!3v&tW@9a$)1 zhGUjGJfEL{PPeJh*GLp&_KRnJgWKw7rt}qis~60hhU+g5G{F}$Y>o5%&8}QOisLcE zh;_a=vMYM=c%GgvE}QfX%q>mL2okG4-V@2DRtF zb2`&6ECL=)6@xov@mbnY5LJMc#esXD-<3dm z#`bW1f7-@$KAaY{KJ&r$)1Aub?-I|O=cDA1BUjf4BP;GaA35ASyj)K@ng1A;|2$>G zPNzfBTZQhJzUfdJDXHr0g*0ACm`(T_GifUl6tc7tsrLEpsrgXU11lT446&b&7MDbsN(*@*G*I?~Lcy z7t7#krCt{Wg#c(q6C^ComDGs}lKe64yWC?SAsA8mGCllO5cCwg3NLdmtlW$G>i#E3 z7m=$8KhUv_=K?A!{%Ubp7%?~GAoOuhEuinIs~-jiQ5=!q9^X#|(}g@8bJT2q{|fb# zwcT-Np+h39C(e{A#c|c}G&P3U?|9b&0C7$b`m*w#qYom5KFBQv8*we7L-==jxJBC# zWYN9Apt|u9J}r?|D9rhrXv232l$Aa4HlVeSv6|%|>A69yHZ_WOb5M3r%SE6_`_r_= zF@Oz=1*6Ck%tol>7McTkLH}3`@*f>N=y1RHY2c)$=mC9$LY9@fE1|Kl0OZ(xp|sG` zMM5pkHsV8;=un-DA&to* zL8*2|TJa~LBuuV?xJEEocw&j$yHRa&_3o{=+XWG{Yt7TmC#X>;O#6L>CaIy~8Y>z_ zp{L2H?!p#HWuga;XMJ=`Wfj5!K1+v(CjeSQz*%^Li~dt7y2QhBQ_Q~eb-C*chI0zNp$nnFCQVpH zP}bJtz#A@tGc=lW@5PV6Acz63Y4-i+%7|aiR(uXH`OCDmbRc*2`mOx8_s^6oxe!PE za^;8Vcbvwg=Lr_|2D|y2Hg3IFs0@E)r!-WaOGv7c=W9($E<11Kx- z9jp)F?3<(C>a4-mFC(*6WcFN~biVn%bya0eJHXBiN_ZYzP4RrI>SIzWEG z$G%K2nMnI3-PxHUtxLAr*wTaso@UrEc`X!#gi0NrLt9!o5LG`16L{=Pa+Bd-&PHLX z{-!qR*@#k?isvtLVrZ%0syw!i+7?eL=-b~f{3%=SC?G-R+3#XEn8W)e(m}11Eh%%a zu-v})F&Jx)<>9ufkrta)R+9FpsuOQ@1Vw3TS``rBi3R zp;uHE9J(zFy<;6c*Z@`APnIac0Aq8Au5m3EJ+?|OKN@$S56TgVi3a)FXXhYFmD*Xw z-1BbFG(o-%Zoy(-)3^E%rx6=z*t-J6D$&H<(J`npQjTBI8@lHR5Ah`k1fT(>1SxQg z!ZL0f6l2vX8-lzkZ$ea=y`jBY%!sSPE=H}>NCc=;jzF*7AVBS+zf1IOak%9HaluQ7 zA8Zf{j(yw7ji#KA%EL|shgaYe=W6zXgU|Zh&0h-&T(7XMLW(@cj5ofnGC7wQW>DS_ z8EDxo#+?LP*T*oIAsH-dNAx+;vyx#~78R2@1ntK|a3ES|uS%TpwHni-g*vO2c+9tj zE)((7dF~NVjt+M*G)G!&A+EZVkPheJpm=766peOe%A{Kwmqmfmm&zoU7Ryt@qZ6vY=zvztU_^oIcYx3M+9c8UcAJaUOw z2qjcsl;;Z_6S08qP;USP`%8S8Y80Z_vkNO}WHd%X#fas)g)(@>x(Bq$PfXyK%6Wa4 zzDaC&v7CxWP3;E3ygG62&5U4ewd=^_F zq>J27U z)3f8jaPB9zoK{AlttL+{oNan-b{|A8d=d%WJQSapjgnj=z4nWawCf8O8ZqyBe>C2E zb>BDB+6W7>nNRH5&}mdlHD5+4$u(t)%j~nx0uU^Srv562Y>#0J z@3iWpXF1$BhGFk>JW=>`zS@)HAAy9eSU0jYzA(JFUo57xj~3go0Wj4q?W7#R*Q9*S zW`>07O?|6vQVw8w`0HEMga@cmmNL6R*te+rw7qV0dd5hQv*%$TJMxt0<1lFREUW() z8mIG@-1NivibOSj(O_GcM4I0=xOj&ps+`o9vTD79m-@z@ul{1zxe~36& zc`&B<1k=Xw<;hol^?2rk5iG?3z8PThWDQbY=aTxee!@Wm8~(jd38>Ed}v zhpwDNy4&mAhxH7MV1T38V!eS6>@t9@r@xx{PR`~K7stuJHU%*New_Qd$=pOwMar5 zbYuevj0|9}7TgYTyPBRDu4yrkFW@+9zev`ZxG7CvKL2aTa0CW!d0FT%{_bKL5w;o=<9? zYuSeJ?22&9@92S#N@c-!3T$Q+G>eMRK&Q#^X_B3(-1D05N@aOvV^D;3mh&TY4FQd{ zdRs)`F43`vnR*Tsm-$P}V#S8`%90t}*#F2$lw}42#{I+W@(bTh%2iK|Wo!2uLx58Y zVF)^gn#(z&`84G!#AWQPP=f{ny^hSaJ4&nKQf`AbxeC+T;(>q!pWyyNdEUXHmv>2J zK1m`_E*H~OZr>p7gJ#-lWkLM1%A(G)+E0+Y@~~Kvon@o03Vj!)@WKdTlaVr=+dNGQ zExl80-`S;+?#gE;G68BD3Xq1Kx)1BI8&L zla}>u>l^S@t08EwZ7ep^9hj>UO9L)F(6&Gl#4-zGRG3~CQ9jXUuPUkkW&XCLtU^$7 zCibMU3E_3T?ctB=^Ul$F4xB5wJR=2}2OFoanOe&Z;1|n0CoartNKYy?8>R zZ=~@;l=e%@%9mA%1F=0fb>=H&LPdOrhILLdzW)c^AqEcnzn|0fsae|C=#|&g@av!6 zB@HvPoh~`|r3L2QIJjlv{%|*&-+#IAVJP0#n69sT8l#=6ltD-@q^XSB9jZtcLk{+w z^f+QJ49(aekDQ(GSleb6L)M#atG!VQS7Skpxd8} zBs+rIxwH{q|C}LSAfet=D78LB~sJQ)%Y7<1YLMC{L_%rxfS#K zzlr@d8GoPCv-BeCeWUq3djbzV=5E6uHz|MIHF+~QeULavEz5aXInQW3r=G1xc&$d} z(FzB6J}Zz>`M2JWj0>c=s!`2CxyND}Abo5dhL$r!b2-+@tdA|iKwOLU#o@8h5>7$w zq{$~wF|JW*`6G*6SSr>Bb|U=omO*|tFxRcbP9~7+GOPFRkN1o zeX>OYrBL+ZXPVD6u5!!L!LWmV5Dv&rCQ4Prq{S<~Vsv(LGWyfcvfUFCM#{{{7;xPe z9{5+QD#dMW$}kMebJ)HN!1R&-_a%Uhigh}R$1bxNC!xo`*c%IisJkT3>OusLqn-Cs z`z@7nlzA>c9wpn}7IqYh%q;!3E0wunbD~@nTvD4URKh$&Z0)wAnQ!7KdaL2{L@;Wv z%GNG3)YDwW$iBXK@Yg&;2SDOfuw`t}Q1~*eq)yfw6|4i@cS1wF>9am7+ z&2wsqgfFJDF=}oVMl)IIr078fR{L%VKNidI7Awfo+)twj?BA^|P37ap)ypP#lz$dS zT+WW1oH-kqAv|*X=pePT1?+PL@8~=~b3ycrpsaA#+5^s3qmn2SG+SW_3rwUlLD8Yk zPSPL{di8`SX!HwJO6!$vHvuRaW4cF%k&s_hKc6lwQW6n*(5{>J}tkS|6FdM#!E_4=f-vOYNVD2$E?tJL8s9v z`p~gCsl1B@Jk+_@3D-YYIyp`n{Vg>%X(M|@wV6{Ly7^$>eaxr10TXddfhjJ_%44n~ zebmZ&O69DjmBD_h(&Jo0vtTN3P>VCl(&bQv^kSg6CG|aqT;IO<#T2uaRiw?}pqs832KY94m8s}ULnwOj&zfzi3PT63y3Ngx&RW|ARGE^}c!s&v)cpxWdK_!EsIWh{MA)6bMyog+Z_8&>1FJhiM^Sue z&i+ze!sy-G>f?qo56b=WZtC~ve82ZcoEFE>?Tf?SFSv88FMzu@EF_%p%{8oPzY)VHNNgY8_WzFgh|^cQ6xh~F z@HpU$((WJEHdrv24ctdF5c)KuG;wk@R{QLWdWN9?Sz#Y8T1?EkoelG~50>#gG8QLZ zloKxVgLVr!&u(N=Lz13#9BAK>SYZBYxBI)+f{}K}zDnG}GhsauLt3QOtN-C@4z4o4 zQ5m8coS0lFFA};t27a5=(6ZU%gIh=uxlj5L3-*193k66Wm7@*$doTLkshyL%RMlcf z{PYR1vS2jR1==8@XbA_9^mmDKNXyX(y()+F@ZKXrweuC4^%yppuyYI>zz6e*gOE>y zAztXXgsL?UTn&u7UVIH!5&=U1W+3DZH^>!hSnbpTm2+|s0MS>`fshqkK#)DFvBAQf z=`F}}dxiTZr8+FouIeK*)COp*pmq*e&TiR%oo3Yy{Ey{Q0@|umTEcQOwzsurT4K6x z7mUbM-&SW{iKS5G=~Y$dAKjVY3%_Q5Z>szIa0ce*p`VZkJ)dqG^!&H%kGh(Gi8q&2 zkUUHu<31vvg+lXQ;X|&^Y*&6@u1enbO5tiIie(FEoPf1r+RD*)Q3yOxqX8%ZKy@Us zBT`{)AbAwzyPfs0U54O5fPdpCEF4E+Z8}o!cKNr0p*&xzY-k;b&qF*7L4pA86T@9; z4R4-FCxtk5t298`5VtiO;_Uswz)5w6s#to4G7^&EF;2v~fA$PpEV$|(R3it_Xq(FmXseKPW4c}H;2Ll=(w>j7!JF@fN#|gxIom|NnqkFj9a3PSbHJ0sFw`9QfPFfZ_|t)24zTz&MXy6Q2z; zwTX{=m0QV(VNszh7j>TV_NJ8mI%pBtSq+1t??^RjT`ukt_UOICwl>dul-$UY=KDrNosZ40J$$9@@CHXH!xK2{b zl@544TuJcragzbM*Xg2Sq$K+}t{^lH**N0RR$h$$W1?<3_sp8CQ!TzEmpiqjT%vn3 zWoSziE}+a^yLKE3G7`~%h?Q`H(+Z2XO~SV>iG8k!gxHiDma%V?j&Iwxv{A*X5?ifF z#%(2e0%BgC!nq3AX|4c5cG=6$89o&TIs9~veKApf(YDtCzbL}HE`0~7;q&5XqKtN9 zTWiFo3HsOV)jfU(i>EAgw+0|)ImL39OrP`yeA&=HJn7K36l)%F`JvWK`hm*s_`nUu z(wmJN2OTu28Q6TA^u1Glb=v`Fnbi?Y5KBzDYX=RM6n$7k{ZP^hU|mT;U<%4vWhWzH z&XTAIbw@(lNJ@y}0Bdg$>%p=GD`2E=Ej8ej-i(EyAcc0eD_SK9;(vy0=pEqS1fDtH zUA_+%?gLMs`7U5Ujt?KoV^8DT0xMqH^8K@Zb9V|H?hX@vV5? znz9%~Q2@FnFDC$-I^EUxnU(F%EklPfWCN_a&!3eRlR6#ttx+0N^GHLj&>59u$*vZ^ zp*BxsPt5Q>JG*BfMUEvP+%>CO?vv;U||rvQoyT*mgtB2E7}>XyZ%#`!klwA z?Pk3*oWGE=X1Ngz2062~`c;XsQmyzH-;Emp7U;{++=}%&ef)9*1a(0#cNp#lc#v#Y zhV@~v_XDwDJu{Qgt|hW&GNVb3ztT8kuNZ%t`oN+FA~w!T;8B3BO`?5x^hEbEN@b9& z7>l?I{957Z`W0YQ>PDHTe)(;#i5a}c+Y)-WD|9e2h*cH>o3jW&zcZSNS=aV}3U%`F zHtRF&4Nlls-&ZWOH^Oy^$sfHqsgP{@;O7N1ip$1lM%3rd{(EnQK7VhVzn^nG82`r5 z`-Qo2zfnbTxFlTt>;T*RfY>EFnB=9cGo(WKYXN^^qR7J7si~<5PYLqko#S58G3jPW zr&jrnPN!j1n3K9T4-GTG;Xh|#VZ7kE^>>^lKb#sjaPjs~ib?ZZnQOOQxMV|LJEqO& zMU~@lq?(0Apjj0v>(Zzn^3Q@d0$(0gG?Xx|YF;c+-iUEQ7nulH&F+cPr}(Yz8Iz=C znRXA+q!^s!KglPi*-p!5wtHG}iBYVz{a;Qorz1bklH;SaFZkq}3_aCB6PR&d7jAhE z+vTd;{CMW9u7|SQd{e}=oTGmbHKerx?%p*g2%xWRTjJ&YBO|*i!DZEZa^E!MQ+% zrg`LQ)NdlrlE1!MkFu;;kZ5&^{vR%&TtoY4Swj4amoC>Q`{~@$&K>`ExTC%FukiEb~{S4#7HMt`&{?tkr;Ou z*6&LwrhI6x4;rLnk;LBh=Pron`i<#pIUKd_Csf48(BE{5H*bfAt|wA}<+|V*C0KFC zW%2X>a2cMylxt*~AQFAvTEFL$rQx?8zY44Ui$>i;KN)rxU0k^CCm1$8|0ZS2L&T}_ zuS&kveL9wS$g7kWFXn41;?%Wx_u_%~uk|4pg)#ybz{t;L+^r|pnOVU~hT-<(4C8x7 zjjosFj0=oH9>d5U0hh#`Zrw9gaGl7iaKpYY4kBcjb;^I-4xX&}vD1-~a_-{o?5ldZ z1s4P^l1cy3*DbIQ(D>-Gl$2)wr;mQ4hN;-vwJ&etM(Ie*L)7;i3$PElBsQ_Yvfq8Vobvlr+Qhw3L{f8!->2X^JlW>Vsm5V#YA4&-kX|+y>)(h9mZ)>l*?SlF zP)e-sQalNIN!u(q(!t6>J2`0b;rxG1T*IzMdV_kCMg(_r=le9sC2rUz!QRs9K1ab? zOf*i%HqC0_eXdwmU|Np#2=^1T@S;s$sq)Sfj{?9XO+i9)1A{zwNNwkKXrUUhAOJf} zhn?dFMr`{T3f4ZatTynnkml+iY#ggyz6(pi-e9%wl(QJX=P)CHLOj&o$1_BvtLc<2{WWUF zBxWmOK8}VD=opj6xjR5?@k1N};(jMZVkUq~{zF|^>2bXDHy+i@z&>|n;VyqrCF_^j zi>h4e9S=J`^RG!+HN`$x4R5Q7on3GGDEkg{sVjU23mm1UWlB64*P<(N_mqgB!=NfJ{JzIF`30 zY3?#WmL)R*4}k+h0===&VLiCV)t*$t{k_t~cqNfsw=zfS>1)!WQ);_qg9@XJ0hd?M z70|T03LPK^c&>2wJAcEj!HmGyE6x;`Eh4!ks2uQaI}xd^z?=m%9sm%q?&=Gx1VIE) z&8my((itOVtH7N)yEKS=z@)7}58L^PaTE%lUF@2F5HdC^<=iSVk+=SG?^WOFq{aF( zhiAPBFAex)uMhT2`bAqz?aLkz@IF8P@7&iIcFB!@CpvNL67S;MuaLnwvx%fg^=VE0N&z#$5 zp@?*z$WBNfUxf;fxQanv9LB){JuWc_hZG7q%GzqJj97F-l9VWL8$itq zyLK-gy}m>GDhcI^iW|%ImCOHVTfZGQ&O<=!CvHfva`HsJ2`b$7bY=3P>#|`Q4!&=T z-0EbL<#_ch{c%VZIfQ_7wvPo!JMSeP9_IHuCT3Cog$_HnLLL%lN<;PUh`c|wK3x4V z%IlH_Fkv&a4u8B=7YOM8N(;AGU?3hGqDmDwXA?(a*cmW)JGq0A8 zx1f)p$u?6PK9(KJ7U8C}v~(0$lv}!UcYxmfm>J}KnSH+nN;VJ_rGKBy=h2G7^VxLs zuS$+!Q5#(MeAR;XKp$%{p?6j2@hic8PIgC9ifhGzLG_dKYPT2-2?foF;t03kR#5H}eu=P-?<$rab7;M32VD8JFJR#_q#;_Fzb^U)I(f*aS z0f?f&h)&J!JxA`=qJSZ8$uzRyu|wP74)tx<_gtQyASnVLBE&5hvsJx;iI<6uIkGIm zIl5AOlwKd%8u%Th@51r9CxaM#l$IY`wg7uRRKV>T#evk)KaPosRn;>I!Es&sRXx?*b;5WOD6pz|`0d(41&+nNeev;4kx z=%wSoOQ%*R$=43nrYxxCuB;AQA7>KmFzM7F)VDpwGNi8mV2V1C);@CCv#@arY*;PZ zt!dF!vA%1ioWNt(~+ zeqZ->y)LY3VceuVX9xi=@zxQRH>#aFKoJg-CSRMfFgq}9^2^D|Ap)wpx;nP^x65az zzHadoRI3cv&t|^+;aaVEK&a}J;lY#kH+=m*9@zU+9F<@+Y;MPj3WCI2HM(Ipl#Wdx z`5aGf?YMTj=Y)Wfnb`@|gMS!y?8)r1nTR$B{OjuZ&40?lBoH?@ph)_fr613%=L0yOM zc6F{tqTo|+F35cm-N>8vclhPh(Ut`rcSAMI8;%vHn<@O}m{66IVTw-&X1TzCWPLrm7>30?C$^e)}ujaHxX- zU3nE`-?5!u9MRlotKc4v4Qgmj%ghJ#)UtlE%Zke7iL zGF?*2KgnH}#wY*b4pdj`(gEocSAvp$G)CH9gTKR2eQNl_g}e2Ji$_l2_To=aQsZmo zBDF-y#J8c8cyG3kawyc}%1$L+&%vp=Rg5J(_W1bOQG!wY#BWgAwe_cQqI<=}qLtu$ zsmte>F@0?@mE^3crnd%Pe)>7`kLui*{s(2nQqL>ja6d=(>z_+`(aGQYJaIIE{3!K8 zKctLeXU@5JPHx`cFSOPa*07t3F=XQk*HJ-@WNpDLniwizwqo@e1B<9KOGidbOba$? zn#@FVER>L}?x~)HgF2;y78UlaLHpy=(v`<`l$MdVnP&se6$Dad`iXTs69VcK8dHx-JPQZ}eI^Ppui z;TQ}t81vHua{KS`DT#iUhmr(pLT3wZ|3}|zt?y?|o0JPmCm%Nb&WW3Ws!k-Xg}VRg zo0w>yGgQIM(Xq&Mprf zh7y5W>|*;2tcrAB^c-STUOM32G}F$~aOfGGgXn~p4d`m8=zxK-McA`zT% zVRA}AF1oK%Ltr{AHDJ| zw>p%A+ay^q5Z*N2N8>94P);>*bgLsR4J3c>dE7U0B$9I|u+n(ZMlyr~0j1>{7Y)kw z-luhHX<3k~YO7Y2<(bQBH>%%Ff8l;BH%(6cdcQj9P}uCDcVh`BiUwXjtGsw7AUY$| zr+2owZ{p+k_{`51*tdsHZeRT+m@ zZy^Zx>!1(Y<{Kd|nGWTG0#j@GRrMA49AvgWF9+9&Kr^bOZ6pZ#jBY|nu&3CCmxTsT zpBX{OeV!<_KZe>aZQ;CO!x5|zZHTfv9(P;N=#&|6)rCe;a-5^|Qeh}^WY**M!BbpwkuxFAR=%VQgT(RoR`d!gGkXnK!&`^Wh zxIvU5M!Y^4~$#7!0bQop2GW`3hV25O4m8l$rjNv9C*zF{saj_Wrow za&V1lb9I88S(Q=@#MM`OCDe>~w@NJLM2W-t( z9P#6^1AB>5*~kC?oYsG5RIiVeH^gXwM~VU4)V@H+bNP43pr1m_3X85q=?;-J<=v6M zJ)jRo|J>QTbNNU`Zkg-0OKuywC-p`}j-KT`#PV7`<>JR_>F;Xq$`3E!yU+4$(K7kY zAuWcR{Qmn}sk47-R(OWQ(CX$u)0?MNEx}mh;H^<}!i6HAP@c(7dXttLtt_<>U07UZ z%BXiaZr(+>eA38T_e>O;Qb%H&0r84ap>UXX^2=Ds#F^Kpzs;H#Nu9c~(eTlN$rZD` zTMah;jjk%ebVsjJ3jm;_ArNRs>%sA;Vkll}O4C>Lp$SyL%?!|AhxWVc11HgG*ato; zEE}rMvPq_lC_uLz%o(-%KV-%aDo_KAM#MN0MY;&iR)C=u|74`t!na#FKgkGa1$J4( z+D_4=9bb)x=9igi9x{YMVMJs3ha90=fjmtqD8_Q3yGyeOKVGdS+v_}f1h(gLD9uoO zIsc1d>f3-v9c#9&K2UA>Dpe)g=7q{=1>U>2mkw8%yFL?-@+^9!m@=7BEy+h;0Id=N z8_sTOk6gXOcfjc-XlZ8?RBSf-6$m&cY;CEPlb04H@y&s#^WtdP-}=nrgt%LapkLV+ z-Y@K%%H-_>SA~smBJ zlb5T;U6uw0QUuVVy1F6z2i`0s-t7Fr5V<-=*bJ|fX+vUzYoNC`jFmIQMi4K=*#eB4 zx3>bkiY>+hCr0Ns2KZn^A3zDPmo_DQM$lq~DDBX$VkEY|RNSk3yaFS+I@7GDSv_Br)D2XdCUyQhU`g^X|p6mDJ7qRy)-f@r7 znUC&2TysQs?on7xmve<{*7`q8^h+^W zFObes7P77~h>PEnwF30WEnGuv**!gREMS% zS+k*A(3(}J(LGtmekHA_bjgcK+3LVSup*ffZ)oVq`sl=nL&7dbNhuzEia4w5h%ocNI2J+oG;$E5kQ^2c-|UJvc6X{2HtdS=Vup@ zS4)pZ@$+gfOWIA~1v!x#lH&&5rnumaPh+J)DJHG<94pQFiKE)W%5HI}ybM(~{;I*8 zPwj!d0kgL=$-kAw{LeGWuHWSkl?k0y(BDrPZWqEH7N4oCiEdKEW|*p+x2LCAFTg^l zft4@MKv7&EM-NcZaBRG^yA^>IHJvo8T3LghiqEkgoyuDFFKg-)-4ShhvNwm7U_be+ zsnjx5rYYaNu}3i`IYG9uq8%Oc6Z(lDuy5B|optqqbiG1Ffj^?(O)?>-JatD*xIbKa z*h4?xSS$!JOQT_spbu+*5yxoP(zLPmO*3SH_dsuI7BWfIB$wPqm$VhPAT`LZxmqlCTpUj`hZ1_X5?nL!?NypT9*3#~~<^z_yb_1#>NMMGo za&$Z>r#2>okxnKtuIzTn@(_=+aJzNAnRgQ=QhRX@3dbi72wa8;I>HRPEkh<$4dx2Z zX&Z=xSxA=2!^e-YPwd@sXVhe=MjiGV*lJ3di8vY^K#jHE`VZx|WzcV}YD+(>v{IRU zE#i;Uw^U!6^u0Lgz`i*0PNnF_3$j*ie+G3Q$rr*pT~RFT3{W7v#E z0G(rHQQ2mLnqGP-MO8nd4UHAo5AS>D_`cLxnTE-Bqr7K+;z?Py0)O-37_7zcI~TGw;k=$j|G{y#DWj$z|(VvwWLSMdqP| zk|~Gj^Jfh2PqDoc^RJ@Y@?+N36I-_J4Ez1B2A=S;2}+2>Cz}a12AHVc9l5YuHjW>f z5Yp*9`!;k0zYvx|^C{>k`_(^3kWD+cu4D3gtJ$pcN;Pxt))?WJfLm-w>f;1tl5+1L z#nyGsV8XNVw?WFd>uf@8!-B;7@-z5_gxb(2uh$I=D%$s)p3mpJufUABU{WAZl$gAiXkmAgcgygz})T>EFP_Y%O$Xf+z;cpd=ch zO76H?gu*fuC~BbKYFhKw`k3*C-DBZ#b`+J8*Y`2A^@=R3#$Uy~I;%Bg(#S- zqno|uVAk;-@3FKlM4VsMY(1)-H6M)2RwV{L-6RsB2(-R`jeGp|no14GQUimo+&D z{g@sZc?c`+&hUH_8^ZOa9+W#Ut%rN|PCDw2OYe@=bQCAaFXb%papI5CRg;yx>rVB< zp>Kb{Ype#s4m`czd;Vq18^2TKth$zpOZ8uWkUbACA{~~?<@P?EJ6u#oP|bLo~igV1iD@I zqR;F{3&Z9l01rSgviVG1ChiZxM4_EA;1o-jAih!3B|Rd3=Ve&J5kAZ0dj+ByQC16m zcO<)ModUB=2iOoJ^+FwQ!RBVu6kB*SqwGJAF{VnA_WTF*wUFCX8++Ov<=D&jf6a3| zsXwj!Gt2L)UfRa7n%`O3nY(v0Ye2izN6Mx9UXWmKH;?WISr&Jr`erTaS)pUD(%X(^ z1^t0&2VHMmXli$1Bfh4K^=&)6DR4h@3(}8(C1-U9jGP28a$eky;#XDWBl-!K>>bP| zr!Y(&w&J+GW%m+T`|pN?DPMVCK=5MxgXCK8*y}6DD%m4nX`dr{6tZEY+)|k_aqw|a$-83uIm*CZU z?oowda-ismS1ip8gRBIB5Ppz3tH$pi0xwtyH=h=uMMREp)aDMA9ybnt|R7t5~B(w|HcZCmC1)6gAzvH^6)fxLhDgT=o!n={O|% zZL*k{n$#SsFr4X9n=;OfI}f*}9t&Y!28t=)j6fbY)Pr2r1ehE-;u(BSp!cFMyf}*? zVzme(l(P^Vx>Hhx&(o*)u)kX>zS#mDQMNW>xp!n@o%&r{L3%p(CQPI_YymZcu<=s$ zc(n-zOi;^{{&)@P9cY_4_9PV3*p)_(^Wxyv#3CVgnc!C&00qdjf~EssF|1y>EfVWr zCE+6TSamW-OcsUZ?QdJnR;od^U&4X1Z2;-eFDpaG31T~1TL~$5F)UGTJRf2Z`*9A# zc;!76ol%S*zY(puF3&T#cXryZKYrSE`B#lG|0BJG7?ID0T^^H%9-Ux6oICmJfX7Fg z-(P~!;uk0blF~QhTcu7Y|V1VCD5mMxP16(D%;o5DD;qxu; z>2{`tChD4_MXAIc34X-xmnE1GThDJCQetFIqgSNR(Rk1)O8h7&(Y4%4_q^;aWI$V1 z@xH((k3>OgT`+Z^AiMB=YU`jlPBexQ+Q;d_+o>TF z?}HMdOHiv|Lb9&#Ar+OK(j?^#`xfq!l6Yxn3uN;{7Igh2^9-a_d52kQh3N_~sb_X~2} zM|JP1OrP-O#=km!xSH(~3xD8nTH*6*$vQeMc2bl@it@tnAaZZ0#S$LqRitA$WRqys zui_WbmRfQv=zuD~btgrAX*7lhb;5lc0}$1iQ=g^OJ67vZ$6_9v3SspQrYyfSCWY0j z+N@*9r&Fu9$l|h*zRhP=r+p$qnov?7#G{1ct_h=?1Ih2CY_(^8M^}A>N%*l&4 zND-k|cI^mPi(R4$76~%*I~gw*cjr8Q5nK5!MUYirt+b_qP*t5OxI)$VC4R=z*1jcc zUQ~K^w@Bs#odo2rVAX3Gdh3QlJ@dRa$Rfm=pr5PnM*uwl+xa@r#+6ACOx80zvAz$! z02{JZ_s2uI@679;xW`)q2LKk2eo9TF`C>YC=^Nahj18o^Ud4gQ>6eA{Try#hV9-8h z0$mI`L$3EFZPI6@?t{>INyTbSuIuRg3xO!1;Rh?5vqo2Pw~!nIiVCts?nd-F8(K#zIW5)_IckGW%?oM#jmoI_Z5ElV`8`GNmK8`cAOsON*_6Y za@Im7;k3|`9bKNU=Z>}e` zfX|`B%JA)c8ZlZzfI!!Ke8@{16}TuJsr9a#3{5Mn41u?f3=+_mMx{o%zFOcd`UkK=Z`=@Q#+jD)8<89o*2HzNMnkFtsA zD2GJ2=qOvGT+~M#dlYsy?c3QX7HI@(N+wdxi9sejxuu95#sJK3sl|8S%%9Q2%Szg$O zQ{9uih+^5t;~uZVqWfB_R8b^A9`XZQz^|`UIgm;H)Dqw?b>u*{^>KeemO;;2caUR% ztaxC(mWA*$(b;Erjs!j8`4*IfJ49X(dim!G&w$_R?t22PvC3fivT^5>+GRbZ+p24& z206IFbEjSO>ewaLxxLE8_lsp9J#60q;dbx z%JET>)AD-;;I@sL(?c@AINeg@{J(V2cGfrIuVO&+CC(-eEB^GeHY=pUC{a1RY+~_P zZiiq$#@~VHjG}LcU(ln^U1|j5sQ3P+1U#5h)Jz!wI^&hT9<*z17I47+@+G~SN^b|C z*Jz+x$3girL5=&Fe%9E4v<+n1z)MvZXa;Q0AN4Bw%SYg+5mLTRl>Tk ze441~?LMP&kHA5eAZR)HQp%QVU5Cgz|N`8Dph_iGT3)Wfy14=R}z4z%*i+ zn}2>HP0J`@>im_4(cd2#zWnb2N>L?#Y;EAqm-n>CCjYVXq!o=Tav#53s}t3S95BWT z-=NFa_6$Yc!;(g&8FitBxA9DANt+76`^@VKxlfg8xgy&4tBVPl>?a<$z=qYlhA+d; zktOk#8>nlf3#a16@qM01t_VwBM@N^SgrMc?JLDZ7a#ojbZqoEkyAu=YDw&Yl_l@HW!b$k;Pz3^F{8wHo=U!_o@FQ0nwM8qh(xo)SDqm=#asFAbZHY0~4l)gqI%$P#eNrAe1>N z_?U2=zk?Nrs6z3*rEWQg0#@Z;V&;r=o`>w@k=uJgA*z0HAU=3f`x$iW8R#Ej#s~VI z%BV_XDqX#qasQBgSw>hDQSm}!@%qWO1e@O+;??T%-&mfGxz+8|KiWU~>X!ZMjZ5_p z4c1u^pR%Pep=B{{#{PvT+K0^u+>pKu^Q}6kEV*etGe;~04MxixC9@!!QFph43dzq~ z#igYPB>tyCidt?B993JvQXBQoyrXAahP5ch67~Dghgf-MzJl(=_^s3;5M=_s&5eI| z;6Rcj5{kImZLK>_`>7~6ybltb^$~TC;?^fEaV}R;6~c0Wy9m*5yQkLHy8-yrP6cn- z?rJ`!hI9(tw0RTI-w!rVJ7tRp7hNWKe27)QEzXn(UI(;*R&SZs4_NKmQi~Hoxw;KW z=dmCA`cb_3ufRN z`T13{__s4Hm%V;#tB{0!b2uw1U41Ix)~%6yQ|+QMgNc=DruSIC5zdM&^cY5s^{<9w z2!e<^s}J8BS{$y?K@ggzEFD?NdxUIkDQ-#iGE}6px=<#!u68_C$?eorCl?pE{BMNPm`tfw>OyZ_!gkHsulg9_RdDLn@>rw9$WW>vfanaFQ z)_Im4mQUPXIa(>dK)H$Z8JLu9dmwYATZrmTbSWPPJiJW<+tN3B$cGN6 z=6a5;;aLDddL{C>f^0A)pDX<3Ci>NlT+yQ;C7U<1$n6fl9}Am1CXzOo=tq7h+a!Z! z-Yk4rF6*lPYDW*=gsu0ZzhS`Vge19RUT#EdK^fA`%!XOm44kJ%!NJVoi{69Yx+Z5~ zO$&F6{5*GwuHycuI4Zw5VqoTWFBf;_JR;SWqQ6I(iki>zog4a~(@!s7DpIAUFcyt# zN!w6k(*zW;jar0tF$onxmDzpJXy_LKb7e=-SZdiD+3$D`DJ2ehT=`nSclYA*>t~}b zzW0@KbNzlWDECZMz)Zj84= zb|JKoj$2VjRZV&$>b`9ajm=1^(>PY0pmi?4lQHk*xG7{o-gUahJ#{(GD#5#X@(pG? zFuCUs!s zgJ*^VOoN(ls%EF(l=q3CZC@Qad#&Y%%k`8L(Qot^@BuA|tewTYw}IV){L7Au^+T@#`gN`;E&%Mfrm}OZI4`I#Jx{8sUlFF#tLz1!LfBAW(H0^ zBnZ5CzuywY8s|b`OrTlCpkLAfCZAH;>BuL$!~vHdm`l9O#5{tyRv93)qOZYrlytCW zIVIkL*QcfAMCh8mE{fC*$rS?(>tG|B5juPea7&k3D@=J=phoi_>NOEm4664jZ`!<( z+@}F8!T7`jEZ|VaiZmj#khJMT~9*~Cv=vbYciF1 z5FPj>Stu+S%N3*Xk#iYmEIW~jK+ z!88iH1+!4XN>(UG^5HJsN?HUQ|nx%cjTL2e#>OkqH!`g4AI+J1Nvair}hLe(!87nD;!`@876Mt@K|Q;m6@W?N-` zV)?iK8$f#QALOV%9psB*H9)gDkW;~AcsvuGm#cnHT6!oz+`dyRR7v64fr^#!*?zmJ z&SCMOpd%xgc*QGCS20~ziOwPob-(mHUF@Na#CdnS4F&GW$pF>cw9V z-g&XW)80uy$zPE%J1tj54v_ed>Qn}LYP$gykRHFL>~7*yUXF)OzNNIdXNnyQe-Ov5 zhyM`G|KO)*|Mn$7UDI=LL0psjApmMW`<@d=B$ibOACAU)i)k)GRK-w};`W7}NC&yn zqooc@RMF;m(am6X>V7^A@h#9v*CNNPru>L|bJzsOIB9-rrQ&^+0!0S!?Omz2W$F+tw^2AC3?xU;BPD=gv0egL$w*YC zd22D{Du(5P;dmU?o}lu|+VFa$DP=$4p4nIwRurNu?pr#_g{=M4WI-m#+Z&?yWT?9r zx}R?Ymkyd-(T-#!)=pGJi$-CybU`@;YH5i0t3CbIR_4%F7N}PBCcu(F*>-hKu+8bC zlMTSmHLC<#Dspqpo8-s@qo#w0bxhVQY^4F;P{FM+FkTu4-7vrb6xL|zQr-XHC2XeW zmCUmQ;-@bDop2m+5ph*|zTtJ%N%**qe1jPdON?Pj}7bT``^B8@(;<4GDr~+LJjNIVcdgQd>1*kv_^h702m*=0uu4T}3L5>i#PjB8}7P^YdE(y6bK1jw_AA3UM|3 z>|9v^RS;K7D?4PLeH4Lzy9=ms=4=2&dO)t_k5nJN7P^BmPsrEWPVv*07d%097q;Pj z(3U~_n6b*&ndmu44;eMU7Bvx<ck2B z(n|}62MZRYZ1roeD`aQ{=xX1N1Sz85ua}$y?9PH&JvSpINh6KJs=M(Vu;}{AwJFd&K0r^OZGeyFy{14^Qr9$ zRYOd!@oeP=OVa zoy~j|f2)X@*{-Oiv4u6Cp~gzQ&gMsynXPP4Tq)Aw=lhjJN;&4UFK^p?wQ#TL$Ba9sabUqYB{Gxd^!WF&3P}SuYrgg^cz{+)8rlJ#dbj^Avw}nT z)gbDu`@JvomG1%65dH8-T8!aBkE}+10?9Z!_gqU;&g&mH%`EqREfyJxyr62o_eXI& z=ramB`}2*948fD>7YBVAjzHy+Ri%$q$jchg$4sWLWidS4%D}(P36ebuktA4P_+bWM z#d0XdN4|Z?gYz_|Ee+q#?N0=Weo#Ux} zRo#}%c$o#pyp$UVW%@aFLE*fcD^{$%t}F0XE({wN+zV+BS%sp(IMQMIGE+w8?p9sv z9JHP9cM1dW(^nOCM;nd}l_&AN^C0~WVvy8eXQbOntfU>Ne>VhIN3Tui&N=lZAR0GPF);=Ci9 z<~=F#v1ZFipI#*;BdO(B;WqCOJW`@(a!UO-;dUP1O*$8ucO^(7N_o@KFL`UUV^s>? zw;j&n&53cOuB}36uT~cZJL8ggZV)|JFOra#;11Y!U4)TuX}aA53v>pg`IeBQ7ig2e z2btle*CzvuPC%bRTM?zf;k!0`KG5bzNJLJC=Wc>Q{f$+_V0>b?t%O^+s=E+IV=a!WE%?Q_tljB&;7YR^6wjN?s zOqi55#AK$$fP32h7r>T?L*Bxzhf*Y|T@cStcz-&)I1_LML1F!gtx_DtW0QV+s7->` z>Z@%y#9wM%2@><0(6^ftZrKERW3YM$ez(yK<2+`dTYNrese3~Go>FkyZH6Cp(A!Uzq124A?6tKoYp|o=2}ROfL-}2-<<0U}>oE zH$?>*x%YEw3m+tY>YpCc z#Ta+{-@PM`&oQX48OjrL^`>p;TRskExUDs>^%V}4E0qK`_*E3jr@^y>C3%^i2R8KF z<5G=u&qvUTtx-o>IxqwkJFVg(gEViCcqHS~ILun$5+4LQganK_g$6LMwo6(D;yCIc zYYthv_8O>vNa1ST#-X%z81<>pA`B_>x+shJ5v9U+AB3D$zV=(os*KTgfMMjK@>zvP zgfiWp7fF1LBf0Ki*X%sqG;6G#@PCj2C+0H zxHf1{T&bN8G=ztfrZ4<$;@E!RX(;y4@fGE0^$#`wZY$vjFz}f@1v45=#;nZnT?oLWuJ?@tS(OvLoa6x(*~Fq zQw|HnM~uSuw(*L3hE`q#kk3KNg7tIXN`<>jk>1;es=QHw>OK34mv`LqfaSGk_-M!X z=+C+Ne;L;-hID>OR_#9La@pBUX7uo)dB6OlrdTU*@qbV()G2mirvlC2@8)!n z6QBVh7$Cv$(zuUwsZ0C47hc~38=K*JcvhmoN;%DxIH z=mP!oYi3z5^V?El{m}C)vFLqAAxJ<3$l8njdP$FP(hMqWhS_|C$dT+n|dJKA~xD*$ybco2xPuF(1(PE-vxNk4X?gMnU3CZ7#HwD zc_3a|sC z`#Xm<*9(p%e!5sZQ8H)L5H;X$c6@H|97k@fzM^v`_)heeaK+!1x??lb%8G?OMlC`4 zenI|Ys9i*zTs>2vFSxK-ES3{qu+(N1M6~9L#lX&~#MqWjTbRdtP4o=R8SSWz-9#Rh zJ3BgO#IbqypK^DnQS7IuVbUA#v^3N!hfR%@-Ko`wnL5+)susg0^eDhk(s6eJzuyvB zlp+Gudm%iHK*x3ZQLak)tp*ozONXiF<|J&^d3?*2xD6?gg%c);i&TObtg8^cX$9a}a3tqPB305W}W6Ml~P#bj+kJ zjSV~FH?Mk2OZ_HkYAhp6duvH)A+rVH(0qY zuIx=Ith$ailKA#j?3An{CA>@+;QOxZ{N;5k(?2@xeX$Z#)p?ME{nhd(8DjLol+(tC z^9hSe$CR#TvCbpDP2V#c?8IDZ`LObJSn#cFD5G*mS5RH{O!{KcgUf^C8l!t75Xswm z_v3Y3Mj#EzH;;T5(ZSA<8lE)klhYXEIN-<^>WLGZ%*xT}U)29Sw*S%>Cz-K^PihZw z)qf=iDvS?kf1Nn6ljj)51S)sB#F>O*)(kA&c3ncck65y(jerG$4LGOIoyK<5b3o05 zLRz#M;1hjXh18VduTY%jwK<#Lyp29RtgepZ{+yIBs^A)NL5ch(mL=5o%9Q=RO+e#4 zdEQj`{i^3H{ZGTWKgnNuH#5IF{#E5S_k!<(!}J@@?qy5!@5xKsxh+%LQmRv_v0~oUMm80u{M^7Twhj}% z5YL9|r?JHRr@;9UT)_gj8JYw5?z{uB5r9*DO4gb-Ue3n{B{~+LEwD%Jk z{~lmHVx)7SmR|&gJV(hauy869VA1x+iWTPFjul4ghOP7=U;u%uy8qo0PlAhexaAxy z>gqPVWfRmgl31cGT)9lmNQTYlw?Hi-4FTE?Fay~DYzPHbLZLA@I}q-@%ud4BOc@XdQi=x44_+=X=W7CK>hs-hS#4WoPH>75L6Ai#NBDW=s+~ z={HdH9Su~1w%iPj>xzd>?w~#U-ZQ#2-6#<6<9LW?}7}tV3`YQgn|K(+}0GRuq{ctY`mir;(T8ePfj2=1{qb>G@ zUzIpi86)hSmWxqLOT7c3W07|?eu^%&z)w|W8DuS-uCPw>yM%piE;m{~qi%f7 zmwk9I0#N5J-#?NIyI*d5r_pTP^lRu_%b!J-Oi0mM_0)~9NmR2@R?A8N?N z1p2lF-()ye+LgG_zNi{gCH1`L$`M-uoJCC=8qu#$L4~bH z=6oEoS?!M()cxiNHyxxH&bhTGgVGaJof z7_X~XNfc&Kd_6eF3JglKme9Hx)yK%bk`^qPb&0bI4YWa|`Yq^2l-EHK_4eC?qAN(U za$qQp`^~exsX%zNF)8k|Xz+|tlop|T=TL#LT0j%*-j(TO|I2d5@GFYBZ~c+?EWJ5K zU814iSl=IoQ@=SM97xYtG(+rszT7oyYmzwKH?iodcSX8SQ8mL@ni;79x~yPu&_Ne( zDC{ZfC*6KWLcpEec_z9h!km*0!(NvunJbEDU6OFr%>*&eVS9&VUzt?a!Xq2;5O9B; zGU%+#2pcf*FHpLF%=Yfq)L{76OK&Wn>EMplr8G9XHM8UH-^zUR=W*i~zZm|Hmub*u zb=W2M2*nsYZ<*NpuK50O!b+Ss69%f;vXBc$Nt$ta3T4M=-h`@Bn1G=S| zy{v=la#l;G*((Ap5_-3kPgaEW%=6*Bz+}?q(+G5I(Bckkw0zBdnk)DJqQwr zP-EGXJnCV9Oi-nYPAXALaE2xu)uczvxvM!0$@*Rm_Y%3SBHS%apH`Vq27bR}$rYB6 z)jn&=dYKtFJ6R?CEOF8AQYYF5S6r21l(cfG0X-OY;JK)U66Qe=j5oRPacnL!eLK8o z7~_Xdl7{D!538I}*gPWaKKc%Urai@cME8!1b=+mHpr6L|^u~?N(b51L6dX~(+Y1XN zBh>vrpN;jctnjYNaJC{-#};$g2h+Dx6a5ng zMR%}ZLX`XoNmI~ExqgjWiw?c1Ia+s)ePFah zoY`i#&ktd!9Wh$UDWmQ*n0*^n;c4}!NwT$3ddLod#m!0RgWI65)9^5cVQ@H^MJUY%f zFwk2yjo33e>Ag@@on|AvwSBF2VqC2^WSk0J6s?}jI+0M9FL8cql$2%qo6jkZ=S`We z#F4?1x*yW(*Gu0Q83o7snVlk*4_Xbg{M_6pItVQSX1B9LL9X8s3+3>#e532Wx8DYD zPdVf$|2C)+dBUEuaO%UVNCWY2qxbn;f7Q>m1#UUc8l31IJfi&{_wz5Wr{r_~s#Mta z5{-@zy>LXHHm3C!2KjoQeLXH=!J;tXy31P46fkikw%wTYb`CyGclN(f)3= z$x|Z_rRQT+c5)p?78>wjF$|anr!qZbiiL8h#Q=YmgNw;G?s-Mc6xGjSJDP&7dTldM z^8_k9Rw~qIbk$pGoo*!+&`>p6);o4<|MQznv zQO#NXNDjr>-FX)w3ywCLO zh->tT%>Z^FjaXAYdaEr6`@5jb5zuKS2*RL-1k%n&2RQ<=wgJ!3baIEJ zx$l5i)RfK=JkH}ys4$tnOo^>;>nLjvdX^G6TnGl2_4BuCJ zja&`gzF42*KW5dIKBq$5j*_6>4Q`Z8-dWObSylHHo&j-fVi0ery)m}Sli-piT-ZX+ zMGkF*Mr+CWlvvV=Whk>dQKH&VKh?D898b-DIR8Wp!=`@2Ta_4k9H0=ukVjEy7}u8b zQ^VBVqme|!8ab5*YxQ@tvC)rj>BZY?}pk&RtBJaysy{?LaZ zPC8NAMgv{Yp**?z5l9E`|15_#7h&E?3S;&n7E^(04lF0nY^>%>PKmy1*GLfow50Ne zV*%~x|9tj+my3dutcR#3#y(B3ZmG3a&~U5~^bG!^-gaCg_5lF3AzwyS!wb;#0M)Pk zRBT-kk8huy0-Uf>g+K+&;~-m|Odl`pc^y~Ol9^v@`TfBS;a7LED__-TfT8cjSE1*B z+;@C1c9c;1-^Fi2xk#YN{CgDGNAt1SqCH?>Ws8WY_XUQj@b&jCfqW(z12*>JR{e64 z{DuqbMlAm_2~MPW*79{d%Z`)2?~0?~*@J<5H3QE~j`ZyQnf&pA!{t-n=eMqy<;FbT z#T<&P8~b`iA|o#CZH3z*_bijc)^ZV{bpj!1(MJU7fbE0h^48!bt4Ela8gywcQBAG( zfgTqj`*_@o9JJ#IDe}_$(3Mq(q7NeWi#>vMrpgpm)`ODolwiZe%)UfMzE73y}W z^J|*@@!oM6n)U@#@t1e1L(p^1S8iB|6l!p>-RFSFD&65J7e{GMrqSJ^Gio|JHKc!k zyTGL7(61jk1u_nse+H5~h5J0XxIk+Hln>eP7SSe$Tjq%s$`%UE$uZIbuTZXvKXG-^ zZD?kR2Px-+DR@cAOgS6Y<9-aRsh|8p$VazRU1B{XBi}-*CG=o_9hl#Yi|4IfVEZ2Q zpfbN}eqQ-Y)p}zsbgr*UJu3eHVe37hnoQTOe+7aN0zyb4)d3_xLJ>kSh&ps31f+!; z5JEr#2~sS8j6(}TD26IsKnM^aQbRy=XhEulpfn5J-gQRj%$)PxIOl!;|8K2tE!R>6 zC7R@Uu6tj5|8`5|!Rp^{m*q~KUahz65b9<>4C5S(`w4*T&wSS^aaK3gLXWclNjF~# z=YZWWVq`(K-ruJJP6`6YmU3Tm8l4p$vt&P7oOf+_ik#}(T0i73L8wlHjTo7hPIh7P z4%(GC9UAJgvc#18+tsj$e>e;3_n50>TjiTQeYSPs-v&3I>)#kXO{$wc!TGB*MJF5j zT;_&wB>0(2alt{So{N{vD#YFVx~45JMEqtDhvf2v0}67yMz18NYs z+cCJIJ%FKJ0)OfN%|M}pcE-I;zk^EhAleZIX2W{!dfAT`s82Cd!%Pn2JtQ2IItOZ` zeL!Qa+i_>iSwRCa#*zuRBq3w>ba7{@xF$V69M07B7-GG0g3 zp>Om+^q4?L#-`{Uusl6B>E(|AX3#3dMHTo(faQL9g&fzjBFMAg3yc5^eiYL1WK=a+ zH;!Y$e--KqM!|+BKnFYzV=zvuSyTmh$#Nbm%I?U8jYu6b+aS$79JFQ+`J`3+nWUd} z>NpzvVU_xuF$U52>05+O(8&Znq15}w+Y(*MhjtZ49R}q2XDK)36txzeQoBCQM*G~* z39I_ss7CavcdycC)Bpn4dbi4<5p;~cdaB+Y#qrUue)VM_Imy+hVNw0za6;9LQtLdm zrt#A;x&~&v(0b}Sqi3Q{J1SJxzRtgIvKQ59yQO@FgjCt8FPDB&a`u3BAZ7n=TdHy~ zHxq$Rx>Gd#^?d1@qk30jQX?FT)vx@Lh1h>N`X{wlU%tJtG?pI8TO6{KRU1TynoVLe z)Ch>2i84J(H1L~K4TBbTV>yfR9qUXBp_I?iSldt*0Hy~WPk~(#Ap@qLA>;GhbArp2 zjYh-*#RQ<`!pjxH!QL+CjthO#SloJSbk37%w9}W18+s){Woj)gn;Z|ln?%djL%o7G z$q`1xm_J-v-4yHXS>xG>%~4t+&%QZD6R=Xnap)&4e$-5y!W8= zTQg1g6O)ELl9=8_zjni1EV!S6yuIzdv|=fAki{ghGmZ?0$J>+|c@=y&)&Fr!Y*Gs#IE=Ax7D=>V~A$T6KDK~ z?hj)1w+s%i?FJ=B(z=$|(|L$TQ8gbqj?mjpCY&RBH1spngwV;EtXY^#T|m1KbY*d} zFb(Z%6r^&9tf_r6rJQyn$EK)UWb94YAP)OUEBPZlPZ!BLxMN1fAD9#~c}|_FWks2S z7_EL{6M%p=`tCEEy-0s|@d*8d*L?cCFQ`Lih}h%%&tH$y$}BEVC>=12;MD%idzF&o zo}hn%xzSr$l~nWhJ)E=Ut>J>&3&#STHj}sMc5Rc|s{A1NHEK~ASv*&(hflRNIx;=yARQ6U`TS}eXBnKrZc)4 za25kR4H*4spvr-~bAR8t3;SGA?{w(BhwMubG9tj8-lv!aO8!7-fZR~taAlo)ocCXS zC=9{;Dl{?->T(KX^#w1ITrbmUIoRVUv}+eS&1M3uahvAy+IkVUgD+_XuukfLkxq4Y zNW-}yrq0n$3TNaR0t4QFubHyon+8gjBRP1_CNOkxrxcm8;AHCpY<*qy5Zey*Ec>P% z*ZB@^aB|0n^9UKp3L<(UHvmQ=LZLISrEKVOb~x^I+6jX{L8kHfs=;gLWQEEa>*R~~ zChd=&aJmW_kc@v(>^3#UR~2pZY-_s_Z5rZdByTW_AeFd~Zr)zq}qh znjHhW=HUHqVwV)jw!LK1z?v&JgN-?_Mry#YR6WQLY{_sGzh~rnbQYU?XrhF>yO3~L zuHSnp^V58csp5e%{xES)=&V4u!)PhTVyL|3Z7IrSYPF|rgc~EUu>r|SZF3;BMco37 zG18PVGI%^=SJXr#x|n}**GqGp3)M35tR+-!^D)R1Lj-<2P|Wy0Pio-)MdZ0}aQ-Ez z`W;nL&j{-rwgrcwS9|m2uwX()1^}g&I|8EOK`gcNLK!JQ695B!XGdMDg}ji41Xs{J zpv3x1F5!D8b^BG-Cdbz|LkF^3W^4EO)@&ztJl_5T%yoNzo`)!HZ^$%HfyBrCZ;<~w z_;zw0fQt6f1-!lh(tHB4=#QrYfDT}3`tSM;6+JXjHjznm@hY9@$B+CP1+=>^qh}Jz z4w>X^W&7WOqIXo^sc#ci(RL4;EURLz#Hol9Ee`vK_1tITTWFsJ&{!TmDfBaOfRdvQf^pFwCW zyzZdBNBSH`>*H97r{)1=N$UeBW8*+$lwcR730vAXGVePUi^z=k5;uOFGjTc_qlqb+ zE@p{mExVZo1bk3s6!Ix%8_GP3p0?)fXEM5GJ?~l4k;L7tc<9H2wD~VXpzFq5S0h0)dmU+gaGUGod$ zsFJ2?UgtMLLX2_2`%=7I%Cuqw=Fp)Zhv}`|au$sk0oK?ALz?>ZcrCI#G`X6z*$-Fu z;p_|DxE5_eV2bGi?5L6kBTw)tv zsH!m4xP*8&M^FHum#wg36H$zwV|2psa!A#~ziwR^zLN61x4ivDPZI6RmEx~IclCa_ zd!VP|=x-6vFEI1(Xh^*WSxK+nSxMjUL&Sxx-0QQF&N7ccHIDk35#w-%OQ4h!h!InF z(2GoiVzfoT8UVT2Q`5qu6F1u}9-8#5_>n^KU0yl@pzQO+#h`ylkxgvK#+r!}KPWey zz+)@}fHrb^K3;4CXosaF#684-@CFQTKg=lLFqt){FXRBFjz0o4y~hAN?f`xb+5uNmg|e%@ly)`jBWJ?}EheyQ7yuRjmyN5VctiW_$lXWb4Q+b`+Pil+7}1x zdn!RkkMOU1oVL2vZ{)0dC^1gd2r?ewMS!TP=8RCh9jrq2{QeUl__{ru7I)SQTQCUe zca(?o();P23WbJlfvBO$FM=e2l6K(7++X%?GOUXA71qj|#?+^$!8pt|67Wp@Xqa9I|_J8mnu4)>{XWDu99v`t=ZCbph4& z#Ruu6It;6iNwV2sYpsMHw$}&f>`r0nK3J1m*p$&d$*2==BVc+7G6tY7=V~tXk#7Br zvT&-JVU-?`H&#Nn6q>ODC~E!=N?nwIJP(h_G_J>SOv}i9QWnwJuo`+_$!FyNbonV# zXt7A3Z3?VVL3`&6<}* zyP>eL>T+nW4G*4a3roYCbjO)pzrsV4M!htNh3wnD8f>q?q(pYY{y2U}*W1J2#_ceV zBP5-?QqU*-?|=Q(!&*0GdhV4z<(`oV+b`L?W-<2A-C}2MvuhPn*W^}0ov_4tlx?3i z{g!t{_9I&6C6Oh+&lG}C#*eIYjx4?%V}iWGdUIHTS*g?aSf-_i)Ny*LMQSE{K?SH6 zoc{f&hv9UyN4CGyX}k2whKVl|Q}PcmNz`UCP}JRf0fylcUOF-!xyMtmAf7WdUwOFNY{J>coP)yYHIWVNuVYsYq{(j( zKKB@Q7(7n>x(fmtM=hu-M>Yqu)=>DQBS^LkoMwzwhnw&c1!hPs9(|aqd zWlu_(+7Y0`4ctsRcP*J*S^XqLc*3$}mhO|}Y(85p-a2U}evB-hQAB_NggHE%W|?A_ zk1^22g{UPVOQ+XWa=gNTZ_hi2pJfO#d>vwbrzEIK31xt9shKhe;gh1@wz99UAbxA=H1PMRzG)@UB4ZL117Et1Fi6YytaJ?@6uG& z`lKiqXsiVSh~J79wmnWn#fYGvkQ~kX^2lwl4X#j-PAuv}FcRx| z9Q&?q0G_~BzLRDT9@=0%fp!bCA)3UUh1V6|T`J!7q8Dys8eVt!C2%rbMqiSF0~*Ze z?*EzG0A1S{%aPd8S9|h&%Bbvq@W`6tF@a2(>*=LK3!qfeCzNCg9^MDdLHBNLVCP%p zR#BMEGRPxEtv{1BGucJqXMe-8!h2}}qC#>=9_4{qlO?tuI$52-5LgfAxsxstkvKIs=Rk=!i=Pe98 z2x=lun-G2G(F{K5ZfW`yk7u;C#-+eW8#z@euvA~al(-&He*w`iq-wQki6^g5GtcjW zM6+m|IwcbHy%*m=nz0+-sAXSU{!E4M^PX2wGU_t^?9uL_K^ObF^+7_K5Zs6rPqPwB z=K@xpigYaZq`=Y~>-UI<*h3IQQzd9p&|L%VD|gefzh@h!siR0b-#CRal3|lmER3WzC%|EerEU6Iq9{7fx*I~=tbjBl{o?8AQ!VeH1^#Ptm8#VcJ{jAs5PX9yVWNrLOXj&IxM|KL;S<~#Pb+eb*EXfM2Iv2Y za?ZwBaNI*S)FP9bK2DAsvU8F1?;ujj9m9KQobjRe)K<&BG4*XFkd2xiEm7IXdb9u; zj^uyah>j6Q_O8VSss<6E&(r{T8`&2(9NDM9WXub}U>X=*fZ+>3AOqO(ZWbUx*RX3l zN7WTu11U$MTis%r75d7i(%;p>Y(~)cP8*h!RUL9eEeO zm>ZMLw`w{0XNbtuc&+cii@oNL&IX%nZ+~`e?(=P(_nJ8SPL@Ri{vsG#4=_FnAC zhKL{jMe=A(jSdS_^+NJKSB|V~HeO-e;uEu^gSN>Bspl3eUoGK@-FjX+Rhxc0cd%_x zQGb7pa#O2R8X{*v$e_BwJ77}Bg&iE!P&?BQ(8Uf@m?WVd>us(MkTFR1L)SlG?~HI0 zN$~~W;DU9YWBtU0#m1yl%ZeTwctdq!UB@df;{*Fery74(?%$g-G@1XRV@pxG4KAXO zRx5xlyGv=TU@qN6cgT)dm(PmYl%ka=Iz{jn)%=Imnsrp1e8dEP@IjaX;|@|5<|$aS ztq`)$X_62CG}?B(2qsMutI!x<-YSxDDOt0DZtM=Z^2#_TOygTbUtq!L#o83~>h0*x zdaECg~?l&8@ce+H1`rOqX^|^E7BT>^VtGMJy_l3*G#g{Y*UpZvi zj&0z2lg&CQ6-YN}6uC~D#%&~MLJ8aS35EC&_>!DTJS8(?$Sx9!tU zO;AXo(W*$yiC9dLTJv>265C-y{#PDtH*CBd(}A~u2Ke|0b7Z^MQxP(hyH~D>q->uV`oug2_UuY~g}#@!L|o$Hi=f<~=9zkDWL z!4y~6DfLxHRn|l~&-hQyzxsN@^wHUvn`^?{>_J|i?`hop>FFZp*>M=+bH(RCMZwjI z*HuckH9o|$GQz{@q1bq5$=NahwJoZM+Ik>ouz4TMX7;SP1fAUt4m8WyXlzoL7-!KA zA^?S2s8{P%F@km=y$BkX4Oz=G*qh?}fNBLYl_z1)NB`BsI<6;@z|=-+pxwsymFe|K zeiR1kGCE1601N}po50gf0t-y=hj#AeAOgGpn}@4NY_7%)aTwT7xZE#7O^*6U&kmUT zZ(;-e8vKVT@x%aKltl;VH37|h9v3Ub(o;)(H3C@vdap}+OZavm)fX=HcRV`Hu5+T( z{QwHf*jbY{o(Co@K>5IPtM)HxT#vKMNTk@_0B^o^N9-$K4knPON3u!N^gcV}nM-cz zhE{;#BJdfMH`Jcz3E0uvT|Qt_nr;HJd*#O+ajl0O%=_M!e~#}gJ0AI0WP*&wxj&Oc zmUA(B*1V{*^2v8+W*uHEr>yLS&28sCdtn-otjeCTeXqrk)pB3ZRgBb(zp(E9#MscB zsOP0s?#ClTG%rHp@Z3281H_;V)Z(N-US+Jsq?uSOz{I5d7=OcN9< z3f-wLAwgtYK-9mo+ljPGjn!&ADPbx14Qi{aQ7ZYG@1$|VF17I>UaV!4%@shIO9<|r z1&Ja>Khyn;v?IMU#Qe?^Z^uJt2X%knFwt35SoOkCpJF?+oRIGHB1E9Jd^eb)fy`jT zko;CEC7haa@8-MhUWlTWOhp5lO44RpLfIwBaH5(a`dTi$r!thXx;VF7hsPLwr||ZF zyPmisn{xYqIDr5l5jg_dP0oTuGmv4kAScs&3Yp&79A{Yyb5Z_*5{b5gJm>6Vp{Fm0 zKvrmD@SvhkKd(Hxh!k%zlZ_XJ{&{%`G?BeF9bEBwUo~rWZT`qMD2beXr0ZJxpSl0e zW`^sf0)V&xb$sAu6sx4X>n6%g2`=^qR%*S13S9)EsX=-dEG;IL_|~W2{z;Zpx9nlV zz8P52`O3uZAd8hkiESfi(M^%fs{n*m3nw~3HTf3_!#B%nff?niM{lR^^_^P~72LKg z%{%H??M@mvl0CiF9p{lF{Y=|k3~OdS1XiDu%D~p98m`Z(Tgo08hJLCx8!KgPuIlwf zvGUTWZ9VT|aZJ2ihL4uLyGg1$iU*&s0!KC?oK0w!x`uIMLja|K8 zjp=pJMcJ5Kis^?hE8K2YC_EJ@lk*`}gwe?SYBuS05-#^KsUEM=B?D6=Wz2iZ`5M3c zn)oeaZ17$VuU0Mm)%WYmfueJFPX}d<&P678{9?Xk=#JwS zcYI*|%PX!~*OIADH6Y-(TGYlYEGC&Tut3rqH zpnjrg!~vIOTH+)S718!DUDj1``u4QKJHtNxm}ToTj7ek(GXMGh?|Y$(i5ASO?khZh zm|=&gGkWDV^?eo0@8!~(&>8mI{_s1e4qItVfmyguY;h>P#aMHIk{i5F%y1_ayCb0e zm<{PvaNXgK7&i@3Ksri}BNKSmczMM>vT>Ng12SJcdTkKGi@_ z{@JkL9?}yquB^d!_mam=^x2)^B#mqnHDkz{^oyU~r4}Wa?mq->X)tl$dE4~ps$VSj z?eVItb>O{f{^j$vIoi|HzAZ_Toyl{Gb~QRx$Rkgi(Sx6Uw{^DcA5%A?jK>_B`G!{{ zG|glS=3K&80GKlg-Bk_35Z0E{cK%+L8p90zGKqshU5icd^!`YyVwD-sd0_Xei{8D@ zwXUj!n;KoLwYuH+iE%}$w6$0?zWGHZ@_usf!2MG}jm$kby$n8sqI^lbxUoIreG!xy zmP2uWvji8~_u};U53?i!a#dFCqApPC{h*tk9js6iF0K)C?3)a{&e6V8bBJmk;CQ?U zHp2UHIqOeeulFejO`l*}P^*Nz2=Pi-kpk^l{;_DzPM(6>b9zO;gk0^9QCHo<<#r28 ze}W9#l`omB2t-i=uh&VV+pv$KTX)K-U<@PPG4Xeh@AlH53=J#80GUpN*ZJUDD#`&r zO_S|%@SL~~OI;NkpHgA^h`?WLRnRV2BbD$ZmK!)uCTNnEwo(xfVw{EQEpXNZC6m;4 zrK6v0@>sV~y_Ofz2TZ!hNA>8z&f6;|duSWgry@6!Z%<8qzjoL7f!$@5Q^*^+^HGPg z4<6_@(=}dIk0WTC^MaL-Q0jfqTFTua3Huoj6WeVa)w`@sut=UO^rItrQ=6ut^HEo! z{FA|m$+@XqZ7PG<#`xnd^G%hs!|@XZDtHQIWGvcHI30$yY9%z#@*?3Bp=Y# z`IXjseYaQ5u}de%Ug2J5+q+zpo38Px1hk8xGUvF7?Pp5yVFm2Bh6zTR9S+d<#q?}D z{8nz{lH?0Vv1RB_1>aTZeO?elfuV_~FHGumfmyC+xRO)^w1SwA>wQ(;iFVJ^^N0f% zHElJ-Y@)7;gQ50nBePeizkntZ+T~OzompaVR4WJSeEhiMWU9#UN_pDLBF+(p)XRL` zoB=PSAK1WU>bD;PlOHxpde|AQ4&O>Oo4CfN4n?B`S)&7)n!H0Z}Vf;spn_upW-ZW>~SU3wbri@R@-LJ-I~3)%~es$0%ZMgfuzIM z*UIUng67ub%=_EVPo&o-9J@f-cS!9rd1(aks%x{Sx3WK)X{o85(xo{vX@;TmHODIa z$vtLHJs`VaMsV8$f{Q9H_FHLfr8tnN!&28mx{*6TiXZyDPXVUS3!DLWUZ9U4I~mu1 zK9LA&joREFm_0-KouIq-f!{w3E>NmML(0xz^gja_xW19x+2 zzWN5~K+FN84cFJqz#%RdxSWD~HC!}u941gU@C19uV_IF!;ti@ua-*~81_LFO5V?a2SpUSIK zON$UEpC6?)t%gun*RBL)GouAA^ z!mZRQ5g)-Ibv&hLpT-bLURKg3FJHsxCw|bbSLUXKO|aWkhAMR3Uz=#HHVkj`15A4; z=;#n%*$s3kNO2-}YpA|Uz$ugcXj?b*PqZXJr<}y|!!T|OxNB*0`0I8c24Z)dg9V z{&F?{O@Q`+wTa!~Z_SL8k9Mx6MtF(*;$aq!#U?(v`LDs$d_wl4^6Cu$SgvJ zN(PcHIe-j0VR!Xrmy)cf4ro1njhVms$78Ie2!jL2~2)^@1fU=Co-Oj z4RcG4)vfVKMl)|I1Aq8UxrkbH%_KVxMoXe2SYDPot^nJP15JMpKQ?*X@Dwl-%l< zzz~tMqr$1??Ea>@F$Xa{Ujc_`!^h7BKCw$}O3+8C zCd9scTX5@dx96sg`$E;AosaiSOd-boUMg5G$+@0)AzoBs9C70S&e0I_n-Wi{zRQF( z7}v8rXS~c93h~)-1$!asM*1;LCV&YQ3hjdiii3)I)bd6VgmGJGdF2epK_-0x(y+2d z1Pjm=s1J&x;;PwHtv5Mtv7!{ zbFZ?NcKsxZ_kB>b-VAPTr%fKJ-AcI7jCj#2xrqGYaJ?$U^UU{a^kbhMvnnH|5l@2s zOs<#HD()Aj6=4jfiBmao5%AUpFt*V$d7;m6S^?VfZUB@>#i(_dmt_SYfvE)3NDecA zrT-eL9*J}Pu$S>E=!UKralG)!(#wWFSEh}VV|k}u)W;{oY%`89&)w~IQ_E()>F>xz zSI4^$PIOE{^n54EAaYA6y>qYASTn$T&A$~#FifWUL-L;?H1Cm%QLTXasS^<5V0JBq zMw7ouDRqwqajBa~x(bCm^gFnNUC+Y)rggnPlu&3clpEn@UkD-1iog-Mv?EpH&5g1n-Y6fCYa*P-K}`SL@V-PL`xu=BUBTsc3pKc?rP zTK3`m@z~|R&UZiA8+<4$_;^1rO0&D2O;E?J`pGx?lrv=IW0%{ZalLV7LhsjJMWo2} zt-_KbMX8Mq5k_Z8!hJ=b&_d5gBh};k(Z-o7TR4OCv*I6S{B!BjJptV1zd2KK)st7% zb@;c>I{E3FaHzD8C!{;B!g1U8%-&wqzYoQm-<|I6z4fB3R+#yB1kB>@bx9A6^>WYR ziQ~lCyozz`{`UC$jH}SCs7+qk7=DySIE?eR)!0c8u&|fG&RGPUt6paqG#8NnMlprSR z4UoWmJ_!RSt|vADtqijhyt>|@`M*caos;KN=K!JXSwMr+(~sLp3H)CZ+kY=%7t7uM zi4gqr$(Y9Vc1pXic2-AcfE!ApZ(3Gsxjl{&uoM0c)UuT?Ix{sbR(ki4rAIV|+z)c4 zII4@-rLYx^vHH6xw0$LonYjQ}`7s%K(KUD4y8tvTew45tW#YS=OggKFVfh*@La!er z=1|+NX%I-`)?L6cH?Z2smyIKh^2Ay))jvThH6^V}GHo}VsjR$khjIrUpFlAS1FoCB zB0ivGN?UCvqWE9V7~`Q5#F(|;*{*PC_p^ICb)y?8>juAFu5 ze5}Q#3e0Ucw6>w++srH@x~2?>@sPd3Jug1q?mX1jiuz*6!xe?fFtm}Fcr3rls*>cBdp8i$FR)$_a*Il z@YE`jH6eg1hXrGs{s(jiEzLJxXz}beSPu6lujzrNj79Fqp%HLo4;1>8am<S2&G_n)+|uPkr<-9@!erQ3)!^{H`WJ;o4hbh~p1sugT^0p} z%+hN|ZSr6Lsg0~tqU@hOE=hG~uHYKp4tW&d0uA}rx(W{@ZF=Z8zNSi=P3OoQopmry z9rq+WjrwC`p4!sP=E;C~Bw3;nat1MhvbH#n5bDtozmJTw=R;ym>^1Sq5|nItFT<-K%7X$we4#xBGBOjR{5C>Jka^u*C8K~)H~Ae6Y?{Jg(Fel(i;OL zU96oaKN_3ZR0XIpZlvvFFy0oTt0@&qpfc02t5q7O$agZ)lzss?bRVuA@DOPMr1Acm zIKFpg*&?5alGHod+Hur`uO_u}AtG2%w4^j=UY1^girg3UJ1FoZiZc=1*0iFo`tdoj zb*V|TT$GyuVKBDraUQBpzX5_bGWMJ zou=s@4=HyTkMDW{uNaHpQPnz2`8=gA0Q1$`qZ=mdc* zQr+yil$d*x$6LPt>SfXqu$|$b{^Z}eM~@YJD(DN=s9wK$obdaT8t#RB=0u^L_<|+z zelx91-QS^Yo)bdN7J#%8^kW*tj|vu@8Cre_!j7`$ z3+#P>@MgF}c>`bB`y;L?f&!Y6A9oeF+sL3KgQez(?crHeMQ5o7()EU;qf$fo+FXdT zYFHG@gyI`w`*orO>E^Ez=9Ki4K{h>`a4=@fWdUar)^||ONC%meLazBt%SBpM<1q$* z3f)c)ZGMXs@T6XrPRV+XSJ|B3DE~aZegb|LqiwVNma@eN` zBT#Fs!1598OlXD!ly;Gr>Ie_dtx)ne2WVmjV6N$^Rl#oKA7@ht9D(68+<`qXxxkqNDMGkY4X(R+mx`4nt$XN4{_Vsq27&r4Rt5t-IhVtbk(%WtnYTh;_ z`Esm<69oas?T%V);l`h@&2tpUh0j3FWO?nO&Ij_R#cl16BEe{j;&!Ejq3M2iMhU|_ zJ>^ab*?+^eK``}FSZg+=nn$K>5W@uHkCVb2w9~q*dzIz{&$Nf2lz${;7=p` z+fv73Q5lUUQ%C*1Nh4#WZdO#P0^N7U{d4wT-8Rea=W4DVK3JLZfH7VEkrzAgBIZvN z3oshfqxiwa#f#O?KaFPgL@-e%hncMt!aJA*lcSPkamZBOlUO*nw#GQX{f#71vq>D@ zGQY?uMcH_j8hesF+Y7Dqh@q){WR4dnL?>?SdWmu%ma*-0J}lLKGNvf^*7-R^o(LEB z@?aIYOQd*np}hFxMazymjd8fBRF`WDOM!30pF3^*`)pO@#i&R>WBNGW?Lg?P==FrWs2QGM*X^|rm0)MJIeN*m-v3hwzN@JCSfIQJW3WY@$d23e&8RSu zrQYC?#o>IQ4wV6FcINu9TaQrIdwpm2;>i+e7VmaM7QeZbvph5Y(;rr)f8@CjfVBi} zu|6NTkM*LiA^luI4s-{=^#7dcT05GF|G$Z^m!$$&c7bFA#>0`^*k>}xW~h1&g`6B> zn>N;I=Y(rD!O)+$c?V39^d?XZS8juosn0~mPW(U)K!y#$3bft(TL`JrW$O&O~#Do-D=;_{l%uVyrE1j~bhhRP4G_7yb zKW)dvff{sin}FC3pr=yVJ0OrcOSocmKb6MV;VuhQVAA0K9545+H1@O_&EhwZF96|S zp|_t%TO5Adlq>X&wwTh6a zBCqq$HWLo0=9gZ2#pvYFJNEci4=(sHs`DzqtqJI_|&IaJ=hp<@Ul0h7!S(b>kSXqKu`;^=bb-@(Ef- zQ)LaO%W%7cMDkN?Qxa&racPXu)qbee*jZ*aQ8ZqxbWOv3i^|`{3MeR7hS=ol2#t{> zg^WTB=Woa`Z7nXxQ8gf7%DCO&3n+PT&1@#3dU`s=k$TC+nVrFnn}q8(glX*H`}4jK z4*Q<8(uyKdgHY|)5Z1488pwBqXMTH9ycpNTS=Z{r$!QS^kl|rs2{EN(8RJ~nOtiSe zJbqRvm0eYKM_eNpx+le;m4h|0eHw=JjM!qz*oO{)&ahd1ByWy{XQH^R@)-TV-c6|5^ z7kIyOCax3s{XY(Jz8>Dbi2Lwod1t@S)X1n=&*da9K+PGjI5Pa*!za^+T1qDaRp`)A z5($Xhiu7bTQ!+(JV;tFp&{dsAgE|ok)fYSdXd&~5%Pf*#fB{Zg3X?!#4#oy7e=ycO2V`s63>J{{#Cj{sQ`zv> ztz!h|>5n6i^W4`B_Ichma^f*MD9uTnVPZgZy4$I7xjChIA97*t0R%ZAJU-XEuAm@= z>mFI6sKm>DU*e*A*}%El_qL^dCtfsCjvnEAhPuM<5PJ7$4evlhkYEA@{QXcj3;xXl z8Ha(MxA}(V_hb#tPO6-M-UTw`BArGXSP)YOXxp8f7zg``(!Odt9(ya^8mG`>^_V#+ z6=+g$-}C9W@XoPkYM3)uwEvji*rSENk2_J8eP~>*EQKDHdzxY^4K-!TrD z%7QFFHPwZ8I$&FPRC`Y0eM7f^yP9P*XP=*r>PJ-Jf7leuhcy;6EV%Emj-A5%yK=ixyBN(DDZk|bWKthAFv_Z)DZy91_R+kOW$nX8Y)fdl1X=B~h418OFYR855>!6-NLj&nBwjSCKRt3MA0%5sa z7v5DQ&;OoDw;XWxzIx<)!?RoqFbN(@TdFWPcJVVjE`D!8Lcle*;6`KrqFbg)&ck{q z9V?8qy~s4as_i&o4dOm4J(;6b8rFAg&jCz})UdSjNkwe1CQnX#4yP?s>a=XD`_S(X zOF327x_9R8smv9FtlGtCw27e|S>|M}1^!Hd93v!G>ehmc;%!D8jW)l%u%aS(nDZjx zu*&(sV!w2iDxcoz6v@l8x@9WUnyTB!f(-kWde3~RQJk4oh6G$PuP}RT^xS1x^5Cia zgBufi?A1H$;g@Ir{K)o5&d=&spO7%N>=&J5z9F}U!k=RPiq~$a+kUpc&Uw6qNFpRg zc*mRcGP9>8id}y3D(d-g!+-b4m@uhcm6Cc*fbaJ4Gzsx7kqC2wSdSZfnV==J&kfm` zp!XvtL{@YoGvHZJap(yfFVb!vDW_T7a+H9#WypwKUqKVSUwY-^yuWZkCe`U23mER4 z&-^MqqF^Y})>w;cR2zkRW6oHL`E#W)Y}2rjYlc%dQW}z4OX7-(f=LXxA3ET;Du^s3bXRzxGl{$~5j136nI}21Ogo+{edL3_f z&*{9D-bpL~$@G@=e*(~9=l5ypIYL%3^otOh2yA>TwH@A^ULXK=$?w2UNKKuMMqsN4 z1(xJehapb|f_`Zkp#oSX04xeLC!pmsG+eZ8^U??yu);-$Ue}6_ zchHWZ6IO!>{LNrHd0NlAjE%APWh>Ao(#t}LR@PtYNnz%r@CoddXAS~R)Z?~1(> ze$v)VWs6vew{0!X1V(TRWBq(Lgdqn6OWr^qVMS*~5Uth4@Vf6BBTmk^p@-THuj^;3 zV~ia4wOq~Iz9)12?q`NScklF}%GeiQ(a%+KW}ohJsrb*MoypNp{t1Dgy;K7{BF81o=AdeX%<-SIsj6bz z=?&Kq<#n5V$COsY5hi^HRKrHzJ7ih(aJjxbOe5hI&LGVP4)cn>6=Lkk_?6q}s}ktW z_V%VIl_2uM-e)aOHbp+yrpI={gFo&61}CUZIij?CbPgZ_v@ifr%q}{r4Kub7XFR3G zhii4@=b8I|xqhB;KK9Fs{PxGUj#!j(P3E8^&#Kd3#ETHlZAp9Q5_|SRZRDnlAZ;y$ zMIqi(o9(&e*kw_eI69PVkdC#OkY9mU+I~fV;!rHyep9uE>=iu^T8pF_E&U^)hb?}X z>;jW${II)5MC~cMQKHEFd*@9x%l(%cUg=nc0fJyK#s9`kePLWbGg!+JJ8Q+Nx4yt{n2(G>ppsoKbF?!JCwrrMAo@>_Z4= zZ_BeUg@T^4!hp^88c>$Wd4yZghT3=@kg@vF?nRgrQ2qLTC_qmk=T$bMqaPgsgJY>! z2;ZUwRiO$lpdrA`;10~_E@Ewx4msHB=v-B@;%+@$&>b;D5D^6d!%Fx=r&5t+)uxl}Pc8>GeBCTox*lNN=N{k%+T(~|Mjyf1gY z_{!13Dr+Bpnzk}{vuVN!_x>J`6c=9kY6~M4D*g0I_{Snag9+IayDWF5RfW{+qV7sh z@;sux$^5E4pVNM0n6&$=npa`=OnPDbh^lXu4cBhuX>8qixgEIHB zLj|mz>@=_E%iuZ~NMNeU*s{Rl=6xDAJYuYTPxCltM zlK{QWXC77!ql3;1c#DhWkL?_1n<}>GPJ+v`<_rawqwEsYB*3x06q?genVO9JOtWGe zl9`Ut?vZnm^lK}fvN10g@437xQ>pwQ>WdeRQ6lH+JhmcghWKTjf-iycExPzv$%RPo z@1{u>?Mxl76HnLcW!-bB?+!O@9mg`VKTe2|>eA)qC(nJMblJ)p7@Bf;=GB!Q6F6Oy zC@Oo1mUv%=3aKRXr>o1o`EE73roZ~EGg`8{vL;Oi#-`igFegYK zX}gT$?o{Hz^N3VV9ZOw~7;=^+Zshim@%w>^%VH}z(BwMn6K2z4QtEdpa=zJf{T~}& zC22?OSCshp_Qpkw83GiNPfam6*H&fS>ysiJ6uX(ad`Qh}t3WNH>OsOYo(W9<0kWGQ zNz$I+GH#joDJ~3d**KK}`Y1p)jz8Sm%``_tut0`qQpqubUlDdJq_qt64LQNxfGT(u zI1!X26g;U}gogeT7K)Q%ufE|D@lZ{mu-pf9YDj+iY;A?diS; zePDrCe?H@R;8%A-VRM1lyA8Cb5+IyOS||ojXznh$HM<>keXMAkMs|P;viRmoNz_yj zaX-T*3!vUPw3LJB;lMhiqaW$z)jI^*qvc%oyV#gJZBDIkQ(%1%-$1Wi*K`gV7|g%U z!y4z~3{o7hLRUbkm`i?0y2EWCU($tY5l}OF$g@Dd8Z+>m$WrBJbTLil# z<`O{iK?VZ+@xvZe+rbFPj>r!JfC^YguK|78HQz23obg>+G@UCzIZ0x^W`=W~XJ`9N zOQ@=qM5DfxK_73^WADHwK=^-ziy|d{Z8hyRd^jR+Kn1{*B`||hUkC#3JXy$-wAl>y z(IOils`3s%tugDiF&)C=nn^Kr?YT7Ybb3-t*P^Frzi&XB*r=m3*Nd+*GxX+ant603 z&=2RWUR70_S)$7Zp4e4oEFRGGj_KUGfq{BAGZlr%y>bhC5(?^Jkl@r&SM@KK0G$-u|>(J;HAI^q(`T&R?GdCw299Xk* z<9dceS}m7}FgU|>DCJ%Anpjs_Y5bZX)q!bB*2G+Cx=3(KuS2$5yE+6iTrx!Ua1w!W zdykV5q+4#iNt9rQFD#JJhv_JI^;)Eo4f!-L(xzw~rv_72jGRAuSv;HPQH17_Lv|9Z z(Z~QM&aACa7hLYYDP80va}klDI+8FbpjY%N-jpe}kej$4>@X^H^=$m-`+BCNwi)8= z#tmQj0bl2g3z4wv6;*v61b?m^hnVhR0;q_1L$bd5@G^|Y|*LJ~Qpa+F5Th@W8 z&z+OUuh4+qPVk51k_$WeFY~6(2giUIMBi309k}f@aI359B^b#80FApZ9ABi;bTn~! zEowM8xG_NzM3%aToU2bn@O4jC!@-|<4FbaEF%H~@-Q{L?c(o3=uqxETmFkfp?+Fv+ z!j8=V%iH3gQG8&94FtsQAqt50s*|zmpvl%nO39YTMMS{zmigp{rM61lR1Ul zjhhQiS;<@iW;RPm$nZ!+Y6(uw$UfF1d7XF-P<+`cq)Ws>*KoZG!LHx_oO&$TNU_8m zp8L|7(y;F8`D`l}S5kpWeRm?>m#U*)n!pL`SiiE!dlJ4*?&Uq2D5~!hrjfJmv(XhQ zaFXvZh<-KMm!_k*`cYMb*jo@x5wlJ^aaXQm1ZnO=eEZ@A{+v-RCO%zG+A%_X+9(gaHK+h-HYAz$F6S+ zL8o8qDv>pkA6QN&1SlR=+Ifb-3dKEh)5Z%U(G@sxb*!#p z*f5uXiO7v_@IF~8Yu6gsm~4r#zV<-)`S!RGCE`fYlSWji{UwCt7HuIu$LnoV;gMbc zbI1qpURLu;K3rNp(3|-?R(oGJI$yqjVOoB*zg$HB?7)M+Z2X=JUp34}=&fJ$2&65= zJ>0d>na{#l);<0^+s_Q6hN)6HhOL2xt5+#}EEcS^gLM$qhF;1vk48Da3onw3P%O%S z>t)e;X7*P)y7cxr>v_!u|2f0uZs*5cPq2Eei<55O8PrJAD%fJ(V16zG(aQNY)9gW z-Lr|nkRS10oEgosdL^8=@S{+@H$jo29yXW5lO%py^4LFGIb{;&=p`(rQ<;(k2z`LitlTtK8>nw|Z9t?K2M>=pgj z2JWBdQ;gFiZOM~76_G~7%fobRnf$#$E#uuPm9Fs$LaCUO?Q2z~P5S_=d<@V;)r~;3 zX3F|JL6N}pks#y=Ilo-X>gPM3JzbA!3I$Z;33-5j`1~MfZtHuWTF{uESn{9` z?8-sFg)5SJk%^%!G<~z!4UqesgaMVKj!SoQuZp&G-7fqojQ-aPc?C2DHN60T^-~)q zi-day+=Dvc=RqFZQX9A}-2u)t1Gpqry1{NeR4n7Fc4JW2ICv~aIyE(YB$33<04r1o z?36|x#4~&ljHv466MzBZq3TLvz#mX3&bJ9iAsBK#T&vGvYXeNgAnMna35d_fsKstQ zs4h2{7F$cYsk+A^xw&DopgJF9T$q;m6l#itMlK$-a^R5yfaX^xF90_1@z`pPV+2ys zfnx1KP7krrGU>5|rJ*sOEgB~!-xQE9j_pK<9PZf6wmdh?$kTdqF|U-KwV*K={ADZF z?qpS&hu0UL{^sedw^6a>GzUBKNQHNrfG5E@RmLW=25mko6`r4&uaI&qy^3>Zb>YFm zWNlAh0e=w91EW*|mZT5B>IQhLH+5W+i7=;cYf3IRgKvQbJrx6EZ5V5)IgQ}7hJbWY zn<}pj2d%|y0vVV>vAxXX_aLhS^et^udv)GC1P_O8<7=o~7TV(LxQX%*50TC&!!+#| zGV%}Iw3%{dktZ5tGCfk_GE~c%>JA)5n^bGXi`n?aBJ75s6TL09zsy$a6pLnZ_NKi> z``YQF&uMuw*#uk%3fbZO+!KvX1|Exo#BJl$5KO_YMMDaqq{a z<3TfaG@G=Q66mk){YV=I0L~kXzS~fgtHWl>I+4c{2ltKQ@WDXu*k_VOny2i`i+ zIkSsmd!4@k#2;@$c{^xsTW)SsIW65GvSHiq24-Vk)zA2K9U9L&5FHdw1qHa|^iYN$ zeF>F+FQs@nN+$ixb>?MRoc;{C?$sXe!hsLTzgZ*pro>Xztq0Hv{&xqCvAcJU9I&y= zQcc71#V>}Aq-RJ)k0hco>lL%S`ye4QMcHOA4y~JNJlm8eRYfTqM7{s5bh}o8t`!@1ESCL-x3no=>z}DpQ9wXT#z2HjXij$;=G;p@_ zZ>vdmkSlu$4kvO)0tb0K`5Xg}jIZ3PJ|_#M7pfb_v@4p79VZ)%7j7V;Sv)xm8A-dc z6O1Yubn3EL)f>jgtm@owi<&sTj-!$=D0J=oDBQtM4FahRcP;k~Jc>}>$~f!3Mop}o z0}Vu)LMG&Mrek_D?Q$EF6}eaYU%BmCM~ASB&)j|=5E=7p>&f4>#sUVzSsB88GHn@8 z{PBK-E*w@+ExS}T9rz~27R?gK<)l(+x@dj2L2FhU>T^OFSgvz=kl2(jr9deqNC1rG zQMX%@9n8-zL-R*z=~h#d{t!RDI~}F$LGg?4)tElpN#%@?Cw7lermSGy&ECKy%sIE| zRZe>G=Hvf()b&<93u-*LTo^ZoK?F}FOScM<|J>9{`K6xE{%GIqFajL`-@-v>HUDg&s_^CHZb1dZM>mat{3&c>$S z*it=#9#g)RRlNjQE8X0jFV}myPt5?wamY(_iVGa8(vF0s7v??3x5-FKf=x70q&aGQ z;Q@_KUv740$+?a!@wayQjs}l)F%8N+C%_{Nfg$+T=dSQGd+VFt$U2ZiVM)gnt*rxX z_1i@RUpo>M^*UXvY~&o75io4r=h1eS|9VD1E?Sm;u1$EYV=ON+JVV~-9j-C!g&lb| znf%HoTz@)AavUi<3^-;@yk6r1_m5=_MJS4=yH{Y#)dbWXkSrMoIsTlKK8)1!e z8GMxcsU${3Zsxpr99;6Dnu<<`bOGt33Ybm{NZW_o;|9L68RS!loFttrQx?_?ItC-f zl_V*ORr_-U>j<-HDUxlRyjs%8E9pSK#w8KOq0N*t909ELhY9l3_=cYl->9zA1_Aj2 zyZ+Q6l$&r-cnRE+w@hr|5z8E=qs<5U+cj*}895c{otO7f@3{l1;m?8ex&N=m*}rM2 zp8Ag;>3zATp_g|E0rg|C@R&hK$Jv&?z04`^R;cieDBHWp!LHvnNdkA+XlATz` zQu)C5G)cE#$z{`HLWsem)WYm=QzBc02zN1MnEeg*Q|o~V3Uk$*Tt&`At_-x)Lo`~W z2L1)MYp$efsF9-+sk2iXM21i8WIbUwC`uL|`8MU3^(3|OWI%1s$x@G`m>kspcV2!) z4}WwHZgmDDBflbVZ?PFvq!Q%y*Un`Lo3X6yKfA*}t@@=%7XJ#K_55^LFGaKuC;J$= zdGH75lsFI3!k9jgh-Wvs^Xz@Nj_MdKydX|s5MAO|$z9Y{ifjY_8{ciwD?wkDhaen&-#Z#`1f0=8_Ml4{$ zY%eb^1b_DXG`E(X-z9FF6ST7qn+~MnQ7HX6^mHNKII-+2D z#FTiM=_zT3w^jX~OBkES?JD?5r1XnTrZe6bJV#7$;-!>ih;O}|TMZFQw7Guwuk#um zi+gx|U5|TKzf%{8*!jQ5pNW~DC~GM*d#@cl87o@6yGx4m$k2?)csL*3s{7ZeGXh6+ zeX^b87f;TM?@rMN9%udKQ7mF38WK4YlK6TC@^W~lFPIUhKAlIGY(r#k@t~)rEy^;q8jot2p89j(*ajR+Pps zyb2K=bt8^1);~WRVJ@jZvN!o1nu>M(hi)A4+f4GI<@lb!_SW~x!4t$YlSbR34cJeR zjD2;16>_&}MbwUZ|3U%i?%Hpi%oa0jpPs$yS}Ffk(!++(F%HH5NyaR6)SLz4uRHry zH~wQ`>4TZoeP1`y8+eV+5e{%Uv>W{sR`zt~@ZgVq-M2#2GqE*>Z_A;i%O3fTphl&; zu{F{Ms)hJcC-G9E)M3J2uLC)&NdK}QfbHq0*#u)rvpGHKvmz=5d$+=|G*jbJ8rg7G z@QdHN9^N`SE=L5Lm&75o>EZGG%|!zFs?WEVSCOI~FcL=elB7xV4r42YKU5R+Rcoo< z;nstsq!V{Q4IJ4qv~k!vZax`YXd@Kz-I3yS{Yk1xw~c1bj$cES`#Po8F5Ijf zq267{mIP{KG(Rb&K=Ha(g1imyFv($qI55sz&-3CA& zZaBu#8rw0cHxZ!w+T7a>&)Z+?6(cYvd<*Ff%+|dg|2UE3( zfk>KU{=tlK&NbPPnU1Obg4g;h;g7U#6A3+!-x(~-Mw71)YoD*)gm%x^SibU^4p{vH zo9Z`=Up;AkBeR0WM48p7u)%-~8Cticf$fdl_K3!4h`Kj_1ZtNIY!E=fe4#;)K)RS5 zg8@x)K^yf{yTO8Gik4%EC3f^VaDTe`y<1Mqex+oGsg+h-Glkk*aBT=5pOt~i$1$T< zEY_6nuE}?b(}#0b&k#l@<#=wKSkqCNg^5J)=Vzm;^33MYgL2?gHcyZg)@bthV6Vso zC!1N2_xP_}XWp7wJzfw;PS8-7$P*Dq(YDLdiE6?;Bv2&yd>tKB3ZBVcvXF~(#}zoa zzI{?G=OL_V8b@YM@%%jvkQvcL}N@nH9(y}WAW zz=waD>_gj+*%kZ-u!h`8)%COW6?g~~8t;{oAJYu@;xYoCknNzJm)~HB6AJ62XmF=h zSJPa~GdsFl9Mf-^0&RMt=Gf)N~8b^2bvJ z!3sI^5qXgs8p#S(Hu`JA{O-e(waP~tn8G3W(D%)KBf^Bu8vb_aym1`@t_?Q17W8fV z>;}I6EL5l`VLMDwJN^h3*`e5YZK#gtSzfGfNT4guBTJiPK`g%!6dJoN6q@N$$4END zMOLG>69N@YND-qUF(=wR`6t(OAZ`aH<10?vl}76h_f4!c3rJ_TLad#%ml{R`bUgO> z^@jtPe;k9bq?^X2E34e+nVLf0c?ei#brHcMaW`K?^+l%rVT1bQ@*%|^qTa5|RLqk# z?~WkY*uMCfQd_m-ZO-cf4gEtLoiZ{jRrWpabaE#9?i2Lt)0GX`2aY9(yGq~h2V962 ziFlY>E^WL>DX@JF653B&^Dz9-&63pWG#_$d8w%iB&X%n9;;8`d+{{`LUp}G!{I!C4 z6#g>wPUlI}spQ=FC3NT&;K6Y<2it$FD$`8?tpClm0L0fqWwM18X!liE6Y+!^FcR`6@BZJ@%mc66iDBTI@@$pVyE@0zl#<9ugg-bxv=sZA_n0FjQ&pFHI*_yOnUgG@h^T@W}@p&=;}(Nk9!oVvL92{i3)WYwn+O0Qy` zkMt}dTtWGUT{kG;KyvHZxJe#oCdhI;%;xXu+bkywz|fUb+e6iEVEseng>RsN=KAYh zQRL>eqgkzeEQ6okQkGCwW#NX&_%r%E41)$fhMUH8W4l|Q!cR*ufLp+Nq@3gi8a$Nc z?0N(YdRExZCG(Ma(0FToc~dGM@1WoM=-BUjpTf0;-_C7w2UWPyJL$lzCzwcs6VWHK zkWU7^VU5HI6*gB%pv?26*#9ac>jxS=k=JyxF!x11CgunCQ+_(LdcLpMUAe2*^|~>! zsQjDGhPUKc$sEotUE2vq!k$9JxTUn~gL?)3@!d*FL5P04oo$RBVuDe#KIL*N*;u2F zc_q#mRXI;fb{u2+sUb(Qkrmmpmfh3fH#tv^#Ul!KrozzS&zQB1c9{ZRoJ^M1Zx zcWDTXy7x<`MGei{IH`P!E5QKBFX%>Slk5<48k56hmes-Xc31lgq1md0kkBtnYr5^U zFcA7F?&%h&F_7*!`>t=tDH(Yi27;}__0Baa5mrs zYcN!$Can#Z8PwJuSMqXm4NW*-2%S1PUo3;Jp;<@x=s8E6$Wi2^;xAk{o$X59HAnU3 zAZ`oC7&NushUBp1j`2cVZ3IU*+H@>h#wfVfhGA!NMV#b$*$ty;oRvx#8u?5PaZFeu zU2;j~K6xpq{b``|pVvQ@eYlD9NPnZ7zA!Rt)GAf!xqZbT=$C;A`@eQ!2NkA6i*L`w zJg#Xf^c@Vg&xoI|o^7>}@@T9w{AC(f$G#g~OjP-&We|r5d}}=Sy_+W% z@c>8m*w5>~B7ALni&0^-FJ$^?VvwY;WsAC`S$i;DBPC1hQaj`NZr59EVJ*I3W8gH5OmR^_7zA#3;+U|0as^Z#bZ29-PIcHa5u@Cq3YNg_( z8NNR!vYJQ-c^|!6gI-4)ho1DhA>Y3QbBjZ`zKskX@;&q5P>wehH*?1|^eMGCcEz`U zrXjF(VQ10vRR2Qh!`)~M24|@6n)@P%@6VYRF$lw*E~ahuN--j8%T`GSxNMZ|tOBkD zZLiK0qz~CTGus|v2SbfJ6km~O$@FnfuE3Y=F@%ks;1@T|OcD|)5Z6)Wr(y(5KczO% zC;1|LEjvX=h6*=2C^}uv^r9n`H8qAI9*OBJ_wt~Ag^YkQA9KqA9ZPPKC(e-${oewr z-HqbrsZh#?rBMmp+F@)c$4$7w*Uwq4upixc~gr8bN`1zrKL7S{?)eN1>w)1CSqcN76xs5#*-Qr2UYf8g*SpqP)CfGEi zdSP7`LjWjS!wKim%Z-|od_X7hXWQU(hlaD;PPfeG6+HMH_6j@PJX#|GbeGovG$`pdcZ`gCumG6oYu2|MC5Yb%^wgrN}_}16Y?bFXPf_62l zKZNgeQ7EG*rHV?r?}XR5n3Gw;VMN@raX}tv;ZgF|=0-}mXsSbjMTQV1}H~rS`{rygy=;a&bKX$-QqBOyA5H{2 z{R+~KL^RIBrR-oP(5MyXA4|3@L9nOWuEhqdEk;@VW!NzFjU@fy+oKaAR&G)qPt%xt zd!KeKPL>~8!EO{AIgo zk5k9$^Mm1#WXDfyDPVk*wMypYp^NAoAwmjzZ)<95z!65FSe06SYLyjV6*8|>Rb0b! zW?@a(5%tM_)Hl~pM%DU7s(=qMb$JctKpPoK6C562EH2s0V&;6%ZdbpMT`$Zdd225P zw&lWh=F7Cx@eP=i-t=G_WV$K6fS1$TX%4sNRTdq@ua3wvG7#>t+YaG_TB_8qm$Z*e z+52uE*I7**9r;#^C?6G7o4sLWH1K0DV8TG`ddO(hUBB0-Gwzp|;W&B)%HP9dpg?@ZoZr~i<)JBh9cz7;Z}TnI z)G*a#&0jMG7n!9D4=&7`!+*SZOdNM-Z)OV!K(et&pTsdR=HmV zgZr}XOFS|L9G`;Gu^iXW|84nQ;vat&1RUP(fT0JPnYeYvZb<|vesj51AH2$Y1o(RZ zKb|{xto9NFuke7KjwCG+yi;nq;a3g!@jD>Tb5!AZH#`NOR2rlG*>LryVg8Z9L994{ z>}ooHY@=-WcFvM#Grf`(ZEoa6tS>uHjJfT*4&EMB+ zS2t8e;oZZZoak2?Q;Sx=>C}=w#LUrOd~#Y{Kb)i2s2Zzpdc5`7xFEivWsDx%(h`Ml z60x*C$Fa74Zuh)TEop8!kUo?d=-B{B5ubp6k#V1mp}2usgrhFmVX0W|6gl;Cip-oz zO3~x>tEy}S7KnC7bY72&2Jqqvi;F!u2}%vczgIU*Z>gqRDnIDuLrhJakx-HD-f1BqzhEjg*kOCtJxh#W{$ zy@(F_ZE8;QiA9!Fo#^1hrEG>;g2 zk}P$hTmu?MFr6VDL)bNH3CdBV=OPqHzWFBt3}Gj!5mby-z=eXHM2tUP)hvRHp|u!o zP}*2>RI?H7HWZCkBGysj5(XP$?GQBhV+Zl7<-SOJlgz2)_iZ+IMUSvgJrtM4B8F8+ zPa}p^O}f03RAthMMZe-oJZTZin-3~yuKV`9*|x0vi?>kTC?taE5Okq`=K5ZFO{qbW z3;J27Y^>*%?CSjHoAlqxwL1bTO7JVHXQs>{TU{ipnR zs;$LCOObz-LJK)DBA2F+qcc0P-RCRfcqYDEXE&dksY_e?J1mA@{JnT=)x8%p3#)JFVtSd}{az|!l%+X& z?4erYOhS6?w%Ne;PNLp(|GIrQmTWLKq(?viZ~4j7xkE#*U})=!lKjpHZ-()$Gx?x* zct;NnW_XL*YEMBCT7%JcBh$$ofck;E({_6(2+8uG_yjRlaIMujOR5q9hvgXPu*H(5 zUnoa$*4c_Y}mJw|uf^ebRgWMhHmq~Q>weD$7 zk>tjT7Q!vTE zI!$Ql{2AP6>E|hok`Zqxkny11a?H^HsX=_rD36QDB#gz>5ZRpd!t!^h7Z`fb89kQW zF7_6kKb@zbmdooLn|}rlW(xs0BP!jqYz&gR83o)@KY=_<(@PW74(LEOL)9s=14dRRHsRi9UT z`q)w1n3T|xLO`iHEu08S`H(&%C+vt2jWB7*9MjUZN31SD+5xVS>t|~{B3bbe0SH66 zw(%^!`$L68n@yY?4WY~n8u=7^1^VzzVCv94&)*26B_TrZJN;EgDFo*~r#w*eyvVw+ z>as0Hm!*!cvdztz0+}dY)^k7W%Rx)nb@`KltDeeNisRQGTe})+KLsU+3XAxW2s>Jf zZ?X>3R#;$|4QeY@u>52Lvr-gqF6|)z1oen&@Ply=o-i~UOikei!M~uFc)Cpi%t@!K zvBF_Jkipv7%EI)B^7q^(sC$5E4;&@9d{8E;i{azi*0gx8t+D}7anwDcIc%1V<)~AC z?vc%z_TcXZY6jBgtXZ{tK@nGHA0OH4EH2-_P2!4#~1** z8W6gaQtl4n{#c%D#s5xz=nU0%+YSw~wk~9ENy)RaeB~RHc*ZB8m=rxj{TQbE2yt!3 z`0<~3FzN}6eVeuSzT#GO-0P{cGqmwt*KvpT%rKc~>_Lguvo)W zv)Q9IRCOb+LHLn)7%9e6vUVLAa_vBeJYkXFS~lqS+=FCS;hx{ydFNI<&M% z`#yJg21dI9{SRrgJRk?Sy^?vN*%?;n4_D_hCJ36=X4udABSZcE8(HdkD5D=fOkbt? zJuy2`WAtXVt6z>Yg=yxxe9LAY-t2Jqc|*haG9G%#AsL7VVY+E|f)7q~-|#)i3aG7u!D#LOGATecp%Yv{ zz|=)f!JMK#SqqfJ*Fvg}y_%Jbh-x~iRMPq38n6BqPxY3eM~F}Xroh@zOygTu0h3BF zjM<(F32iiqHvHEB`|4l2t;@R%d*1q1mp4Dv}A7aTKwjbeJ`$ed^9qa~1DkR`F zQB$_r(XJ=ll*fiR<7a)x25ccd{5g50WP@^UAsnay&IriVAX#EffU~Nvj)hyLUjcQ$ zK$HBeC8$jT^|t)&-Rl(7O8|WeUhoM(U0kOiC0|*Bvkq`~Hzv+;yYUvn>Xuzz08-I? zq1>);qf~OQ?vfFPmF|93wV+Fhr;D*+mQJFzGREPc^hZ(Uwq|;8fz!iV!Nnu?dYcoW z?|+o}bE7>9?9=aKcSl7pc$Lc?drmjx(I_Vy%G<{gjyrU3+OPDE=Dj>05;eYkJAJlD zrcV{suJyem!9nJ3Qdj&=3TNcZO|NaqWzoNAlmn8&pBXXx)uoGXE_Zem={ey2-^v7S z)Me``D5r>w_t}^vpdTscAbW76gH#hvn~&GtHl}yfWAvzoSU4FBgF$!NJhkANeXAv! z8@WgexsAm6p(Q9i&Vb4QrN$E=JN8$Zw{e7S3FveJe9V_(|87WmASC=8%IBJs=f3KB#!;%vC znjxGUdh1HI!75J^J)r|&K{uCKpvI(mNk!U71@35XUs8x)S7g-OtXO#aT?g?>=X-j@ z37MOnIn5Xn$Dw}0JD{xN#KF$G@gVb}>Vj;&4mQoPpq6h}MntkocV3y`r*ay#!F(9l zE{KAn^P!MpL}5E~F9G&yxt+;ak~4)6N)_Bo8Vfj{kl<{C0kn)!rb4TpCm+h+54pkqVTH91PDU)v!~VW&EvTgiIbqBSS~*KTZ_=Gap0GE$Nnu)I43 z^%Ss->O()>CSclVmJ_iT;#lrUl&KPS196c04&f^l<`%A=Qi>Y;qyd^Qhv;aVJ=i}Ub8`}?;N`HM3POJ^enkE zrDgU?V}__&BuhZm_BtTCd6bP8^Xub-RLQWd$3dsHtSu*sER<)9th9`oX@S_OK&<0s z;1i-%&mB$H#|OCOt8PtM`~L@_*uc4L+@_5jysnVpV#P_}mywy~RVXoA0ec@!L>Ja8 z5A>@ix`J037<94)kleh?|I`(-$XROPi_e~y$VB?OEqLd)g3}p5`7vKQgnM2At6i{; z)7Msg;a=go%7S?&)DwvGuQPo)l8G>u^yiym)H4QbDh;JGJgWrf)gOnU8fI;c58|CR^Y!=?pt|EfqOfqUl-B}vq z28F=2kzw+mkdCq2c*k}Fu(p;-ekCw-J=MpkH-rs_wQrrFy2lklga$6_OzXLt<~IU( zu)6uobB+bB|BI7t*teOIW48B@wO`71310p-$d5}}qM8ip?Nvr8N53$6gW@WYlHeX; zjXsKVua-~%QKU&pA7?}c+FO(l)Xum;q#7rSOqzHT^7c22z^kNrU&p2UH4lr$-+gvF zH4TE>2x{m zkezrmlt1Qb#=C57<=Sc0X?N7As!(brpM7@7xS*yq1NZG?tNY7svSamiP%M>W{752; z|4O!cx700!u&vV(k#I|;M0#S;;#un503l_L0G}enVZ+C^IK<^w7|W5-ryyz#x`)_a0JxjnSD z_xQvv_R{6+7fLD$;%bzqqN|n%PI>4@{|Flzyu7M9eXMWhP^xSuAAe?g`07_6$|XK^ zc#_i6ut8x)SaT`<&OGHbphSQtArq)>1>4pjTO*`VFk`5lN6jMFrl}&GA=~t_YHrt~ z20s^%k2P+xYqwjcr}|DBYHQ4he`Zo2YdIqciF0olS4yty^!I%s>ds#kPrgz%bBI}8 ze{zxd^y==dgz1o{K?m^<|50sA>kBwf5kJxrS-9{9h^j#Xrj~oFzt+#kJ>4A|J%6Xn z<7N7hs#$+R)LtjG_J`13Nq$liPWAJYw7*`By-E!}O^NLzqw3n54rZ=vP`V_S&FF3g zTYZJ28$2WV0K_#QTHA6Xf?kX!Xa;Ftunw8_B%{fQx_zJsj2jgDTBYf1t014eu)zm+3VumV#L8 z7Injw^C%>?Fn*((sMP1)2y#hOGg0?1%SDZr$OlO*Ki~3#)ymyyjXs(1w&}mT z>kVQ$&JHXu8`K9baizDo!)43o2%*^?nFlR7wZ_5aA-X+HezJy}TW@BXBf^h8_ckr7 zvlo2l^>5ZeN={SK`#V~Ro<`5)E_|B(fKAG>oW~x$klZ;pkzO*sSt>_USYl%NI>Z8) zpcuS*6n?^ULdjN3a%7vXBrF3CL(Y3^M=wCu`y@s-!C=-n4_uo=lncV{H7V^W2rcd; zHloud4LTX+g0%sSlR{}a=9{`)(!dcjnis_AP6R}r#J`A4zc|nF^cY_zLFws?8Fl8+ zNDkZtAYU01PTC6Wmk?S7h<6m+U2}NpC83uK&-OB+l+txq&4I zL^FAK>Md61k2P)EY=@33@!BXuRpIO{p2m3<>A5*{>N8u&hUM9kRvgrQ-h#>?0%LPY z3n=-@>DsW=C%E`Rb5L#K*yx^t^Bywu3gm#5Tp4V)vQ%6f-2AKqq6g_!y>cg(lh%+Fh)(*&iUG5C({D8yKI%@>o`ZYrj;^m$R#4Gc<6VEh!vi&OqwZ> zC1iEu16+$$WsxdIHYp7pi$@WrEk#Lw36cW_1pe-r#{?B+-;ff2h|dWyPG!1In;Vfy z{m7nFO*dyBH~N}*b8tddH)vOq2WnO@Hv$q3(+8i4J+dVd!5N29YMQZ;S3Vh5MY>c!}?=LVBq zK|VRCUIYya7hT&;!*C9ONDDeDK?GUvgmUeHBX_^ZIy?N;2p~7W&jQ&ue`{VC2Dyk=TEb|o4 z?)WKsVUc-A0aqu6W3Gl^juA~5`JLZ&XcolBguJyoK`aWiE)a7t?H14kI->!!XP{1# z)BWM8mL=+aYiPwm{!JToo;VjwNJ^#)+Yq8)D(K(4uk@L)RBB&G6x+ov|n+g4M2s1 zz4|hhqccl2H1FbfK8rS=_q5sNCYGByMqpr#3EOozUVQANrDz1*&nDYiKM(vzC!oWD zGfrb3azYSnQY}PFc{nlSrCK6cs1GiaIX$c2hMc6<@#|qKD2i>nZ8KL3Rgc!Vs`FFb z`kyw+jVwQegMR1;{342J7gkxQW~eY!H*er2_Dm_~<=}*_PTFw|pR&vXzTGEKFW2kF zZ;8dAhEDJS`OEcJ^=gjgEhnN@wntB&WR`Z0$%mSORp$bCkg5bvHn#!=2&a;vL#=>l zCL{0}zmcH;I_lVDtOm$1k92_X?~UqW8i<3$cQosg{PkYGs_3LEe15I{<-l-F51lVu9UDX>1%(<=B`OIeTN8Cib42^f*$4K=g%Nvp5;(-QbNSn1_OoxM= zIW|98!(^bPFE9a>`{GPox)9iemsBK#q+7)n4n1BKp8OCdUa7QcnyWcv@kvH9>SHIP z1(TXnJxg|&JKS(4Uu9Z!K=!+h)@eLdrWq(A>MRH)E3YXrEHB8PUGFhL9}E?!w3;XD zs9z$Eis*C)G}-gylqgib=2cn=qL?SJ_?l506nvcywT-b(gUHYIb(nYQvjxz63bGqr zVLcoIs*WM{!Tw-ms6jjyq3L<>oyu@5nz%<$Xve)r7hep2N^51hPaWTHxfkF|kJDqA zI1_mlJI)IQgh@_v^aInqIz&nKFdZX&RK|6uHF6@TbXW}hK6f)eC+)Gl=%ccAJVlw+kT)K{~)~ccJP3arNh6NvD7NKb{*ZE|~(k z)u;&pf&ngRS#LK?5zP%3q*6q4&!xr6<|A$;E~$v&Qn{j{lA5BHHKbzV(xR4Ii+lT& zEi*Omnfv};H23eH-!XZZ$E0oay3XtQJkRs-`WE+)3**(dVEpTo{cqm=WqH#F{$Tz= zZ1)Y9EIWSP*M$H0H)^GNR4H1$`A{AFR>K5tFC5RkYI(sve^(~`t9_2$tv6C);k(W; zwd8epk_CV+HB$>adK%IqVtTp_S$JR|n>f+6j!%P03&HbpR>ia~h4NTmcMf#ofGLK!?`Qfsb8g{NA0F`%4ASVs=$65~ zti2v5dR#LMjmM-JvWa^FQkY?R_mr;i^yG*Ze*XvC0toVeEY&YHT+P2Hcb}705ME&b z>c7ie77k`|bT=cfwp~!LlJiVF|2)6W+6_H(o z4O{LvXupw*M&7FSjWZIMsyMCm&>SCy1PU2o#z{utKNF;O`G}eai{sN`8Q#ChK%X@=k=0ZPzN^-{HNKWGA1&<|(ej!=tf5>_!NR8uEldv|kw39r~(L zU7z+7g8OVpVN*^=-bOyIbgi_uKN)omiMnKa%%Ht?K}Rd)o=u1&iQyqGnoF>KWEbGk zwHPK~#Qxf-?XH|B`qoK2Y*;sOC_OLV)F%3OwYMn{bv-ifn?XpHsm+W29iP;8=bd}@ z=wsfeS!5vI1{NAedKGFg4pL+MoCkvqo^+UvnL z;dlH}tOpV|&aVVl^p2Ya8}vM=P@IYS=x^e}5Og*w-8Hcumn*AF-Q#9io~57$W=4}A z(Nr;|pW2WcLkhDoz*sE{x&pTkD(TVzO*x|vVKwkB5ZbKLU|7xk5?|#>Xu8NfxUIb%| zz1P`kl_HDbJ`lS-CAIwjouKAdnC%hnpzI|fND&{S@AHJ%6AJ$Q$|1qQ{{|coy*y%Q zbZ;)Yhwr?dRVtr~9fECFpjvo+7NC)J-|H4s@5A<%VC!C)`1p`2BmKhsLUTdvwE}4c z0PC@s^um0_i@QNgk<(^xj^P3}vie|al5i-8p^aTi4t!T=0VEbc$4dSj6*eXzJCfi* zfJ9e(xnP~d+U7Y6Vz&TD(nSXRxH5V{PFIkDB2Z(&VNvfXA_JJv9}(UG^0I_*$dmU=tTWF=IV`0V<7al*#mURGiB z@s7;G`ZS?^OEkh68Hfv0mNS}Z1_<|T_$d6)Q0EwHV)yBf)5Iu)?BAci;osECX!5aT zPLR4Es|kGkhZo&dsfv~dsS=Z@EBJyVrx{PLjC}+3guenm-Nilm7tPqIvv@6CGzvcM z;^EREpxW>tvfQqwoUB7ILvnPfg9Vtg`0k%<$~H zMs}o+gW5tMx!!{~1+Z_2{mk1cilp2$gP*gLmC3o*voMkVO=$C(QN$c7aHAri`aP~r z`ll+Ie`a6GgYF?pl=h(uf^$W=G50sDLaPtBW#2hZnbRBr5!}eP^w=i~e~gD6y{>A$ z9KKu3&?r|#nl>?PC&)pCBqS(mB-6iUMZ`3sy4Q#+hcrByF=7qJOw0X#b$#mb+m8Aa z_}#L3so+!Sl}qlhxF~V4h^|}#S4Ze}98JERy!Q+!sy)m1VbixxaN4aDI;CkmIB;@q zGt83L_+>EjgOkiL`%)I+rp$#|k*yR_EIcViEGFc+JKn5ZsR}6UK)bavhjXMOXocu_ zf3K-NAs5OVh41>nrUP3{fadwt_?S+uKAQj*d;6>bSti>>7PL{wEY_{S{r64Zp!lcHY)BJdLg?pEyw(zOYGs03#q;oo=Mm>4TE1daeXl4v@RchB*V`2w z@7tX17S$gtyb^k>p}ThS<-crJl&Q%)m35P znGSNbOF&`W;_Eyo(j;LKac|gKPx(UamA~v%JYM2*ccGr8Y0jN)+}MKE*%*fuK8xwz zKACy+Z98%Q#!Eys$Vg3HYDi%qCTgLn5k+NjjH^}x5Y_uC0h`Z2@UzS-HjJ056ay1b z1s&)vs8=a8pi1OU#2y+#M*9!u-1+Q&LJ`6JIsD#1KvJu*OR>O^-x#rTuC|sVl4|Jx zF{l%uCZZBUa)K;XG&URL|A?O_H=auJ1BC$cIK(MQc}HW)5_Y@4V$1-xc8^P`R*Ji& zmUiU5!O=2GD$3HlxUd}3)yUu~zEGCW^1SfjIRT?Gh;_{y(DJQDNepjKMd>CtA@*E2 zF>BT7HM^0eK9kVGSXizSP8Mdy20lMMDb_5iKWO_4juC1*7xgX@dT9P{oKaI@lkYS~6r zv2K_{l(iJ21>e^~lA8Mj7K1fzikxUmavIHUMmgB$6h-|5(IDKg-+B8}UyUWvG zm;!SxojXBBN8uB^kw?thK|XyV6c94?TS&7&Z%uW}LX}iVeNJIv0>6HXSi6~j92~s- zhUe(vVu*&jU1wuot7|yZc|^a%l$?_+cCZYvD;=|Y!oz_5Ax|^5Un6B%1sD=Md1@%} zf7e<4X)BjCe2bA|1v~Dm^p0Rh385#MZ4iz4Z9%kq37Oa$=7h?U{4Km&s9DUUL8wG* z&DtE#_bzBw?2!?Xr!b%#^J)qS8?a|>RZnlZ`c+KX>ejOV-9kt_yZ^#`Q^qMn*}<5n zN89G@_ZdI5znd^Rp}GEa4TG6b-Qm%ZjkEK)X%1GQB4ulDY{#-or^_LD2-kV4XXKG{ z1^7LoEMoEvqYH5E{*5Wrm~d)Bp4w@8WJU+t@2h=82$u6b@k(C6L2r->uA(}P=ejW{ z$hh*n`n)>cxbl25dfzlo_b74~gKsfh_e7Q{?hfqp9~KI-%&s8^E=kj#tVWdJvP;bR z5e!QFL=!A|kB1k@99@;qEip~w`FyxbcI8=I)=hg#Xrj9ED=;6qVh2io3G%Y8iESWd z^LDvwIV9opXgMS{0ZCL4guo2buPafz&=nS!H9RT@zfXsh=A~Opc#WTOmD~4}3Lx%h zxJm(noUhQt2adM77@_i(m-%DLT{IowUz=4&xiYU8~gko+}_@v?7Z~62zy%u;?r_KJzRb& zf;Ap9w@ZE+{@mb;Fulwir+kx8nQ{-qA}+~v4h)Vuz<-C*fj>Tw%U15m$q%j#Kd4`u zkPK3RgY95F1G1t4QrO#Rs5N_w^85c;M7r zcITx=2-bMBA+$fjGtNTL-y!fqr?6w#E7g4$mg9iU6rE=*} z4t1|q#yy_*%jG`}AAkP(BhMl63#4T)=Ul-^*^axoKZ=SNLb;IAFI{)D zWTendgLn2T8f9O}Adq?*9S94}yaS&D!KJ{Lk}+*o{*G-aWoQxP6a6wKjluGJEDbz4 zcu~{uH$}$cijDyCod)>dG~a+Fq;+$>nV+aau@+ItD;}G+V+njsKtO7sX2W`Ayl`J` zm=(e|9M^q0l=J6UfpN@RGB?e;TTjeZ=dtJLtMu4hpBWooDL!yiW-upzV+0F2$Bm zEPo+|8Hbb;F8~eow{Mb_M@FoFqIV*JJbj$|@D%3U#;Wp>gJ%g^&J=D{f$2;Xj=tXF zU!|rBA%oUE72whWudEJpKnKwRJ$sDrY8zunsY-dlX`O4TkH|>ySDWFURG> zBk5scr!;eB`M4gZ--{UFSGgj&NLs0BE-Aq+Dk&Omn^`8e{uRc&pXao?qw}y05n&N7a~8@Sp^Sma6i3Rp zt>$P@W}(xJWU`&5h1WD7c{7Lr9;lgFGg?mBtS+cx`lIO;u3235-986W5#bZrT=SP` zOIP1mQrE(^5@bq0;*nlew8un*c!}7;F0;X@kP^rAL=K^FA6pcxAQBxmre;QfOQ+ztnqa+HqFPJm)0f1h;DDt4PAS zL;sDJ@<@ctQMef?a1@RLl`L?Cfr-o`C&fv{owLSo*;eWdxK4e}2;v}*C*=sgN{R(Z zpk3;U9)s2fHuS3;m^T$vB(|BH;21FiKz06(%|P}C&UWS%W-BQCd>iJqH-9&?l`T+V zcoF;pKM&@+Of0fiW=SQC>hY|slHFw+R7QxYKFHp*4N7mVv=@GYEBuP91WdhfMgpv1 z955iGi==NHvQlzbvXR=OVN~DnQ`cvhO?O1k+JEtH&$2vPc-;mJ{%V=d=cW_oX zp;WNslg11O898Y0 z%ECk-d{9FPK9IB+l24YKDXH?!VsH@HEY>A0?o_b?4zL=F!iES}Axr&4kqux{^y`eg z)MQ&yO~l>z5?z@fFkR$$Ww)_^8QkDVe=_HH{l$1D=vqyv!wN0(3Gi8^NUUbamS#e0 zr}HLP4VzyMK?bnqge`-^tv3L&>?WuG^jO!9vWA737uA}Fb5s7Q-OnX0Sy>0mMF);Ra`UB0}tcI~OVcVNhEB}^0U88Je48`~YV=mSa6xQC8hJt}f;H=m> zM-rqMy32^xJ;({6qR$x9V(6>Ujka_a*#1`N1E?>VM0G!buzH-K)xYVv{gLDt&9OtDSYH-8wff8OX%Dm$~SMRXyOV6#BuA%NY95hQf z(cpd1OtVCN2gGrWWqwIRtc?m3Y_5)E2C4|O&6D>)_pc6LLpo90WX7IdhygEze&+CG zWD1jN_so#Ls8{lvkLKA9dqXRK!A^uKw#xIeMnIS80;2=MwWRu@Hb-kiN3Ue|n)nQy1UhYiP@9O3`#C`9h~eB2 z0S$HMaJ=8VwWJ*)n)^9vctXNHMFS{Nu<&gHh!O)Do2sE_7v-nM3T)-zpU70ityJGQ z&$MumXtYR_(s$p7VYm#Rr$2RAFs<_vZQlGgJlRAum`h0uz750gHg8{}v`3U!M0*%n z;M6@)@gg!CgZmsw7Ld`r4jWx|s()!%;7U%9)WV+Cl1~GbI)mkT0@z*#tuC3y8*KHk zi$T1^!Kz|ws?h-w*_o76!jWLuLf1d z=)TGdMpg;%>K^nr*>H}@PVpD`G1rHXD8e%yKpgG3t+|im{o%%WW9hZsC$a*s-b>Ms z!Y%!t^frN5#AuMtMNCG|aIwUcWu9(nWR#_A3cVvN#d-m{>B8ocA-1h{nzrrmp;%C% z4;<>WeJmb`mI$-V+E>bf5z+b4?d2Yt2BT2;(Yp_i2X+lSR)H#adoy5iI$qvY3tWY+ z;R9Dj>a-{@Y!)}eC!dD+;}WgES@X%~%f|Hz5BNpEpPXOU9eI?xa3D$)z|1O1KDQ)5 z1=hS0Qz2bGUDaLq6!h+K7hjiNMh6j#&&lMnvJ@LqsL(+|u0>4lqdRV{4OsSa2}PhR zR9aLF0#8`^e)o2|`cV?-j6RKh8N1TXrTQnTDgZN{`U$Y?wE#vd`HvCl!-4h|PL$q; zwtxgfX>RfKZiFu8TUV&Y|hR)3K8vD*6)sSn4|1%?aS5>|3-Ml7A8UHdKXJT<`{ZlT_msH<6WzgiNrez@9x@O z+pgcY)IGJZlMjX07v=$D8j45;v#Mc_BQ0#dA1enOr3IPQNtLV73W&~aCE}CQrqUP> zpA}#CVP#ED5W-GSR_KjR;@n9lTYi1R5uzo$zd^LL?30wS9ag8qlnAH#Iz#R&c=cJT z^pIiFJc}xr%0U|`?{=;$uV<`#(e>0uNqK+JGLOnG5Vmv&vf&cx}g&VxJ-Vd^(C}vthKwD@3RR2{~{}Oy!>y?WL zUnkl2B9A&>r-1-zbM}66whh0zJH#Slu;%eH2*SadKqjuSTVD>U)$)h+P!a9y9Bc+l z!vuIx++9sTy13A*tL`Q20iuzVr)s8RoAY<-PN(r~ZuBACNJ$fRe-q--=ZToHYQEdN z&D9%TH&rbU{Rd%7i#b8joL#HxxDWA&+e#gj?f#Fk|My*j@QMB;^nxsb#SqcJwQkMX+ zh5;J6wA`?gS(9M6w;jGv*kF`lW?xqPMygT zbzJ+Qhl0fYWBL+AFrQ1^Y{eAA&`YaE9kLN+@gW0@&4Zh;18Nien1bqwbM_vb7eC4p zVH-n;ocZPVPj{Ua4cz$>k?*e5J5;+Z;^xaM&#EVL4NvRb^I%+|eXQ6v^1@7U`%}^3 zoU1}mrLGPd+>vZ_k3mQ*wuTi9cLc@B1-@}=gEDFX7YUYkCScif6N)1NyQyoqy6>)Z zJ~My;%EoKXtvy=Kq1)*+AXGIurp&w4fbX0EQjlnoZdfARzP5eIqm3m?NK&g?3Po zclU=)gCW6F^v`gZqFOA!u1bJuBoXR(KFn?*ksedtx1j0sLG00%duwH+N`RvV!oZ?2 zfHRGN;8kP>WC&CF)_X!-Yhsh0DK zYa?pAfwvcI0|UEGP!sMdlt^MpDia01)k%BRZWn>IAngOtTp3c!LEb79`Zhcw@lC0E z0E5q@xlsw&{8A@ZOGMXiy3+cNi36_9$gCftPcBOM^TzAnNGqi&*Yxi?eL3|CqyOLP zOIz(*xYC~8ExWN}Z%O4^r(?b2Qa+#hLV3XKZXt+ta@gsFg}#D>z`|S0&Y*M)%r%!l z7~U%hIVTqv6hk{8vr^nchmi5^!7}h$YuT35meZb1EcOWp3Ho9e_lht)NsZYp%bW?Q z=n8M<OC1iSj@B1Ocw|FRaR;bW`o!!xK4z!c)c0V6mMbzTyR;;| zN(m|Oo&!>9pfh6laXf5TM!gH3*k0FlBr>N40ixfF+&BW9DAWpU9Le|WeobqMOgj`w z1&~pnK*XmZsz==QB1-ua3ot4O@Xto-7M8lZxy=(MU?aA&+_3t`UCFpXjJFq0$SQr| zr7aDRiO}(3=wy3%Pv5`EpZ00c3I!BZB7a+=046^03Ew*{u!xh}H9td}j=Uu@`)raa z3(G-4ih%!+ZE+UaGj~G|+|{1|R~1lb9hE0#IXze9-Y4Lo0)1#dJzGClKeU+G@P^P+ zgW?qsRSvfc?qvYNvSVN_GIZ3`tH@Od2^`B;EPL>en`2LD>4TZE1#`cCt>id##HG2+l35fjS{LO+@ChM|h`ULerNPSO84$ zi_+6COv$rS`#Q$)RR=tX#t*#m=p3;9b7GsG#$9CxtlE^`7JN6FOZ+pYjd1>Hh1CTH&5!QYO16@iC z>X}tVG<1L37rz9R`J+EBJ0y;43%L9}ar~RZA39>EqeV^=XPxF(X6QC3AZ#{itpXD{F4i3+n;% zar(=e9xexegoXK>07LwX_JrV;4jDlnD(0E>C9px^QYT$C7tN@@t61Eul7#$K`33_R zuE1*rs2uP(z124b`X=O>>pkt(cW}EEDT@Y0656ew0SKVD7(Q=TDd5{vxIcb52O;p* z0ifF(<-<1ajeB*YT$Kt*_dlZERq*SUBf07`$KO>^38z8?U;d%2@Fqvp_N9M0`#+6) z8PCGW}O_QX>v>WM~ z<^Oi{0zW$zGg>Yw%3hBg8wr7PXZ(p#AHGgS{&lj{?}asEC@NQ`=?=aB4>s4L{~lK_#-lu~({JgrVj|8D98=NkMwWWHgh_ z{*+9Z(K;+5JL#dbA|yTo;#v#xVVo85BMN9?W)7EsUlA{5!B+xS^NmjAf(|)Njf72U z0=2*ox)4VB_KpMXkY;ruLp2y0vG?{@2OHQ1!Fw8$L;VxFkj263_u^Y<9z*f&$>+kl z{kRm61$`uA`;Cv2B-JYQ=K8Qp%T@6txlp#cQBraGop1q2>8~)H;BZup( zVIuy5JotFR{<H)#~d`8|a7reD34ziWjc=yMG%!oPK|w^QxzdNx_j=vwiEwDRl25 z&;Q-xUjNn3L3i7a6gc8kVm6Da#td`5)sv!ZQ+$cR4x_n|v(j2mDYX;H)`|6pyN7+-hZ!tbHKr>%)faUW79qfkiR2wW7xVe*{xczHc z1mNVFK-;=o`NbDA1NZ?Tf?_Yr#)HJV@WJ;Q;L<(EYz&ubkO{T-6Zs9GV@9Ks!qxs^ z;uFX!?&hrIv;yb>2t3!rqd}EdxU;YtP%R7)PIHZZJb`kX9jHZYhRjHKw$_9CWp|gw zf+JfV7dLQ$B)e$cMIEsF>|zTn3VopUlF=l_fJO4UfiCZ>q>?@a_XTj0;q1F<@2)s@ zKd`-jBD~qYisOw4CQLeO&M#NBO+vOdFTOuLDN?^x{e3QfH)Qg`wez>$kc*?I{z_^2 zbMl!MIIHf~e%|rFTjt>t1yT8rqZ|H$JpR#aKm9r-{_D2qJ0Xcnp<4o!ETfc;n^(HH z6TlFTMPKt&gxO7-aiW*J(A6r+=;}O|SORv1fGYO~zNNw897!0R?q=bcA;~ay!hHkmn=`bIZB6FG!ottG zVU1SFsy6$cm5nLBAilcb%vstw|M2R#o9G1hZ)xgV_rJpfcA3e=6^_!{FY9Ov1`o}fx$1?+6|`D zQGjkVBlHvlZAMiC$4gIf-Mdm1k+1W<^%{WcFgQKVT%FWnbT;=eio+;_Kf)r0e-94I zJL7;^Icjb<4ZK()>+!;%nR_0q(c_s{hR=Yex9>&YJVwBG9f~zZmcG{)NKE;`Tp-r^3IX?*Y$Bo(z_6d*Y4# z6L$kdQSyN1IZ84C(?w0VzBv{193q4IAJGWSRw;UCHW`Ny=>{3&HIp}5my@6Nf`k>UvV4+7ciYT?FQz(C7^nYFx3veB*x%hhH%5i)(lU@HyK`;{mw8J*$^wi7 zscygGGvH_M*y9V#$U(j(_!6j)gzS*>x#~DTb0u&9s{pZ-=O(9F=7T2o69pyMc}3$( zGFWLO%YIwsKFsJ?qpHQuSg-KgpswV!eosKf&>1<CmPNZmuLNhEADUaQ*AvNvJ;ap5=?9JSaBFpS{U+bW)*-vF=i`zYQW@m+ z+TjncY7H*h+3Qr9_4NHYfs@@G)D{MmsSx)YxC;SVCetj=25W zJt@}TS1=uBSp83Ga;ElmhdKDyXL*x%(bFyCsNE;OwM-TfmL?jkY`i0f?LO}TA)NjCQYO&$l#@5zW55KR>j&d04p0131>eV+9 zqI#xjkIUo8m9bPM>-YoOrH7f18`?3;l``mgE#+%Hb>n|{yQ_9SmR)mu0w2G3#ut5Q zPVX)Sgq6_Btb|VIfE#j)LZT6HLBS1KKx`2G02)gY7#vsFr7)?Wq;&&oPzz|)DP-?C z=p_;M5H-ZFfikCIqK95F4_Ig2RPeuStWFOupt+B_8IpxKo}@VFi`2Gzd~uh159?gD zitQzRowrQ!+#>vpLr_jqy6@n?L0pWFv5)g#1;JB;b9f$R$0rgPd+829K~3i02r0Of ztAvn=VU$gdQDd?TBiXBaR2!q~M@$P_jvN*QfV>}6`>2aamlMK9+FUjTF}|=V4(qYv zgduhJ6WSAL0rArDy&emCTO4*B<8o~dd~OT9o4Ua5`jX!*iccOWPvTN4O12*(WS$&% zN*;A_IBOAWs=XO5?t*EiEh@t3|1nO0)jXP2!%+k1nqc9g`Dj^;LZ8kDSN;Y+enKHm zo{4*w+&vwh+&v|{k?UISXtvFw7}=rBh~bq!*C{F_wIE(+KT!L!GjcS8h42a3*csA4 zS2uC;`(9O>e!=@s(u%ozbiK-no2UDjWB72v4|b{dkg@;Vt_cs>LC*x42v5qwZUYpiZL4v^n=6^f?K^KAUKlH?Pz5x% z1CbX&D4lO})dW+v@5|+dF=T_b!&g)5{E7HGpYx)#3N0U~5c4OMPyMIJ7q4{r;Dpf?`!ve;3qfI-PpTs>U90MdfT{bA7b>6Xv)SH(v7!W!5+wngY7%$gt+C^u7L zN;S>P9z2=2q-q$6uHJAp_GtWg1(FHoVqt;HOz;I7)9kuHn3@J5bBin}uHj_U25PmZ zy7INR*i5V4#?_1x=H5^7F;zILQ}Z?VzUlAVnAUtAm`3oN>pK8!orz7jXi*o4lzy!G zTeo9y|Mt5-e(?c!^yP|V%U2$IXZ|vO(M`E<^UVB@qfOpvS>_tzlXH#Xvc@Ko6J=qI z7jc49oO5clTp0}?tt)dEsQ8AnmC+>`ou)mbu$rog%i9|yCghqTE?hHk8w2YM4E1ad zSJR|t3~QdijWyLWTe}B3r*uU}bXTY#GXt1lDiG^&rGR2-mt@AXA0*OWWvi?l{RK(w zyOJsy@cglKSdX!JSoeRN`R%>qii?cSqp1Gyh~bs*&YKv7%wHST&m8bPyXw9Y?@_ob z?}opb59@I{LLwtK$Z}B>H!6=n@{1t~ZX$P~48vFrtRbqpABokHhmGtMn7594zC~x@#lb4K;qNXc{`x<~mJ5}SE7|Nx3GaY8S9c(P z)u9PlyaAxF=2~6S)e{U=+B)&N{s(UqW*i|X@u5vBVUG%w&hgF!N1+ES{KkRn2hi(G4C ztVemyqw~H_n0U|?=ZCTWIBNmkITZS{n`@1io!NH5ec_&Y0=RDk1MRXrm;uYl^PBSDk$!N>W5^ZZMm+z?FbUMKOoL zjsn5A_aS@pX!9xzqWckcCuq(u4g?bh_^ilPER{F(tL4B)ZF9na)>5Gv=q@NWkKR&5BPVC!xMWZ@j=*+0ja|5$d(2cTpB)Xel&?X(t zWyPN{k95;!*}yJyzI56LboTSV#T1Zl5r{45OFKm%W z0+s61@6K~JjF;92V={gX@z7f8zOUw2bKiU$*x{6d9S~>$leGAoOWS;wEI7UQjp#=S z$a72XN94*2=&ln$G~ZUdA!!Fgt|c!(H;0KtGg8K0X;6BnXBr%gqFcMLPvHPc<_!A` zUeP`c_s#W7b4psc3L-MD@4`(ph5Ho|fBO0OTM?y$GL&ia;aOX|uy?6mnQ!&J{j0#v z{?T+G)+#kqyWW9MTf3TiyvXvVX~cFvzy9k*mgZyLAN&E_wB|QZ!S$~Iqb-F+?Et2~ zQ2mBI(kc*YhY6Qh-OHX4@MFcSCxq-AF!F3S5IE+(hW-BdRwU_kum-M+o6Ns?hJN;d zs@sM8k1OB*_W-2yaM+{B*V=I@+i$%8aVPFjX?9rN`5mwqd5LGy_9s1mW6$f}W-U6~ z8^yyG3~hG96Z{o3METTwlWi{Xnvb^;RMS=X|7!@V`=2@xhsJql-(UYcD|`*Y;U5yVABXA{G)APDf5 zthA!z*jRQllTt$ucLIx4OOZ;nVhV;v8{qNg^XxxnKyQXxcbX=9QNQ7y7wMD! ztH00Rra$Tf;og-6sUsvV#bbVzb3xIch+@nszrbfq#Mt{DsvXNuLi57GOiUv(*Lv(3 zpyQoT1hKm@J_dBzCw1ut>R!9QYjD)V$9qU2-_)FX#`P3n`&6gw(*0C)(ZX)gjh38# zDE(sg_(*_ZzV>$qozbX3FR%Me*L&^I$`zH$4eul5x#NP(~C3H9DTrh zerCM#{%1SqiOL4GJ5$cDXKF=J@R$QBOdPeWfS(pz+99U!7qfz(B8jl`+o%Vh8g2x5 z$($JdiIg$-Zut&z2&_7N*?A{^?g3l9VIX;8(lBIhkYfhC@iL)!<{xi2}Z|_$vurg1hSpf6Nb;GYq+jRKi0jg_};Lzp07GnH&?x|#1%f= ze*JCose=Z6)3WK~oWtt1lfRc-}A7R=F`|Ipl@iBTBDNN9K{wNBvrK@@Uhc z{pVc0MvsRyU3>8OEPW%L5PvohW4{ya4?ST#0W2TvAebe zBu%uXbOF4{AKt_Swlh2k%5p$VwEIa6$8a~?|6LAeT_3@7l*U9XGo6ir8%}45Kx_oo z^dynM=qTd*3MGwhQPTYh_w`-od_mk13~2o#INt)ds%01UUEHv*Z)OVqF~=rC1X+fY zITsO3^j!VmoLSM)3RGNYkU|a(;*p-CMLQM2O7Hh>NgBnf$13$C-V25=|if85w_DG870HX^$)%%RW zF>Db`t0OvTxDVzO^f@ufa-MK3(dU#ba~j?F#S%y*7DxRGUPTIo)B59NL1x5DODz@Z zT}q`a{vdpXVLmwE*}oJy{M(G=8mfQkklwF?xf{B5vcf%jky7Ge-3P(-d7?CD{Bc~V zUFAC4*lYVB)UNy-pQ2g$s@SxRm&-5%FZ#;EntV-ana0|iHW~F!(zI@1%7}c1)!DOe z);vEBX1Ah<2$xrx#KZ$9dTq=gC@~;MaDQkCdGq@2Ima`ZMB9lQ;5o{IVDS^RfC1*9Xi_h)=wE+Uik zY4Ue-!l!n%QE2d8)4-<{)_q?W;r@+87a*V7ck*jmrnNeOO{>+bW2*gUwUzii`ZQ2r z-3464tuQ3Ri-c{MEH15CkBy{4efI+vyhqco^N1E$XAwU7bUfOHTk6sZqUt$db?L5* z6B#-$&8d0a7;UVx$O*`k5o7xr=Y&fH7 zc({{Mz{Ku7uQ+NUaLsiJW*Ml=b;U!MX!CGJU%BrQc4ZyQ(?nFEJ7J-jm!Q@?Hl8jg z9=%XBb#oNwIFj%gSnR9|fO3=on_PCz9EDw%{Tc5zr9@lYYqQ5ImUG+O3p z4|zo6bt&7bTR=vqqf3SEZmzlatSKJF#6&=3@OE>Q-Zf}JTrod0Het+vyUpVauR4)x z&$k+@8bv?B{u8%rS^R5E_$~O*a_8TVj#`dv)%t(d zJvRsi3C9C#X3v@ex>KyTQ8q#AU)JPh!8m~rR#ro*GXWfgK^U{hKsYA3U;w_1$NQHFwNd?pXjj2_9X^6rTFXF#`Q#?d{1f z9kun-a6eQ9)(LC*!^BYLIEf0?d1A(W=2=0=&o8A${k4*@@hjTSAZpOY=xR{{;og(u zRjMYxbcznwGq+LJE^t`BPE_YzL-Gjj)C?o z6?Uim*SXB*gOd|diM&I$ixa;ZH^PEllfTIt|C2tK+4iqb0 zalB)z9_;OFe{!6ivZANf{zuCoXsl^!q-ypBseQul*|rqvIheK<$fFU=RZ=@7vaSB5 z4Q76hb^W>qiha*GtKcC*Uo|{xzH7pp!cWF&dt1c5USmO>$mUrh*PALj;O4n^G>leg z_Vu2-xZH^m_=J6fs?LCXuY2OWGP0<_S*Ve6-ct})KV#x8(LKvaFnW!?KF^29{2jOf zTn`&-st2w>a0%f6wPUw3Hp|Y}r8y1`3q=T(;E4FpcZIcH#l|39cJNYrO4q}}t)v`- zfN?t`=B@lK>J#zYWE)ab*}E^NCJtKc*}hP5H00M<@2H5aaAEoTs7O(i>#g*>^pc-L zeK)WWc zB~qulq2ZA-!#$b~RCoWc4}kP;$p8Z?Ur-Xhr6}*`WNPXKcEe$9Z4g)V;6+0eBBZ~s zX8e8o>n)RsA-Ac7NO9m-L0s45JTnW~%PE^Mkj*(`CWzC-21axCIXtN}RN`8g>nK)n zoWq!aTiDp**39zR)O#K)XZA&StVG{6Ji1$T^PXocyTi{ZU~7rsbhEj>9mp}6k4{^n z{-H+<4tW1H;-E1y`;@qJ$nX}Ut0tdxp9qOasV{-;{&O_RcC+_g&`#JUjuZh%K?Q4T8BSO-7(*|HAcODg z3tQiYZ8%PX%FK^(!&7lJPUca31-GXSUP0!ZH+ z(of0b#Pc$CpKAH5|Q%iajo zBfmZwvh!Qy9vkK3UiGr@mxFnFFbrvl zj47egJTAv;QYO?+KyN51ipl#{czW#o2zY>!l8JG$AMDbgGzLDX^N5iU6M!f7G)kD9 zR7>0Li*k2V57pPRg<}__!1>gwaxZ=u(KW5or9-b*z}H!x0_TKT4u+8zDQS8W7G>6M z=#P--p{L`uYRnYFqZDgkQec0gG!kk>e40-u`8mvlTxq;Aa@%X zdZsm=R^A7W)K*t!ChUO+XNu5i>I7f`0d-kCi*nb#c{4V6f+f9z36rU#SvkeXQsV); zE|+rIv~S2mEdsLUo6K(kELh@U177F)rtGfdIV3M1*4;!w59{4wqy^=TFk+0o&he)< z_dgkOU!}9`z=!H2bK*+!IWQ#hdqurUB!xaSJfvF@!7wdsw@jgW_~CQqLI)Vic|>yi zjdY*K)`Rd{E4eZ$^g^-=@SeEP1TZ`Kc&IjQ|5D(%zW}=+HuXi%*qaplt5ux)JbKBAVb}+kFDV{N zRpi?@V2`5TAA->rR>besflYmxbCD^2p42Wjg7-Y@3=Q^54*X#pCAlw7R|l#+i)#;p(23O_~oEu&uGJ! zyMQ!)*eWa*jnN`>p!X)uqmYRhjo@i5EHA|!kbusOsdpJ&&{Yf8ZG`6t z`3Rc^!40JuV4D}sLUK+SD9-D&)dL(0$B9?|8t}^jjT{VyUgsqBD~3AKlvHji90>xx z4d@ssqLBdoGp8tId4>f7MbyU(Q8_t^7!|IC-0;unQVBNB55uy%tmmM&s`E65|39MM z1f0#Z?c*Lxln6tbsI4=WL?sa=MQJWB9>?^p;e*irX4e{*aa=I#nKK+5TtgY z%fu43cA+h$i+!fs)9SR(wDZjSe%~4MJl}U5J&sgc%ANbZuJbz2|Nr+db798{`0u}< z_3}hH_Nttn0gxOi>tHj`#V<^U7bqgN@uA^`eS7~L8h^j%4%r|hhnt%GuFWuI*BDvv z-lan*ABHgX5K5C_SFM^KJlVjWNbId_12<i$JBn0)-T%q4y}3#2{-l;>wduB`#1G!N$DRgW zvkQ{$O{`N9bz~mN1WVs1dv!Kl{XBra zH&!j`hePhim^r@fnTt7arqpQD%=g`yu8ThvIv{-FC^^Ch-f#2|-P(ux?rQd_pa%`j zU#(}~V>Xsg-#%b2KDxWc@BUQSCGXpKEksg`D6g2%c+_TGSfwawC)M-V!C!Sbe;75Gqqj^nBq zX`Pq)Yj6E*$Yr;(7%8cK-W8C{m=P7{#g$TL-zVG280SYg)Go1N5PGn5=mbmCob4WEt4nS+V(nCxwOj89W+}o4gBGb+gtk+ZMfp?S*{(CCk+rimB05LQ zGvS|k_`|e2rGmSRZpf9B7p55GCw_Uv?2(HME<2bb^4)y_TIvu`V>@*c^P z%^KId?coktv06DNK6v{h-4E%EWO>$)aA3e@5ub-7W386Zn2ZW1LXHsxKU#uZz$ag|PF%633HStyS5 zv5avrJMn2!HCchNQ7jyL4YnMOZF>10q|ORs%>-%~SChs$XJ^3!DOaGAuvV};#OK%htPQCvtqR9uixw=9aHn6x)BGnqp!Y zTPk*+S~ZNtC|8~d!DVndY|E79&17wig*xAGHde(IMp}AbL9LQ@)M{Vjf=(63c^1uR zo{9`vN2~XUqt`6W97lR}|JDu{^SW z@pSCi(U7SUt?F-k57P!gZNZU}aS2A>*Dbd&yN#uc1?GfdexyLue*N&aR5m8p4@srEA*mC@0=;;GiLMx zI@A;~_Fb3Vf=S)WcWLD;vLM-RDyuPfUZcV7TY1r<{S})J$`p3`uJ!cYG~&(3bP0@S zj?wtL3P8q9aZ;bhX)(q;+Xe;k%=z4D)?Do+&iT+=TCV3`IQ%c|bN;E>Z*4E!2v`qd z-XY|kS**FN_}KE6yF(IYw_R|k!p$G=My*?-rVOvVI=#a*&!lW>U?wIk-xc2M+MyGe z;dX7FzTaAAD?94s-%y#Oz0%+vyZc9pvx)VO4T%df{u}=X_F4c>9zz_XsfE&v^E8r% zdGzREbw#Z%aWH!AF5ygikN6|BOJ_d z?PBl**Z6kzfbHydTY>lUzsbC^NsDaz{(`8>~V%y#u?5aQb{$02EtvKt zB|mfblj~`b0l;EXXQz{CRVyI*-f!=Wyy}Teo0m6}kD&m7cxwAe-u@22>)IHU!VV^m zok+$L+BD{@HvzD{PcX}IyPh5BudP<>5vYf$cXxzlDh2#n3(zu9vV8>kI-I0V?09StwCAYQA=R`-#qy&M6r|cLui)5oG6HnJzIzaRW4HKvEpq5;}Y54ADMU zd;m4s`4(Di%ClEd!?#Tb=Jk*6Padnt6G4G`qULV_7-JZ!f(|@%KTw{*EQ|O@!=vSS zGOndq-|hMiBWH531_ci}8f)`HhWU~a-9sq!Gl#;XXP3=8!SCSL6TW}??@gP|0qd$* zjmLf#2{ekCAB$;TYB2gBJ!He3T;n5Opbhjx12oby7vBT*PTnOsM-&|ZHaGIkHsKW3 zDi|qs-bRGckbe_T!0aRocRRKG*hAk*lq-FLcNe;EHU%LFWiip3Vp9 zX7FCBH3-9Wd#Ik?qYw2;R3*Qc=-wK5UAa*E`om1lG1l1^b}6;5lV>tt?XnTe8!R!^ z$`36a43{D@T4wC<7xuFj*7CW0TJrf<&{dHYd&ZosnFIlm^X?C0{Xl0zCX@3ML4JKh z-R?c91B7Ft(sIxMHKx3fOFb7vTXRN0%S@CB03tC=+)2x+n zhJI&6-p50--`=)9+-7622A*vRKB3ccX)^nL^co<^0DC=O2nh4x;hn*`_mSqUd*ui z9JE-ATV}Vlg3x;P5X!zH(^TQ`PRV-UBfq3@Dj`syKXKIEBL0-2TJNjO)pS44-B#Hx zof<=N>Sz-rIMl`EqeIttpV_=YJ@es5kMgMUwXeT!D=Q}?YMi#Tyi8=huGoS02xJM!;Aoo& zRm4n&M|dG4c|mlHRzsAm(7eY5r@j-jPX_tHyxrF(LiOp^U_my!jOaAhd_XtlsQMDp z7iKFjLuSe0%V@_y4@h7u=0%`vC!@_iyA?kPeVk#{HFH;{5*59}h4y%(x_3k+Ecz7H za6Y=aTc+ZDN<$^w2c$kbgRV{DSc1%xcxb0MGNgJfok9;I$TMkUY)28bsh&-2AC$;{ zi6L%4p0K?5&me*wgx@UZ^d+)hX7<`>p%LrG~!Z*rG`6PP;tQ2__Bq2T)hog@?*era9qveJnzLXRouPDwJT%e`2| zBP zE*SZ5(*Sr`o%@1xv8a+=k=ascHUbiBml&2O5J58%bs`d_cT@;zOUbV=yr(h>GNrWsTpSu zp_6Q9{*NE1KX%XU!{geB;`1}PI=?Po+4b-nzeg@2-ln@=VnyBT#t*ioE7H3eM@o*L zRtt%H7&C9!KHxNV6jQgPVNDDeFSRdFa~ZeN!8=zf`;V~#i#Addt>XYxprX8=*V6a7 z#n)5^#zZ^zsP-7$^jkYS}2Ok6YW0|pflDVVjOkfji$%gnQ zKmN})7pna>r%5GgXB>XLG104j>Dp~-!VR3D@ZpuyIthl8hY66rI>uMHd$L<&k;JMJh1<_(L* z<{=AO&iYOz8vE4FOjV3ZN_g}Go382^MRAI;WYIii=?0eY61!Tl_wb3khiE-jtB}R-f4;^X-POG^`W_&iuTih(oI<)=MpHpmSGXS8B zIEOuX4f0Clzlkf_Cf7i@frO`s6hEhdZ^PuDSZN4#{JczxgH&PBiqP)y_5;>WL-uw$ zh4S?6oqPEW?e?9PG7Tm+2pEPVI8d%8MGBJ$LEaREDwD&tpsFy1j-7boRT`JfZro{` ztl_3$3Qq6r7@-akiY}~UAGK(CVRk7$C4ylRmtFLjc=KCBsQp9_c)LD;MJ#NKsUj_- zG2rrvHCXW7osCTs)mOn>hcO3Fms;tb)OuQalJ<1$K6jXf+fdj2#Ru5v+^{Z$Ai!Wf zp8tk-T}j93tQ7^KdM#Glt{H`zU>~x*PNyLIJYQ_ODbIr4rzp# z^z@tGBV%yQ2Bx&k3Z_orvqZBcu}w9g&LN9=X$Q9VleF*ED#ZHL%hO>PBQ)S}+Kl#Q zse8Q8!QVs$J{~Z`FVrPU83Be|##sln&I9c!@2&x$g#Y#{)Esv1o~6w1ztC!5=n4J8 zv_TqOY?#-!|Li-#hq3j;CUF|$-~IlL@2P?Ztws)h(5FdT_ir{pPgv_N*JhHo(N=jKs+`wM2H@5GQhh1q>Bxa0M|uMTvB zxxU&|Ve~%A{&o{e-Jy|9rjXhCrH-ThKnIH_>Ut3o` zAO;Z*J;3w4lC4IWQv1z4immzXh~1Bk_uN$m#N*#&cpbR-ubgC6I_u#k=4ii5*_#74 z(ygYxK-|AVEG{3NjWt0HqAwOYbWEU)R6-1wcXcS?%IXFFYd%nwX?5}?_2qx2w37+9 z?Rdd=kh8@nBePOAc3Wu``H_s^79t&+OGWlJ$l`b4GOOS~MS8eSZOz$Dv6%?4k`Yhs zi6}JIJei2W26%rkSHQGziLZKOdBiC$H$bQ;dngq(TUim;Or&BHNnNc(_~=1W9NxDo zBC=_`M!CDOXIvzDn~xT28pIhh&I8#bYk2r69l2!=4k*+ReWl2v-MhpnSh4hZGXuA& zLm$4I{PAXWSgB5g;85VvgFRgdzZ<*-8{YGS{_%x(E6a9)HuP>~coI%)S`aUCaupiH zHD+1&;b}qxHw!}cedl2bpXbjW;>5a@;MDbfPjoqZSo@xM$?sD(r=%?7^N@gn2%61z zD+ZF>cNQtd!xFb*N{ph3eOI^HxXh+MeZX}S<~CO+UkJmat2;+jBBD-($om^YUCL_@ z5^bIhWrp&f<~L$+4V&)f>(ppme_9*GqI#6#pDJ*IY_**<-)^z_`R&I)tEOX?{PdDd z-wQbTo&)egygbdLGjK>q2oMtzVVUh%n2l!`n7A1E72ndN8!e%$LnlBI1{kFxPd`D4 zCL4FzebOTVp(PwpT#z$+pm<~q;B0xQTUH=&g<&I zU}J)K0eCWic4`l3(gc`(Qj5V7vaQtx;&ph~AWOK#dRUiVG5zI?i|ZtF zUyQ3uA*I42d08LlTaYY$7!Ycv6_88|kuflIMd!bQ5o=0Tx03P30c>?!8mdAet+C4* z*{}xgEoVmJjTl$;%T}ZB=B~e}M&ai-QD4)&FU7y#fLyKIO`zIs5>!kizo+j_0S%x zeP?q_f8H6_z`C2)Q|iF#df=79rq0gxNPDu2&dKupV8K7_UmecQi71W?2%_tdnfflR zpi(ubn=~mBI(lAU8yC54?WA=E?Wpo&2ME_wOdga~stPH>w(HGN0;||0D5}IC5gFnEG_nW|61yn)PVI zpvTko?Ywz-AQJ}?1rJ_CkG+0md#AK90cs4+za!C)F_vOCyMAOZi}B1IgeTiho4@zi zgfgipLE;owi%!8C_c2i(TlEAjUQ+Bi0MkIxa9pOcEuRT^ zda1v487qlv<=R0gVkkc>%MgfYMoQ?F7)$6ngVohM@xvc z^XhZmM(KHJ*7BSJJGWpRH*V(lO^$hqRgLp$ecFTZWc~#Ds-EHm`WBq~xn*6@gJ9o; zK4P}wL6f9e z8F`&RmX3Edj_5?3wKA*L)k833d=X@Wh#;sv%da>F{1X6kWom4BgWNQOYb4g~Qs>Iu z6o4P%*x6KvvDErrgTe@|=yOv1JSMcYklYgrjbK)+rkE680KU#pE^qb+Fz7)&;X9B- zY$n7Mo~eH78gI$zQ5r=_SCG><@~WIs3p!Mo4pVFylJ!5 zf5+ss#>{Db-{h!bYtOh+LBE=b5?hsx?CNvO4i{yl^O3cj!hHznRSF@~n?{waGoIkX zSa1S61+Z|q*jw^a0!~mNbd#Cw40ial4&s>Y%gpUt6LaWti9#k%kvUZlRm6?FZtWboI=`^>}P-;g5C4MJ=x@t3(cN;V0u^KwM?Av~-Xh@?&2BX7dwXX>EX}T` zvjif|fjU`8z>J(|G!ZyOHcYVMm5moIXafI?J2Y+y8iNd9jq*wf?`?FuY~p35T_mHI zQN25PXX6ka@26QMr>%#|fV?>Pn93>+?T(+nPXWYyk@rwU223d*e zg9eU<}zK;m-C`3X#6;Fd2n!d zMwWYOtT0{!WBiT(KqB!kwR6}c8<{Wv7NiJxH1cmHf6n>Fja=3W^s5i%dmK)jH1|)I z>OW=|_2wk;bm(%41am#uQm8`ZS?M_W8s^*5)YNnORH!{Ne#RwpRAu<$rFy1c`G@yLo4*^FS2SEW z?{D4pu7ch%RQ$22^plt>@(Jbzg#D|Uv}E(T_LZtb|4`Y})hkW4AN2ZU2a_>+r*YVz zB*^Qa5UAEru1^k8Tut4I7Oa@0)$Yh*9zC zbe^^La*b?FqSiu;$Qs|v1t~`{hYn}GBJmZJVc@O+4;EjQMTtQ8qAZxGGGkv9g4g`n zExD#dcxblW`8Vgxd7CdELeC%)wk}iRzK#1aJ7U+@-!t$16#3{!_xKM<`3*Dt=SP~K zCF7WuO~)Lp3m``be<Q5UGJtVEz3l zz~zAH$nUZM@TJ3*nL~ySx9{Ys#p#eCHnoIRf|c(^ulbBazCs^_|7lub-44@rbpuE0 zXLYpyz=n`%hK6^7&dT?Ma0+;@%_AR804E*kor(9tWy`<13Va24fn#tu{8#c7BaAoL z` z_ey6vxkbma_{$Vr!`|`6_BfSAj9S;z2#s~)y--d!F81>b>qI#$E*y~KAJ{7y3XI6`Z4nq2UP)8@( z40#YOK0Ag}U-%1B{NiZ;_xb%^{ra%E+e(F|CQ%y8-+jo7iofLgT>X0LovZsz9Co(; zFnHD>{}7Bk_H;a*za2@{42Y>bY>sf^AQZYW%3$H>3ew$Rl?vzi_LNesuh+ZNTJEe$N!oh6HZ(j&UH?a?r9 z^KF=cVxo5aWOT7cGJ!Kohi>eqzgJ#2Q9J3l*>~qpQ)_1K>`=!ov~S$;_u*JO&2Lm| z-X45?o6{L}wldi&|KFl{J3-Rm%>msjPyZLl`)vA&bzT3f14i#UmjW-rN zNzsT#jx||mL?>YX2T(N1R(2{|Ai_AO6Td1R-zF;;Jtv&Ph?K(KQ{s@JDgsr;7acEo zHQ~Pt}8pg3(>dons%(}!|((MvLYAtySW^LRLfZbjMgLsRuX9{kh3!$CZ=tM0*i zQ{RbQXVt49%<##-eqwf>sBrTb+12$R`eJm;r&7a-%&QrC9pd=HAG@~9o@3a4nYzKr zf$<$i#^dYfhi8u3eia_c)JGu{zBU#jVHz|q1nEqmq%I{SgV)>@-)G*a*o5&H8B2?_ z^PGy=*csI&B|N%ErWw|OG1Y=$px1;nYp8>O6fFd4fpkOOrnT@443J9894pbI6Tx&H z<$6YJelYodWR{fedlTg=QqdX}*G`x$uCSPSotienldpGM0r%>otgwBpn9$b+o;#zK zgX?N+Ma9hor04W*_mG6#_UCcpun#zID_&Uy)%>EH@|M-kyW2Z0$vIpa z!Zh)nx6IEn%8s5=IBD!C*6G6VR>GYtsjD#lFo=Sf#$IE$3WIRyQN&|}OanIpgogzo`Us^F0jxw6I6cR*oDL>RW0X+1>__PO>UXy8rg<$)Gd-i!!CfYpy?vh9C~|0YUj!jvft2!VJT zui$|3&cBzx!$^5oLY{jE36<1pcDgeZ4;&;4Y%tDtL174${N5yxjm$9%puWqk2w{+eiQ}P^w5Fxf2Q_*3=JOPRu|}nmEvQw zh@@)O$)$kc*z$A{JIcdFB178O(S)w`|{&MLFW-e_@TE#Gc1A)Z9`5Ms7joqDY8?%t?&@@9F?6Ndg~*tewavuPCTLWZ!3eH7rZi!;#GYnLIEl zK-V3rQ~A>NF#5Ra(%nOMT9r|v(@a9hK6c%apKngJ&KK=4Q4&6kPPx?iKQo1z?9l^l zCEZWupyN7yeF-{}cM&yu>ay zQsrqA{Ky1Gm{h4HFCv^Oa?nnyQ{{^H1J6%bS<2Lt!X<_l?e#CtDTid(d8ptd`m3T| zY2O3r!NuD9cHLNUI-BHNY47`Lb+nz#vdKZo#}^BA35XydoZ(a8pH0UV!jQmf)=(33 zQ1J#*zkR<^e7%iL|9ScDuH+ zXY=Tpn8oIi*aS&D1TL8O$<@vBX?lWWXMAMV!ru>wsM5Y&tSg+C_YuoUaoqogh{`XL z*_EWg0Td%vjIm`Qp_##G^OYc1sIMYI%_gBrU%WV9GkjhE3%WAVqANDwJGojLz!4HY<%x5)lf8a&eYq-ZDhPFnecMA11 z9y2UBYc{W*zaEAD!+_TKm8EBa=>w1%5kT2f{>Pam>*Ix!*{&bd+hkYw6ZLj_{CXWU zeTKO+`uusKbv)Xwb)ID0f%Q$LSa2CTOlj>Y98C4GDTLX4wip>5mnZF8ux~tv_>Y7| z)P7E^5_f?qw#jE%;cpV>^ffx2{N<8P7zfW}TH`g}$xA^sw(wU=2xIUR$GrZ#F|pKo zX`u3wrjBQ4{?sLK=4z^Vb{_oq)kiaz%n!U7&^>zX>_W^v&HXU*(7Xv&m3xM}V?3=} zTe|lfvK%H%uF|G5S+N-5aWUJ~hMyM2lBg&**HmL0<}m*NvdXq?<)#`36559=*!}>x z1I-N-#!OVO$`+_RX&LzTOydzu)&lf|gs$DHg2hTjgIap%`KN{=r@JF&$0Nv&V1?kF zzj&d$kV2(fnZ5hieeqqzJM&#jwgbzb22s~zAN=zbrr=Kg;)$+X=UoOjZysR{;$NK^ zu%Hz6>J{&@DgQiYp?;Dhw-r21V(dMZ8mqJRy-i<7XKL()8xI@|+{o5jHuj5z5Zkw0 zB138uG~@#pJ_VpI?g|RNbE+ z4z61G@xex=U;e#c?e1)1#H`&%UB#>(d%APSL{FIs#VP$~LyOpY$+fHqzhWpMAn}>N zBf$J4(z(P;E2v0F$04C@;patN?rq#d8)7jMmvHJCRyp2%}+nVdR7T%I?YfKwYIj-76J zD^aV6mrp9Os&w8ziF<~5eCSRI!E_gqj1No3a|84+2e((dH6Emr$hQ$wU?&qnw27p_ zl*7epB3qi3RIYO3xSo}?xws_!F*fMpYWDV_6ue)}Lgk)?me4FsC@?_3&oDcNDy!|K z#UnE~sQGp){5b@N7tYXubpTg~ zGlnqt+g{@9U)ao4?I%xPc7L~a_+jRx#_5P0(v1=RlQwh?Ybj#f9Q zBXSKVbL53b_6d;f5yei<9i!PA3bgcon%W6V64*xq79!L8FPX>Rv{obHnCd!`RUT_c zwqn`G@kUu?@L~-cB4@ZeG z+GxVP?K_-yXz%pmy+!O~Sl~6%u@u614!HvU*yp6}TS#YaYIWuyvMNjHyi0H#wJ ze1hxCN-awrk4FFYeJ1!}fz$UPhLJfAQGRC=>1a^cM<=$ru0m#B-EBc*itUg`$U-Kz zVTF-dVBViRR-EuXRCenEC~trb{2`W0axk=!MaN|eI`X->>sT`0yvISu>07;Fb*oG6Q%-Co*#M^=ZeIG+9egZ8r;y(l zn7ad54<1-cpdiYJ2reC@j>b-|X1lcuR1S}JCovk{`GFu>bGrx!m~{IG`^Kl2h>zqR z0cQ4=%Skf+;vX6#Ya8L0@B z7@{?6=e|&Cc$r(+_`4@$&cRHmLF~MkN8LybnN+J*jjSIVu+r+1Fg!0|{mcaLv&E`g z)xoz1OG9fGfazZ?djX8E|2Fohrn01J&4aGegv9Pr@Hhtdok-|Lsz8@=Vk!eFGdG`! z_CaW-nK=wPbaRLn=^%!f(1H^yq*U@d^njQPd%wNhKeD8i=QfUhrp05x`jzV{T31Ve zIs)bys%5OU@@1@}*cG&zX+k#v3zQZaO5sN(>Q=Bwm0{+TtXjDxLD&V+ZN-_C@n-Te z6~XDeY~rKwqG{}39scpjeTrygySp1XV}%zF^O?ToB%ve7 z-^vD8#e4JvU;hzBeZ#nT^^TGea~|7UkQC|NQj!%P8a@}c`KSh0WVamJ0|OM*C_>dyB=s{gj#lA)Et5u zCO&?V6bmZr5|)mb{TQ*=rK+vCsdmo$w?Fas&?@0E`Ew__=ubW#oD{ti80IhhcKW+G z*IR3&^Jd@lAFh1)cy5Q|X%qDMsI!5o5*WtJ~LJ+XtRppoQe8s$=CkrpT|tNFa<*>Afv z=KS`TOYQ|(n^36uQ%$^- zGKMv9Mldqos}&ar;#{DS;zm;(o%mgilrz$@UpWX~#(qv)dARMoN!g_vY}`0T_|FLa zlB)K=1g=D1=l35Y|6Oe3kd7GC0PnxX)0>x~oo#-1_%#|Yn734Tylz)=mr$aq;#)?g zNLOgaLF^Gvz<42Ibr*G3TUFn2^VE|fdz$SpTrc>MJaX428_mWupFh^G8JpNcG7R=Eb+|sY=Xkco?YqdY_Jzs&7Qu#E z_fbPNS29fmBHw7-z*lze7Ny5AssiC}AMpPgJEH@lA-D@moBf5rHmHez6A^UCZ=UE8~N_7D0b z^wj3Z{6Amq#0E78V}^S)k)`AkC{^bJ8)=dcywF3yVaX{c*i2MfTdH_8%!#8N zC3=b>tCfj~>soIpAhHRX!zl&(Z``I9-6%)8Fl12KSnR=a^dlIJoec+(%{3OFm`IRl zfs`}|Y-bDhbx;BA{xbsEjgxgGVl1U)nX6fp2Y4+EYnNnZb*Pwm09L-gbi%U);Y?@8boo&nXyd5c)m zds6u4Wro{#TU6q{GEaeb{0r+%0~0#i;Zm^4nggp+InuDXTeSgBbfk=;m|T!sYO7KZ zUB>AA7bL(Vp)&v;tDOtg=>yd)y*Q&Q};-k=#^F(g$|vs zr>frU+lMhc)E&hXV4E*WK)h`3i>kOP8z=%mX@uRZaW0Wb=1D zcaQciQC4>8Cwqu-&nvg*C}1eJ#V7$`vP({w45&8K3S4mkR>$8ov%oPGulHSXChZ0O zyTaMe39JLl>=rnKLf2!en4o@18Cv2C!D3x-{A5=`b-Eh=0dX5q6tbTtOt`09`6>U+ zK6=@USmE6>6D^R+V{q|1!{?o~FF1&cF~3+IxOhvGDLyfOrdI6uT|z$T@tNxDev0!+ zCESbg-+Q6bG($HXODazOc8!eSk}A(l7-9CAfxTJZlNMPUMP-E7NLqx7+JS9HB^c#{ zrwS=Vw^S>W@(x>R)|B4Cns}Qh1%?6Hqe^1M9uWqW2g%4R-$t9`mxt!H{LDzrd85)( z^PEVQVFjn!o9~ob<$?$tj=h??yuGQ3$8v=_Vl)Ooq_40}sr2u6fpyPM27s zi~S_c9y33xVt(opCN$ML^yn^FkQvS(#1^S|=7srMJ69WWEz75b z^zhde%#!L7;_=yUnwvOtC8I3wn5InM3y(exC7(?G&26LCA^!4o9KBLoQxr|Eu+YTK zJ}DnCT4k#Rw3cY1hBdNjHd?jAf*_rie4Wa~^j1}wx~gRN8*arkzw39o>aDFn~Jc zf%)gFm9&gA)Z>`4NgVK|zbPy&dWtkQ0GUs~MxS!K3|J40YYTGq*ZXtRvHe4o%jP6p z$VPN?Ye9zMFG{<%3zI)B(8W=g{1{X4VDGEV*o@SFoR@9ZA^u=%^sOIwuYo4jK)=qLy%mVSl_dC!_urYrh@O1CBnan<9i#_csZ$z7;{&linMbc8v;W8dhO12 z_2REuxV>$RMEkm%jB8xuh~cu=S$P4b8YT~4X7eR5R9KEsCGvoH0?}SC^UuiQBXncQ zTz(_NZCDqb&v=*a)?9uiebbCpT0}N_covc6t@)*Gs(k5A!=ZlGc$_eCQ zI;EC{s@wB3XCgntDudGlaS44e0P2CBbzK_+JRy(GfwiPXt+bsmSt9pZhk`vr76wix zvCY_`*i(ZES02i!R^Dz1HB2;qy4Bd*u;vkx*m6QYA`*HOz|jq7__#p7JcG+bA;DLP zKxSYAl}t*VKcaESEX282DktBOp-f z?2^NxG6L4A&Sv{SM-`z&CkT#XUYRXfvkjabDEx7g`x_9PZ48o5DI`30t2SaMr^4M} z*793sKokkelTlW4`R7c3v19pfnN-U_uQSegu&*vdOT?)IA&uN-Ke8473}-9l%8C*p zyhb|V8}7k4W|$WPTb}4P>}?{7{4fs6u>^oo5L=7AQuOTXWTcUB4>CZ?Z|Z*p^@*j7 z`L!W}$=+6$!w(12AX%u82(B6Z@X)YH@Rdk&BD3~87>$-17CM!?Lbq;xlLz7;K zPfEU8n=mWg3}9U_R;ODiG*nphyw1xvV0tF25OJ4y9FZRfO^X;1(#`G0V*k?+ai#1D z{NtxDpzMH2OybcWHZ8UstR`!mE_e@3$3}rb)GqtOt>5*7o(k4!k%NCsMi)ZC()o15 zKjTcyZ05Z+fO>ZrR^9BJlI6DjsfJem)B&$k=IPQQ;u3RWKDt^-hdEOg6#~{4sS+rN!H_ z>Q~DG{pez|fq|{*W~b4bg-_3BNR@&u$pwma=+Yq5PPcRLFX)s zUOPo^@9;z$61>efwmJBqCdEsj&z`(abvdpGjNfb>?*c^8wh`92hCc(n zSH2I)nh6ZLam%LiO6JX2L0A6$*yBeWXA?eaN#6=8BR>w(dOppd|0pupMD<;4*Euix zt^cX&3y#ypD`v+VmZ&&i_hX9xtml|ADJJQyol{EKwi|3d((`{#nG%+h5Hm)5(uOhLpC8mb1eP{=>Soj>^}cH)C9_2uIm zs(}B?B^|p3RvCfxc}~N+w_(nN8t~*VigK4iIBq?@Y_CB$o~IL=9%GjJl-!ZkNoM*j zgPxRDvd_ZSVa(d;SLPql6EE(#J(X@_sr#K;vZI|@N^dbJ~Ew}Puw>jG$&#~uO^3}1k&?>7}2gVC<-1#t`J%CZa%~EnQCm6_c=^Y2AGCUmzTI9j4 zoo~VPI1O2>oNy0dQ!{obS>1$5@pBbwh7x}H{7~#ME*XMRI~oBy^)G39#?MLRP9je7 zAK|AD?#w7lKiL#j9e=R-?(g2LPkJ}!4urWV4Zg0P*}tD@(|PI3FQY>z9I6$|UOk*n zt3K?sf2UvhK1BJ;i+Q;jtjvs(OK1Ld-Qr7TlU69f1lsUFj&WYR-I~y~Z2x30j+9g2Bh}DC^ zMT`9lb##|Nx5u3jN`p8^LSQjBpoK?Z_^3FYC2<8DveiBX21R+ll*lw$cuZWdmzO5D z7qlK@L`LA;X{VJSG9#kn=~(czri1t$Y%HSjX6AEHASAJa)`94(tk(GykF^lZW`6%8q`L-ViJIW`)rpT!fgB z%4?4R`|rhwNZ%4Ubc3ztJ-q3v&MhZk$A!^rXP-E`eIGqEzpiRpo}*7?N5l~=VoOju zO4&t&y-?4*2h1AmG$JDm7ekgU8+Qe@r+V6RzP~5- ze81QK`d?QiiMCh0Ao+Zr_x;@W>z3&v`a4!;Q_qc!`5*xb?Z-)T>{1ItaPt02m>VrH z?qs!MhR!ANOk~wsiaHuIwg$NNb5wur6_0ZuYHe^9o_{q#1$x7qRvDV6|HkHj{S?8C zRI|Dlv=Ukd{@1TQYq*lkMbT!po$}Ga;C3~kE+YI#2huBnj>pQYs<-r$GIqMVuIN%# zTP)j#MSvf_xupGq&~;(dqt-SM)q6yuENbN?g!)^w1*hYKaL`h^r`xev2=b9h@DL-O z$^*(UItp$SU`z<60IlP{9RInd?}=0wJAmq`y5L`4fe#gwAd*;$efxNmDr}-tNolBN zMo$oPXZ-bf|I`py94*)&H#frfbe2m!$g|5kvpU8`&t$n8Y&hQ;G${N#;0nCne*GXo za!7fHM50kl4-2#fNfL`F8n9@r5(UWuo&z`=1(H*R1Tyb0;?8N=5b-}F0>A=23{{d^ z2S95BXpDAPRbzr^?70qVVJprzAf)aLlt_NnVB=lxjKf>KOAYgE7HM`-Tq4v&_XR>| zeoEc@H}L#g7%NtQqr$}V!KSk`r6_2jhYKSKf!qE$BU^8GJ0#s|PGCKay2+G_feSc0 z@tXn*x1!M9!!VF!3wAOhNU5yIa)%3EH;FIu`n5Fb&Hv0D20w_MM%F&UQqu7Cy*N9+ zQ)f&-KFJp6`%e{I(tC3H9W=+(DfH9JHn$a9q!Fsj<}%{RE{M7 ztVjB*Ijd0 z!nt_*<^$c`Kagv8X#}t)(ir4XlXB172KqHF#-)_etSBtvU1{>PyW*mm6x)diO+wD= zPdZk@APWY-4rzA9#kxMgx12}^Onin+#un)qCm7;pfI=+`Ysr;$T4yZVB%LL3Up zj;Mydar$WOE|+ZM4Z5<5QHPhkpsd;s+Tj={4QBLmoER!DYE3yogoZ+6e}IHjr7j?L*EkBD8?^-Edz zmy#wo#v+3@#)z5MUhl6PMT>{`*)>it?z&E&B^m5CCoeGGEtyLV7CLr)u}R5b(Q7l} zb0|e$%8ZmC3s$6jtvf-1HY!(h+bAR7n4pRnItl4@1ctjMsrK?i2eVa0P2I~T!6PPx zbVd_nsN9~?G7Eryb7yI6TmIcmRo}N387Cq@Fo`Jcc!2RdvuB=7dK;hi%&H2~E%oUk zHw;;?++9WyDQYBPFa+T~DEFF?94*EjLcHG^Cxmjw{yEU{JFGv1s1kAw0(k=7#(4U&`QaM4cmZ;HndAu-MDdCqN13j`TCiZ z1m7^ad?=v^>Lc(_A|XZdV{>aze~gVwN>?K_biR%$Yh%b!O+KL7p?)>uq9}b%4T=c@ z|Gw)0dpoRDG^A>X_A@jRn^-`KRx+qaIej;aQwq}p>Br9y%c2XQL$B%LN{8=Ox1tAU z3Mj+0tbwLDP*xan_*EjUA+IUK?<-zddEP>^DD(VivZw@gwRF%*T=jf|U0-69SGr@? z0MhFA(OATYxm11qo*tYTSj!>x6{m%<-#73O%yHHtNU*uUXW;<)SPpI*hPj<=S^0lZ z+EpyW18#z&q*Aw8gK8~rvv9%h+UDi{w~1E-XUj7b&;MXpanG{3Jn+T-&-Cdpkxca9 zCM9U!5khHNN_j8?ZK$qeh>NL>&LHg{tJ_7uxL?^{mNXcU7p!MH zpxZu6d_PLus~pBy|G2L63~vCtTA6z1Kntf({{0(k3mNX+g^hhEY_ zzd$W>Y4kaZ7N~y4es3|gE8P8Fyo3K}TMU|Iu7=`Hk0$3ZfX)VQ2HC;O?f0xeepip4oG~0jo;{+%Lyj zx3r#}OEj@M5K))|OQAXJ%q+fq{flh^*i0(=mAd$-qNq2pVD}>ZppJ5_wJipcuKKho^l4r?-4px-LfQBsf+BD_ z=4uv&=&mj74bTCNGL>aup*3d(12 zWNajoWS?y)|F9g%+JcgI? z`8n^^2eJm!qe60Ci==+%PAM3{udFzZt$}~xyY9x-IQ#_64lA4t(GcqO6#9-dN9PSY zHX+x&QKcP!&kND``U=zC@h?T1Lvp=yy@2Lh{|$>xjj(#dX_9^(HhN5zf~tA?XGELn z{eXaRsfg&YPooHK-0u&fIwPXjTR>px^cT8MF5yER%wKTKelR&T+l3|st7S?TIaZ6r zK<3MZY;cP$wHeN#yHxru{K3L-V! zf{E)oh$szzu^GYr3BK>^m$s@=-GAzSU>&x-%2`U)$(#LjAft6a;e#1!bI~_ zGSQ%?10WgG3W(BO?3DQ(^>T=ZuRFkz)}+u*3_yGRDs^*Fn7MKeOLwv zO-Q+LO0QW2t~+{@0z*tx1&9SkrQ(Dw-Gt=AhB9X>vckjC9h0=nd}u)rXztO#*xb_n zB-S({6FF<(C<(Vpx}Zg=`bA6N{R{eK0zdq8zPqP>-yhIHNV>Eu5^EpZmh4k%=5zgvLiZ_ zcC6p8S2mrAwjad>$Bv(~o|w2;d62I;w6PJ>e=>Y{d0jkgE^J~v9?27p7V2G(-eekp zhn+GR=^XadfP2)O>1akW3GY7+)UqS2RykRP7iP-HLg?aI=Z3;JdnUo{N1%ZGe(t%tfPMgY}Msyp805?Jp(;x}v~Rr;-Z+UVy?NUqSn%SM%_iaYaQoZ~m>Ny4RxA z(a`9KB=cG2;=6Wv_G349ow+G~WOa8k?aY)#V)&gab>m0rPdeSFzW?Y~w)@cG9Pi(+ z?e%R@xiFSG6Lk*rX4qD-EIRU{f4A{=}^y_u4=T&3=eP0r8-F9%ky!B3iSCW8>2vj?HcH$Y5D2NB*B z7{{T-=nnsQW>llEOYKFI`cYJtoPDR3!Hyxe*|rE6HA19=aawL=K~7MS5mwHteDstM zkDOF>v6(}?;VitKf&k3zWo&^`47kF#B;$-?-LhnyQG2wrYkz$-!s5b`RZjxH^f_Nn z@8iFZc#|}ox?G8u{!MGj&UtK29$(up)K$apkUHB`>kv$ka|Jk(oK-8`M3^04zp&eeBsTY*Op^b@IvZ$*C z`mHF?v|+p{I&5N6GG^9RWc@8(zZ>Ologia_;5czhMVzaI;t)!e9c8hGKT?7k#qfQi zjMK#!S^to*UuCS~8lS_(b`Gv|uw-w1s{rmPg;A1UXc|f3ycTeob`Mi5WtAT!wW9H7 z2h!`}Qqp|by`G?(GJrrk-fiYmK9!wQ_E!Wec@{X(hP2p>Iq{KNn3;M;O#+jn`5%U!;5Jm;3;j?Z@{DleHVt~@^D+B)L| zj zM8q}B({Ikh4{J_h;s?_O-Aq3Z5wxqLudIXKzDly1K=VwBlZUw%8C)WCAxtW=a-%Re z*~Q@s$le|tVLG%HEa;F|xkD+^k%pwPa$s=Ea%ZP!2Gz7=q+B~F<+sFdaFODHxnHZB zOzo2sRsFgm9rqocIoFGbDRKA0QEu&>J@?vQZV7+)T+i);7lNKCrzI!M4Lyq(XCXdp zwHd8Hw;R2BG?6}Q;N)CeeDoiN?kiIDB|VQko2!}Mze>;cmAff0c6KH4OWitQ*2!ih z2GkcLXqERv1fYm4Au*wjQ=FDj{7NAW9IQYW?_5IO61w0|3Mu;2R`;W4;+nD##XEqn zbCD_pgx47)9rXUDh8(?)-v=P*39WU)A^)F@v?I$VY{qL=))moD<~EP-4=y2&=dW3Y zyh*^SzvJ(Wi%i>Vs)7akteFbMDhKukCV=od7hj96U2jmq43|E`3?R&aqtVsGP=I9> zwmGoF)1q+zRw`|hBfgV_iWha@U3v~1Jg&dQ`I?EQP_|#Wq4o*b0A=r{LgvDNPb*h}t{je$}!Vo1uOn2LudMNPbaO zih|FS>Ns5@P=B}eojapTeVFC@jI8vPeyt$k{7u0vn>K7L$(l4Ng$5+p)+`uRQhO_r z-S4L$H9$0X%dapjU^+Bz)nCCId4mERR&vyJU^ z^gIwLiHy89N4o*ds@idB_0+^tIjpKn!l3>f@YzT8V|4Xt(&@o}h?*|11bhQ|RSwvn zPlI~edlyDT#{X_)T4qs;Fj_RRMJ=J!z<+|(RqdOkRP4n^j+B%Lr44lzwpIF{5%56D z-B4#Kh#&a}8{}Wb7qk3~uSBRP7-$#|1Ls74x7t|kl3K0_`VDCEL%t?qyY#6p z3E1)~2}*DR?t-lZD5JXzj0b^C`W*a7d?0Cv2MWQ(SL~viWfN$!>V;xj81%kUvm_ya zuUc2r@ElsQtdNvYSH+p#Nyp5@2Ooo;$0m;1jBU+xReW#v&e%!?pTtEYHSb3WqQSb` zKePI!nrlCHF-2CR#=K2ux)Gp>K6j9%aCF^EdX*d10=I-u<0e(y>jJB1?ki@*l_To7 z1Dp%ci|iPcd%=!UwWuW^G6G-#x?we7_kr)wOFA-gHUI{j*fufGpu9#FCqn`)qU{NnmSn;-Do z=iaBntn>4miwCVf=kLGxIcC+Q;-@47g>8&ZWf|h2i)F%rN_d4Tf2sOKKaVZWjCq{I z8y2lQ9(uVwE{D6#mBxPF9>0;^7*wEs;&VjXz?A7IYqs`xyQFxlaXj_V`~DNkUI#za zUfBQrg^BL)_`QYs&!11f<9AE;Ohy43v!oSybzk?0vJsLYCL1LTisA2sWI6K|q$lf3 zG`a#M9?ba=Wksh-UPxPORr5gOB^&dh6Tm2uW|sq0=Wm^+jn<52n}t zo^t4MX}0j&nziP;6R5^?>Vac%! zttq39>m`Q7y}Qn&8XkVyscHnw)x-P0uY2_SJ00(Ooz3k1l4jcnYtk_T}=n@oD2uh=8rpwBW2E$KCB%p5I<+JiK8he+4MX-D%Am z=`}O=$x>sT;s*Tc!64-iw%W@m=x`GWKnZ=az+nXH>WVFFhJVinR{<;wlcCe zcZUU5y^2{rwsiwmMlPBe*Z`v;p)GQ;fgM{ThbXA}0y&>2#RVs$$2#7CHx2LSt`=#Y zv?^7i&pPQn?HrPP;USJO9B~KdkY!=!_?OsCUdE|A_n0}OtMmXEaR*9T zkdWsR_CaXiCoPD0Zp!7k~eers@@~xxChGTB;I+<*F!1|?SP{Cc6 zLy>(?I_ZT7hwpJPw^!nXbop1kDwfSwQ1mm;j?p*gCcG?dkWmsclzfn&fca-VIOSv5 zSpOEV8*6lB3!Ir+>E2O=rUQ^1 z&IqYNx64?{s$ZovXNY{-?j_XdIJh3cl5S(>MtAFMIPXu%Ir5&XHi!HUH&&|?=4b5n z#X0q?pW7qvV5A+XX3|Fqq#AE#pb+jREq)Lx2AQRo2UXZDQ>gtEYGYWF!oX;#rX$3Q zc`SuUm}^j)%y|P9zw**8_o&>rTuHuB`85Gas;QTo|#yz5~1({96(x+zyYMyKHOe&8L}NGbbXOk zq6r(olAH(7P>x=`s$3`lO0Yf>7$98u;yo@l{)Mc=sST6Q-5;-cN*a27kkg}`(R@acw1=&>Gk^<6vjuqSnC6>BNQo>0GGTw~JZT0yh8Pe~ad3TShT zD49lf9;BbZ4dxtLC+nPxny}8pCgc_<7^gGLn2l0t#Lnk&HH%7cgY&k~BKn+sX{n@ zZG+y`di?Hh3F6^v=P6zHPyaTqP9E1Kll8j0g-WMNilE_wvIeuW2s#MI z@7VaJ{yAjhI`y)$tdAbB&vV(1?YV3>=*SLGRsh^N97O#XHU&a3!Zd!dElCl!l%|Mb zm22VK{Ot%%BbqbkE4-C%Q?G+;lp6|=8^!jnbe`^B&=KlL03~aPI}BLJPC|X>*OHoU zU4=6!lhChm?yu5G%ZDLA$wBo9z>CZ{usKtTQ3}EO^FJ6>lYcTi)hWlq3|7Od){X;R z_9ZCG!}g_^jPqi;+PfT-PGy0E!)U1s+tG%uSzlRR4fplBDWi15H2iZ|{=!f|dn4KS z(FY_YZjv+gd2rK93^;whl5Yqwf*X|;k;&^GdRe(o0BVw#JB2qLh>+f}R9^KvRY9!P zaXbtL-!7B1APTfw*j|Y|Sjm%lWLo3p?0A^SC5y9MtHyGY98Fwl!S-M)nnT+|f>kL) zCVe?1QU}pp_k)Ys&U8Yu08wxkI@=`JvuO~@-um8ig@0vAO7R4P6D*HfsEfnS3>{;j zx_$wot-LhhJpMK0+41v>0;tY0iTxfd$#4)0N9YXE3OS4RD=tD&SpX_H!?<$}-5&3M z;QAK}&9B<)|Dv0<-aVEJM%}2xw2(Pme*WgQ z50cf=*?{YIFxwo#hwgq6_f_xlPr8sfg14=3^=Dle`TI%qi9a`vD1}U92QNc=s>aFO zrr%edFK-pE-rCi)mO5~hvoyQoed@powCAm1x6mWGHM9TuqMx2UQ3a1gc&u5-WEBjE zHXQH$`*LM#XpEir)hngSo+t4t?@9Obnl~KCm)@7=#`=yWm%d*z&$r<--350iver(O zI@hdj_7p1EPL&_E0)=B}xTNSS*R{si16$OS6gFc}<_<_o9|<}t#5*6x7i12E{$-Ei z(n&GtN}B(K#qxT=TPna&{yW6Xsqe})tr#+60++Sm48rMRU0aUc&loRwXEidl%xoQX z;g|}2sZxIp(Bw(sIBf4mRpSD4QsCQdqSySXDTNQ|CeQZ1~ttM_=ej<5oJW)KLeB`ZFOU$QYllvDh^ewXlzvBT47YufP1ZefI@>QCmu z9@pNKC9jOR(OAU}czGp6$7)8KQ}1dzPO}wKQ*#|mMam7Fu2!0~GB2J}2$f?@-M|)G zu-jn?mqQ$1GkrXO;NR44L}(@Wi&0Z&$2d*6FeXzY#-~4&nb6k$@X+PRJH+!BrAfEH zJ!#F~EyQ`S?tPStm|IgU63&y&?HR5z^}XxtFh``zcvzrXsrX*|=h7neFPMsmZ8K+m zUId%%R9TF&gVbc>fJwSaC{;8i<-zr3>7brhpwBa|y$r4y7-J;D%`RO&BbCX_&!>QehD0$)ssSgB zRr-f)c?0bJtXX31bDsW~hkv1DstII5_E7DAPd<_eC!Ee;tg&)6k*uK5a#|J>`aGbm z`559ROm?Qz5#0+N0SW{U$Iq~TTK?MQ8~rskA=GhP05>82YrS_3bbPC)hnLF9R5O!W z<$A~pAeu3&pU>jH=B&<&N)A-=Vz9^;IL#8v2zGOQmuE`$?qM~KpL9|Y6tY{arJIE? zd*U$h5H)fihdFL54uZ|FYsd4zF%z;(HBnU)l?XQ)_-d+$9S47xe^a5XKO4F4z)bC% zik5@NY^wSm`~R4=`EkeDo}6zgHz!qP3esk6dZUP){I+c7)A42B+C)8+HJv#?MYl19 zw9KS-cnj)Tg}ry4Oz9Ce=bR$XDAA$&m?C!_{m!O*=os&L^&+ws9372 zb_mrLob4S#s{(({8s&aWJ`}awKq@-d4>fbA@1Ht0B~`9%Dn~VKkW#^l86(#t#cLf! zAANVG_J7GSEnRdpHS~R!Ed1hAoL5YE-067{`B!j+Z!x?0z=ZWr7PKlGd!=*piG3qJ zGvmchr8}07627^6&-C(oa(tap{+0aoRXU|8`=k)gt|tzB7?qQ$E4ucd%2w;5Hn+V} zRM*{GCVmWAh3-EllF}WmRBi`ccO&RnCz`oLM9u0A#i+T+xygdOmkA95p|orP7<}RM z&_|6&i<;cv735_g%#blx-RdgSvKMLAxS9iE%fi%Jntn6BZ+9zd0gC0jRApn($M0F7 zI_5G+_DB1V$!ie4y}LWnQb#zU`R_-O4l|o%+!Cqg&1N#I+jhf#ePL`MDm{m~gjD+a zu7>|F3SGOJ8Gzc)X(*OU*gaU9?o-i~t&PjSYAy&jj(5{$rPE_CvV6O3bX@K*Y`80Z@c|m? zKt;}JqWJEb4l8XW((d1)fOi?pM`i&in{P7U7Ovja-D0*cUCAyiqX+&8=?n~(tqWzz zR8#t0VjX^xG9`PT6sr$U2W`>K+&W1{HBW@|2>Pg#2Dj+GXTGdAGz%Z5=v6Z!*s?Nz zMpSdOD!KyIs^;~E$oXoGCyiP_B?eRe5LCSVJ>^LFC6eERgBKk(Ws=8$R5oFYbo{22 zTY!Z8n#U~=p8;qM}3 zVbn3F1+T(BoP~TKw7*Ri-}d)`PSbtT(zq?`MNOar3pLdt(q2RKXv6n0eF2#_i-6R^0BI@I>6|UP@2ioiEdbz@_UDBz z`k~O^<5FF@E(%NLe|VXJCGbCd&S>Y3fdCOx6r>Mab!JMzMqyM@Fd<@F)#?dJx7+rE zgWb6FRU-n(5|omj_0M!Dw#J#pQX>TRLpB_80~cViuGo=$TF3->_wI)WHb|*ZRk&MZ zM^23^C+__NLP;`D$<~*3n}=G-sNzB{&qmQ;^5y4^lR6>d9FL!qnk5>I(xtTmTYhfi ztBc->M|JWNdw2@3Ye7M7~*4kZh#(^Y^J- zVOOPv37e4xI&vMEBcNS>0Z+V z=tz_q%Mr8w1g4qE|C?su&{1$Ky*LQX05`a#?j{_S&iK1|kCH;G)Ccq9u^jB2l3rF0 zR4iU!I0u5vYQ(c^mZ;3Abke(`oNID3uV!&!>&`a}i+X}`xQe**cVtz4iZCxiM=2v- zKN~y9TVHBj?3sFTv;O7f`zY%rtNAUzC$Fsi|EaPoKxHtMw}0Sw`k`a@j=E3xB-d_w zncg~*Y0P83{OfkOVppLXo4>OH(d}m{%USLmMReUw9>@vw?e-fKgo(uDlXaA+gYK*j zNp4-v3IKe@3F3lBg^C4&9z#E=s3IZ>TU%2m9R%Q@()KK>P_Es_WRIDvTXKg;RJQY7 z9{yKb6PrH$Vcz!sDMWLmxTpYg`ZANutZ$nYI*(=-ZARptDS2#s)oO>k-?<-9tn&|D zZydWKkJD^Q7(54!4e53*CAnWZPR=MGl?^`pB_{9PtMHwQYvZKii}ghO&U2SY3&)K3 z7gbGbGtG*lvE+PAJ5AU74$4M0J|L_yHb+sPIV6aptQqO*viJP}=EqU{bsi=IWoC#l zi9JH0K98sUi|PFgKd&h$T{eY+197S6?_COXDf=}h%kW&J1Ar@g&M zPv~RlVG7aP&IU{*Ztv3}y$1R-Otv$UEB>%d*Zv_Y>?5Itrb{;71DbS$Iq;O|<_s;) z$Qyb=;DBbBnC28>T${vJNxtFpp+?8RXKQo~mQdS8XJq(S zE!GFfzucYnBSz|GRZwo&IVdv${M7!8I;%$?`%6K>TLK~cEtI#<#5HbN==X+n1VaX< zhUm`MGD?@%(u3VoMxYKtH@Dd0CWTwqO7zGQ8v79_HZ5$CK3*3nYyOW<4BBd2r~3QTVXX_ipU% zy_)gunBDcG?wjKZ=lD5~&INt9`TpoG&-#7)r|Gqu?-kC@gBTaBQK4Y#>o9BcUBdmw z%`t3&SusP*W@2<I5(1yynN-Z&aeI&hWiQPoFPOjVyHLb0KTsb30|(sgxa_&fdc|~Zg>V7O@<1N%gI1<%-kzB;e9(D8BEBn?tGSt;<0Ldb#-N7Udcz_J)ipmP8KN+==sTFL;lh|0 zhB5T1{37`Z5vvv*FgeS{eARKzMRB;IS@=xe;WU}s*W&ZRM0^7QpPvGu1)8cAEW+_ivJ4rT8%GWh9)AL*EIX+P&n?bJJavT~1zs{H#3dxxGEM&*9L2=3pQ0}fq6O@i`vcYdPZ+6H0UBoLH2#iJ z={9)gwwhWAf$-}hRf4a9=jgenb3-q{-1;TwfRA3N{Z`*2`Sht-mvb0b;E}2?U{>_c7qV z&u}Rhx>Q`OKQGgm$}V;eE9gT?5)mDNv~iF>dqdAFDBsqz&nE}EI_4?FQ4 z!;_;w?aK=Bo!9P3azZ4RY_usOgK@{TnW+UE)s18(ps%|p9mG+knS0z6wfQWkJR=S5 zjp`b=gvIp-}(jX#%{k7~bf z-nq0#b}i=3-Vu19pN`uy%nR+2;O-<}Q>}5`qY6Yz@0hDel4R2X#+CI!?MKdpUnGN# zbfLl@$6?}Weu(@-DR z05q!(O16qKFz!J&-=+J5ow{~}5xvF2&&PNWJ=Ey(qb_QAQ8CY8MkXAgT&Ar|ws-3P zH|Yir+TDu_ux0B1k-9Ke_g(n;wDMQsbCIgCfjKN<@Sb}!2-?l0smN)qJ6cCHL+>oK zMZYf@ws~W%{fuz#VsJmxRkhegFnQ_ZUXr2wv4dU7mk6cFYkXskVnI7igJLRCottAZ z%LgF>>X1$d41qfXL)pzyOpGdU-_&8IW=j0St+qu@JBtK0cQ6DptO-&0)zW-K3V7Q3 z@G~T1LDo=IBh&qsv`G=Q;0%wiCG%`n7y{~zrOJV>n8X7V(hMP1-q#l&SYv1OjLy z>m97WbR4I;Ai=qxBRM1KpO@(p@VH9Zi!8;s!?ulYeSZ!u3^+GL+erZr3M&kFmLw}N zUb+x!GF%FvYZW&mB|wM?O-zMI3FQ`cRX*+QI#RBN8%!HSZXV1%VScBKUO`b$q1N86 z_Ola|!h{v0*Ot6?l_Hnwv2h>g1~I0+p)hQ2NM)HiJ&>_Y?GSY>cP}hdX`x`^Q5ERW zfSR^5y}&yjpfOs*fjW-5xfNjxp#gsAt_IFmGjYpm%M=u&7s95}oGw%35Z z%dDQ8H3i&b0JO&X>llHX7;1xI-rA|caT zyB{3&z`8P+z=K8@%24J|cPtt}r(KXae4bhY5W)k>3dVNje5MnF<8tv&-SI))gLP@q z>TkVbU6zbI9`(hKZ&e6S6XpF%+OijJS}yC_=_^i5$mL+(RM=uXGMF$* zDNbb7#U=n<#DD!hRpf{2Qn6_4B~uhT7>TQ5)atX(Uxgh@fnRvD=BsT#y|8sBvqSRa zk0x})y!7pg9Pdx>2ou%GvhDHLVE)PYK3tf&BVmGfGO?OpvJkWTu<2-ia6&b=F!*)V z@8g4uYux=gKJxt(=4q{+4d-?G14)(|)Zf|Ta?ac4s;VA&Hd%CCUJ=99eX>=kLj?dp z`zd62)NwhTunwW?RLo4=g5sI{gKnM9n%hJyLm6{_n5WEj$r_uOPshZO>DUMSi89M$WKk9cA_u=Iv7{nikd-`$gFVRmG~N-k3` z@=g1TOPfub_~`_p>ghY?7p}dguN`u%>>{3-vO^x@=eF6iw;x;M>%Mut zQ{2hNT0T6M5v2CaU218`X!yx*e+DQx>P+8aSPjq04H1-nf8a)t4v=!}Rc?$hNxPza ze;kXw6#LQVI*p!v?)-$~kcW~iX)X#M0W=hy3kRcY=53qu@oiZHYV92Z!j^dI7=WHT z0iFtPGW>0bBLc9ZIy@9f3t$|o8tn#yeVc1dzdjA{}%*Ml=h zo$FHwv^DP#fx~$T_BdER2?<#ra|Fx@?e^7ic6xr%?1o}lDD9+isXyxSOgclp9Pku= z1K8av7PqmAWz&?p;$FqV_t}`7DMfeQ-==+8JJt0d02wT_5pLu)g|>BGF!g?F^&qT=EA81%Obdq{NqIV3p@Xpq@6AlR;L|#otlz zfN4iEF8uEzOH!=+Rs#4%!a9oz9O5Pj#3(XUEU;8BgxCPJu4IvuwUH31p&4vk5X(8k zTFf05>t^lU+BB-H7i^v3O(iUA1m51`MM2mctiLm8r~y?UD1H!20Zf3Q$ACBiK-@Nt z(?xV&knHj*Z13S`fDp=e{k_X-%$Ml7pgQBoOS{rR(R>!@L zWO62Zcm|{)z~#bF*6GB#Ga}n!_9|Uv3KC(*77?B4G>vrcNgIj`O8HiqGtph}J;*z< zh<9Z>M3THiwgb7Ju3gJ6HdFES*w`LlU87(>RQ8%%P0o8A+l6s+wF^-pIo{q#wn861 zZif${Z{!g>1D6aO9bB6ELCWlaAQH~B1@hT(?VS-71|edy+U<2^Eky&&DqmVI>{Ln5 z_ShpEc)TmtWeqz!9@k|L&yff@yv9S$?9e|MOh@j?WPV0_jk@#Qz2D;;%8~;)+4Ru_}v8ad6 zY2@gvL9oG3^LzOIm2XXj({D-H2M!uPJHV{jbO{kMbW-Y~p!bN%6dXzKAZy9o-3T&R zcVwkZt_x$@*Y51c&?B`^*M}P>O}_4U_U`Pz8ZXnXZmL?LA7Z|@%K<&-)9+?`A4)}S zM`C&R4wrR2nEbRvs;!c<9&dX+aXeM7?`VbDoxc2df4$8g4h5RW6_go0HBPkAn)8T= z-<@b~$J4^c-=uVS=5^2FjH)SJ6db!*r9;N^5;%p;Yiq^zpwy9I~Z8$4Sbj`C0UI8@;4 z-Z;$aous%|mJu}H@aE5jncm1PDShv!^eeyqv5>UBKBAdvv-kUE-wwOXNXCL?^x!3; z(nGg<7skWoJ$?#^Le09|-Pj&}{o!OsFFB^n)m-<*Dc8&J*3&Wp@*D!X$@7{IYgRXg zrF6XG>=uV*fC}u}+uan{sO|N+lT6_GN?ReTq%4YJ&2#Hc411dsag5_7m3Q5+jzC-t4i zpz=7jidzZ8a)e=kv7x@>VP>V+m|uvJtC|X%4b!=0y1q+$D9_1uG~-E?E5%aDZ4?_z z^f)_29|TjyWE7w#qgQI_5L$3)uw*8OxfjFER;HQ{Axx~QSD>akY_SGQ4#0rp9}KIk z1u*tNZj2Pl28jy~ZYYKGs8Knri-g4kuVn3SWw_}_F^XCQrm1a_-9YvS19S8k30pvu zPRfQFFn9KOQ9XM$6 z#TGH)PU5EjJRf~0VJJ4}1ggn&dbs%1%hQ}chWXihOUsL0&)aEWUOp^)^{+pVIE}%Y zzF2wsamSKXq5Z?u;)<)@k3ap-4pJr}uglBw=Kej^{GgZ_L>@z&A3)NYM`^!suk5wc zS>qq?$|nheG{Q_H|6m|~Mw5Qt zDY(S@{tw=FPup>)F_gEv{65MOXyRXb-}dn2ZU}d^1hd-pXPM(H(jWf&yj26GQ;Q9C zyZiR`Q+8o|$(!A`C|r<&5*{;P(RIx9Kk`Pft&H7^e_s?HEd%udbN->S3N~|ad|c6& z*LEX3pxr<%Z1{M6Vbs>wiv;28_$=SV6W7mb7B%jFcyJ>&qCG*uJG@Bwt8#c7xtoFP z*7aT7GuHs0+HYA!owI0vfr~lt^N7R6vFcv<4luNUE|LG)yCc^)PSgS}H$R4l-p`0G zRhdT6B4AVsP?j~b(a$aL&1;|Qo+AH7=x5%)KQ?k^|HSjtw=H!l!VF2|cRgAh7IdQ>&tcS%z?0Rd|0 znm91)LvyGQN!xZ@KCtc%{TJr*>fF#m$$(U&A#|>H*+*$^KTa|xOrPOE<#}0H%Q__4 z88;;vr)7yG-rHT*xL(k`KOc2V{sA_+{#zp?ReJq?@QLxSFO&%W-xPV?`X*zt@30Pa zWlOO5;ynlIOHunkuKdx|7et}wZ4!#`^EO%!*q4Xso5X<2yq1m=h~|BH)!~OM=!$-% z@Kc?ZID}7h#0Q*PXC6)|7}j5s@#sfowrMP-GXbll)ox56u<08qg)s!q?&Bk`!35yYshjyhdf>Bw-{RH33(u@&QIz_VlM?;XuKBf?? z+}l+5urg>8h@*q-(|_33EGiBK12gr360FUBaFLjTpk-5HRT^O+>t*Axy0jGlM;jwwy!G(WfK&m3RfZq`#e+A54txUBHo($YPEqkn*S4aAQfwAvlDaB9hdZz^d79G^_3O)gTi9$!03~k5S zq|nbzknf9gr|QjZr;-LnInSFfMIG*#ENcpyW$;7B zQmySv#t+($WS=uh&DFoapup)oyJ(^B3X@X4p6gWD#W&og$D9t>-QK`dIKw5~E{yUC zT=#M{!f)SVG(SvlAk{CKT&G7WI*fidGQFvKj2X75sb2C+bd2Vk^1g*NbI<39gL4lz zJD+6|m2(#oq6eYg?cTBSyBmc=r*9taEI4|+QnH;>ak`2!1Z7?{ z!2&Y7F$irD&@q%{nl75rfb+OTfXh-s51?w_@c2P#lZ(Qb4|baK$A5z6hij#Xhi?x= z-m<(iFcy3J#QyL+YX`=Nxj(ht^qIu~TFGF_RJJ({)lfRvSx`o~8}ZbPq{tj!A{o%L zJbq$zW7^do0$yu?+ae{eDXd#jHB)s(vn z;kb1LtOOrSj?u%o%Ro?G{wF{t?4y!vQSMM9D)J$A(VQ%NvgU(e`?Y%di-gdZc>3SK zBOhfSeYZr!>2{-5ZQcX%A|24=W-e+s^#aij?35~Tm2gNY!N-@9u1}QYVL(B2ngZqEWmmk z&bVfQb;x%kVqNmxEI=Qk0Hs-Pkc->h4i#u52ex-;k*q9KJ?lG^;gr;C0`{Kr2~8t{ zTL>MhMWA*J&+l071`sI#gD)~LV!Tu#v#}cq8Q;_QEX;blY0p(lz<#VdE?F3DNbR1`2%0 z)lBFqJ=pLdMn~c#HLCW1eiJT3&p_e;|~eX%}zAYE9k!o_XGS+?Don(=&W# z=)3hk*QZ>aJ#Fucw6t>TtDBV%#--f4b+|)$+zD;no|3(tlb~vE%zzt)y;5!JjL|#-%2id+*1EC zLEeC$T*Qd^&zfF>wa0hl#7$(t>ewS%_Q~-5{b2{L2L*}Oj(QK?>Jxb$#VhN28GUS1 zdUt_)#FQ8JAu=qWydnC1sr)xMSG)N0^x8^Zai*P7@pI;2WfU1({st#~)dc%E{utn0 zi;|0?*F`Z{nRmy(;A9$Z$G_n;WXs0a(M(Ocke*eM(dD`ArtZ4CKMWE^X7#ZyD(%_W zBI{M_(<}w;Tc_@lymCo}PxRRCJG}F*L@3|qyFY)dJE^*Z&4vg@GRzgEM z{dt(MTGZkT?e(H2Rk&yySu?0@U06aBJ9d0loeP^~>9od`9?K``#vE_HXAjXZ3qZQH3z?^eki1UJoa zlx<~T^MIrp@ziR{Gf<4u%trCsY)SvPrS>UEu0UuDLL05gDGmRd6EYS^J~0n2hI%%pGE7c)r!+l? zNN(KvqUT$$V}uOI9Hk4tS;=c@RJ2IBj{7&orVEB5PvMKan#FI@ZI=F#6dNlHT(~o? z6A&QV#AsdUe5?~3b6v*;9%;Ee%rid87)!Yq-$hXJGhjBlv>Vk-kFP5ixL1%@QnR!1 z*w%PrCn+Z7$?a0vm{Kg}mjptL;*d%xWYTR;&)HKAt}qL3Bh}s&D>!wJBarG2r7|cj zt#Gtqk>eCNpo@-C6mDv8ie}lT)`NXHc^2I9 zvZ0}Jgi{$D1`>-;;ZG_B!B_-tPGqw#Gb*+}@atF<%=lIo>n|CP6io_=N7bu}diG%K z;fEb3n9a8EjrArc57{g=TEyMb_~91JL!APKf$<&1r1+KN*cKZDZb@Mupg$$=E2ocm$1cPC)t)Pq!%&k)^w7Rw3eb2f7xZm$v>#TLwUfspIlp*;)&plk% z@2YU~!cw!SVc@Nukpbx{Y6R94`#{AGnFpH;oDXv#FbwK?e2oZ^D}1AKDq#*7vTbPu zMuM`{XXQj=FdD+1+K39?szU-o7xAIufqQVKa~Dzpm2?kE%4(qkiU@=PD91gCK6h%7 z-etkHHP@6J`uq1C9^J4q3?(5uLZ1w<*Q{SZjOWAg`(C)8wlcKGF6cSo%Pr2-WXf1! z^MUTE+B3A&Ye`lwqXx|nd{Y~(_wHJF&9%qXj`wXO&;I9F1@Z7dzD^Y+rB+_QJ$L8C z7iW%rV_PQv=QXqC`t3h_qdfb?jemVpG4RId>%)d8LWf_ipS<^XA6!7Z6D?y4l&q) zy*(V^qH9aabzYB{kX2N;M^uIEfqF>hf%&%F-QryM?^_h%4wvzDYgg>6o3zu#b~n9$ zll1NLbWU_e=huY6bVxBa*R;Vi`)IB5Q7Zi(sZI&<`Uj&X@zYU12VKSQBxYu8qzQe zAs}{CY)e@D=zt@j2iYcXJ}7L>T8;?2&ACfAma25!xo4KWFxqCE+Eo;vf9)&Nj(e=| zWBFYrtkxaxbISKw`M8%jbJu*LYH?|*Fk5q{Vc)%+4qR8wXy0-Fuv{kTt$PJ}!jnx} zU8itu^dN1q@i72~LV*+30gb2YU_t(36FUO&_2onX_DRR6z?z9W=ehOPbuRUUl-mMmq(q>ss3i`fYHJUThL&ih zp3Wy$4{Ur+j``tiH2+_9>vxCK%DD8@E1imuvFgHv6H_mqLW+?(G2G(yCq3f&GLB2z zF{iIqj9H5peQphG2@zZqXB{_u!qsZ{kKnVNrY&^o@4Nax-rDvY&Hl#u&AX0_9{-?n z&gruk3fxV1?TDQ4d$YDj&OO~dM)f$+Zu&NGD^8f;`||_yf5%rEQ7;4>__>h&=yU+7 zdg;6L47UwY_?~;@O3U6!QNRHeWQJRW(3`8TI%+gknbOJh)vDU-bus8cygxTJ$XP5_vkBibq85KA9E?!S;PQ7mnwCdx0nFdeB*v)n?s3 z2r<&|M)Mq9X)S)l?3~6Z57`IvBq7~#%zrwOI|%7+vOgEtN%c&Ay6k^u&{qqLFH32~ zAyPkscMIK0U2+L`(*VCX+> zd-*l*UP+s}Mx9!-1?`*Y?(_h5D8kKF3uHpLrQDZ6OIhh4k;Zj;9R@WK#x)>4TVc7# ze23k<_kkm9Pnf~Z1l`)+>3@2KLeFeoDzIrd_|Z$87@mb_z#OcJqO=gGt>W-B?hVQ z&>i^@*bTllDwSvbV9EY+KTY{`KObU+Leo}r_{ZSG#&>g+WZr(?$mcOVkV}6@c|S`C zMbYD}L;dWKcst@SnEf@xO2g=9}g|O#aMFR<~o;7At8u<&Fjbd7up|97Ty8 z1QIzm%Ew7ovXX}`%<3Qk(^RGV7+l^hN?)7U5pqPxOH%hj9Jt;(t|cvaq~bsn9ScPd z!U_G~k|-w%@QfgZ1qd!DQYRQ-6E;TFu9>YB^=(Sk-X7aT+F`QAS820I1$02^F5FAi zzjYY&Ln^1zE}`!0bD@wTgD$dWYZ3!lkz^u3TUepxL{texP%f-Mbi;pZuK{NN6&@U& zaPYJs-L^5=zO1mr6@oZd3AGdXE$qZwvjMBo86LR#v7%3ziEauL%eqUyiTTls$ z#XiEM;pPsl>zCf&>1*>;{89j=a<{QV`MpXbLhK-n9v9=TU$XE~7y|);8P$h2O@+J# z&L?**0%Tf?70gGs(nI+DrzYkWRu-D(Wa>lSD%$rju)g!_DI)E+&>fhJeGi6fgpd9P z1hR{re*tc~^*m~B$PS93Av+77)*7gVVzXRJLMS2)oHRfXW8+t5ZDV||Qlq#QS%F2c zhrl^ag990#-mqz_yfmbz*VAVUI5W8WqCxR9{Z6NZ1_ChHdBWZkG(WWU59ac{Ipjiz zTu5n5hwA6D8onJE`3)|@&tIKfLCM+Io$`~FG%%5NG$}#6_eqeH$a(tVFA=6)is ze3oANXMN4jKR^BVUZbz6bwg`Ob-jOW{9{y*@ct}Dwg2|kC%yluY6@G)?U-I(AN?`O zPgswm-MUy7O5u#|PtvySk3o*&e)kbsl=`kH^s`$>Ud6mV*VkLS?{J@eT?FN))%%mZ z?)wf_DWZMqTE``^=|9T}yQ^PcdS77`=$v0MQosmOja1`lGgK0MY^0|KGrPNa3VXjaH6zjudGoF(k>M zJc3T5uA_==js^T~=`iV`b5vSgoE(f_uT!&|XEm#*i4tFi&Az+~;|=|Wmj?d$(v=0F$0hIxc1^ks5{Tx{5+1{X#6glto8KU3Gw zw4O)TvA^ftl^n~j980!86gTc3B>GBg&2~s^Zg;;Y)54>pagt2&i4OGc0f9qYZ;V&G z<~A5-Ykx?#ysskhq4wF{729DWSs986N|Z8mIaLPX5O@>{mf<_%&mxk~n{r-HP-1zm zC86--3CZL_$=qOSzIN2$gCXCZ0y#vuaaZ!NG&iqJ$+Tr?d_I0E7Dh`TAdfY-B~}!m zU~p7?kt|}i_2#ym0(u@*=Ce!N02xwKudJp zv__Yu>~oT{ZLvq}9oE(#oa{G1uoNPSYw$7VyfBS2bOSZ?u z^S@M=tNonL2a%s@Ie&WiuB_szp5d_fAca@ZyKg&o-SxPq*cbHKBYyB;BwPL;cq2c;@vetyIP0% zU*Oq<_xr+)I$S&t++V-VPxSrr#xCA@Q!QIVQDvU61&(_wNpgk%E&=ryw+%mOb<-Nh z>pZ^N-x)iWomAmJmx31jWbrC)oV^39bLQdo0jBcVS$bZ*#PdmSL%34cH!=D-HHmee zC!54Fy_!l7{u&D0pzygh>r=$OEn7U=jv1ICS#kxpzZaY)5a0L{xj5XY zlX)=R)`4du^axN!ZrhRU`f8dQv;Aw&;bYge*S_8rezEjk4~%@i#dCh%ZERa*?AOu? z_zaNCx?L7uL0KZl11zr{;omG%4u~Rb-B3$%*rIP%+T9&y!F=2ntnx(;4$&yTRk63g zQzG^;N4H4+N=UG=$nA@y6Au?2>|&}3!DoKU(FoVt1y7Mq62!gQd?*6kl397nh2`G^ zhXK;56xwc|7=o!EEbV-@!jk@L1V3DN5T!5@^>99cSr0lsHMMUg2N~>+GV z5em1lJ}AF!jz$`&^9)bq%t6rmxAZ-~=X5yPe=(A8W3}2)4DkNkruWdVRO*97d|p9L za|>%9P{A4mW@Aw>A@W>>jY!F^%^BEcUJGIk+pKJ0J|J2LQ3yEWn1KhC+Pp|kU1QyR zqU)o!Efpmb=!kCA z=lHTLxqx5g-ulFfh=>QE+tHRCR|Azb2*uBxxLKwNAgkBv=5(_;2qL8Xv{a)ZB;CX- zAOafL8->qcFb8C6h^fI=h2kUk=7a~mQ6-O(GJN}`zR-eMS$M#0n z4M&HG>9L^_0@z}I3K|#0wzN(Y7#4#j(Hwh@CEin3sGd-=E&O9&hkW~UuILs`6aDm4 z(6$|1-syKebGZ8Pk4BX1V~|1x9BAkuDXrUv7@H9f%^lDs%p@B?9D3B?yJSA2-&Z?o z7>b*qFGjL+!-NWpIO$zm6G#UMNmTgi0+G0rS(Mn zd;vDW6YYY5rE(}vLE8oSn!;F0P6dyCrJ#_f^S5|i~;cknair*A@Nh&9EA6uZ~6) zvYa0qDs%i3Avc<8we%D&)OW-uSEmLi3xBILNpZ(SF*-cj?s@k|LDZM8GEg#CVkLGW z#O@iEh`=}w53_7_fO%3U9sA$iP4_oe%(R)H88<8dFlFZ?lzh=xo~T4vVPA7*Whlp$ z=GPj~#h{PFOYj8ji}Zr`7;t0EO{5l6t{}P?bA9UP-wA4)cE-6YMlBoFFD)}R?xh9& z9P2PO@)mq}YwuT=9kiiAI>9JEG=`<9&ADWUf(%hw)s)C2eo<5JE0~3{7 zz~}lFo+!8Gu9@aGCXxq#OjDuRYfdr@k%4HRD}$`1Fv|t{jX%^l56i8iP2-p>N|k!A z_f^(JqU%tl{;r?=B$>ux+)D=^nwL1O_luT{e-9h!Yy8 z!fi{U0%4(FFVYW%BOmrpssOo2j|{24h=HE9^--)xaKeb@oz@D5xUssTAS_aFPzLds zAs_~|AanSVboj?;eR#Iu7F7i zJ|knpulOhoR?He-Y~1_#&vsP`?4a^me%$-bbJG*|hRtr&PsILPaPnTY+l}{CqQvo* zq8)x&MUExGUX_(GaDYjVmieKIXX`Y_u?y4GxX8kdU#faI-Gt(dwuDux09B006ZCCsf!!T4vZ6Tp5l~4-o>JpDr>zry0ZbDJWwiQdPf-YQ%roamG#}lMj+G~N z_dbLAFEBly7;Y=?D|aF^0_Zj(C%$UMwY59=r^KtlVx5TMXb)U09g*zGcfuXc?cFh zQYGJr1Ap~EQ79_;0hklwlTvO@qP8KvpQfeUpHkuq4n9;-reTmW*Jis?zA)2p0vIl< zg;S5mU>?%9(hl2%IZFTW;mNc&&OLCKO!Ci2*70*c=iX2CKUWH+K#WiKCq^yNKN^7} zTrU-<9B#6SfjH#{>F2|YaF#?iRA8*Yg6Dv8J-E1S-x3h`u4Hn8`g^;;7Vd2Sv~id5>T@-afnq4JmJ`6v&)5@u<%JpO?QswL`FS=MA{TdI}pM*@5o#;e_( z7+12I3SXC%b~RdRI$M%#B;Hpbe?f7TObkb+9s|?(NRsYt`ps(XZ;h67XbA2Q$&mqi z;yZZJk7YNJ9K8W@D)yK}iwC$N5G@8t^XX;{cOZJ*AC?+A$G|R|PWm(M0!LRT+B2Qv zXs}#-@N*s7fcSg8!GdOav3^7#7C|EA@9Sr1N9jfqtM51yvqTAl4nb5}lopcpGGJEa z?(XWa%MNQi54mcFfRG<5D7Ij9Q3U?YP#$-9)+!+cI^RO^8LFq`%0irx>S}>rTaf3Kk*852fgVguaJE5)%wP1hQT1vTj$yIsF zFSn9!?hI>rYHTyoRh2{hN3YTZE2}4UT<0Ylz}%S#RgDMatYTdX4D?5|!T)9f*&!1s zgtGmT8GiyKif!=03|L_^T#rE>s8+p&pgO%%#p!>bvVYXu*JKD!Jc} zba^FYCg#%xWD8}fb|S& zvU2YIta8Is7bumRGb3LJV?trdaDaf7y@ik<^6>#VB%J7M-kY+iXFzHzXwFvQAUPKq z?lJAm(NiJvb5)%ELPl+Tfh%#uRqSAY!f66R>>^BhV@Umdm~Nkp5KG{CV3s~ov0}oF z`^@cGoJPi=olAers!_!e-jn>q@2xYPaS1VhyF^O%@pL3}dwEkU#*&+6Os$U z7ob77&Isu&rQNY^uoLA`&ZbsK4}7@?a?q9*S3n4xmSp7eMXvER*C+pK<)me1QPx_cZpiMjETh;rSd!(_qZCTlwRhS9b z(trG|s)mXVGX=)S6Wf~)+zog=?RJ&{t>(nS`zZlLFp_W^kLq;xGj*?1(`IX-p>qJBt zma`+c3JZ3`kA>A!8wWbsEV|P3kf?V4aKH~SU#P3piRVA_kBj1|48vY`az5tir=r?b z#U(4br~&~m+~~El-oM3)j z!FTD+jl^OLv*zGH?Qd0%X3(jN7Ar`-*9)yFEu9JmB<1|T-nMP$C%1sA9y^`rkoc;V zb8mQiqM&?yw$|hY6{A((y9G`6Ub!&FR25I%v92xzgB}JEtiziiXvz;a1s^FmI-6=v zgHy3SZK{$^@acsw_`_@t5q&6~FnAL#J-FgrOP8K{qpJb;9o*~3@Z#WX`3beWFV+Xd z9Qw#X5g?h81o`*HG_gy&TApkLW>azB+(oVaJm7d+e% zAYn8jLnPFPud;VZ9*)kQb!+z7@X#@wP4YBmfKokRnqnAc$&)ZYowjY5VW#OK02ffR z7=~<%ds(So2igoTw~zkc@?=QXRg z zPUiY@kDyD|I`J@i#s*)H6Axh=4@6rj7Kfn>fD1nq6$_oM3%M-Y%Z(@^{s-Z$= zE`;Odzd#TRTwja*+T;c9ed{=7_xFXEs0z1+Se}?v`I(7H(P`>!jE!xL!&XntNI*>s z?YEp+9dP!#$sLF*jE79ZFyTnHIDeAh8x)`KOfsrOc+FlT{ef_6uet&f(^b2c$4Mqk z7O(!40leeit=+l~_PHlzG!^nUVrBF%>?}ry2XnjQ0JEpL9lG`}ZF0qeepe-2(RJ*o zrGnAq?Bk`K}Q8VT>yZ5R*SO&SXG}=JR%soNl$k#q(d9FCw&69sdsi{RE zc~mdt-b7Jpqg?YSgwt$zJCJgzu=c#uV{RO%W2t_PX!Q;=ci?xcK4;dzOi$sANl}(04kY4Wdc6^=JPPf$H26WHcN9I` zer##d*0Aq*1jgnC=%$ZUTg0crT!tmj*WObQ%}E9jV+~xxvOKtD;bAqCU4E8Bf#U_) zdcw5pIh=pzLQt;(QY+Tgo_kHqArGjV(KizkU?wK?4QJ$Vx4vEt;RzWHqqdLHbq60v z;~9JJT&@01at(gO7rkdotXH&>R@{RP^}LDDcf(6h{}0R^W(JVdSpTp=c_2{_MD8;t z=L7?DCLv0;$*_<1Jx;H#C@caAaT!Naah1|QoNe1KOw{N~IowaUZUB3>BScVr73AY$ zlRzN{PE9>_K@=>~df{Q5+P=<|G^9G3Fn!BrZHd%H9Wqg*^iY6!;XcPk5)p1YTCF6K zxZx<=ghY-!$#>qG$t(^@5uJ7w$N#|^1HT-|`25c$@%VorrQ%-h^Y8&%XZ zjA`0T%=4~$g^x!Ac@USrBJy#8k#s3g7H6}(7{!Jc63$^peiM0CxJ9(DiVXOLV=>9r za^3M>zYgrdOl#ah#cK7PuImY%#E1aufY>q8sf1&RVr^4t_ozX4Z+RCp-?vo49y2ld z_4KE#7dez7sReV1zU-$QT5M9FW16p@>7AHcw{xvauLOXH%&*+0o=!{{RSWRJL|Ulz zYP|h}8t0X@fkqJ>ZeV=)pG9O!b~`oBG}S+rY3DYF`XNFDUli>B;oMNpz!JotE2(n0 zmWP$w8k@$2LE8a z*rp>@sVQqnNup{uJ73R#<1Vb(X)PhG&+myk*J7VDxhKRSil?EIh&>7GYXZZ+gEZ)r zkHzsjV^uVp|W1Ppo;oN=i+weFoca^2-wA zjjub$Z!29~7Foum%!_)DhWnNG01)M~S+C}f|2KrcTYFg%ww9j3CJunUPt|=!}gUG7=cyO^-ZaycGLlCm-)`EU0vq+^v_tH{AjkdCzIJ_YxnSBmn|X4 zVuhLRx=C!y8IV1PS^H&Rv@-NLS0Q9*8*~A2;$>^(Xo6-hgZ`%>uKHOqRPF%Sg&&`6 zNuIZ3W&(lLq{P!3f2`C9t9_lF?=n`c*PyrYE+N;xd*?{CUT62t{s>SA?4b^rATeWL zwGcHUo5BKJo(NUzZcFS04|s1-QC1fvVRxmYk*P-?LRITeNw}W@B0|Tb=p{eLiY)2^ zC)tJ#WC1^4rLS(+)u_d|o0_=l*F(N+4(a)_FMCunG4~pbMU+CwukHiAz(Q6A^WlK} zbUghD>3)oJbLyqdkve{EwUh#NqASD{F+pcsl8T76;EhWkcZgD0fa((q$yiAAHHcen z()^GK4x$Ettfz(}zz_ZyC?GnUD-TKx^RG}%VE)2Su!OemAAtp1w37>Jlr}KZ=1`-N zT?H!FvFCHJMbNne-VZh#`IvchkyrP?spZliyC*|R>4r0_#GUpR<&K8rx>CN{Yr#V zdv?@fb1Ld43N~z@8-(0Og$|tW7W9{>nQsxoBxIPAb6yEqz!d~AQXRb1n&Dcdlt#?7 zbz}iB(t%1X`xYEriayOMMqHn!dT0DRsnRLbtSs1WrfLE=YHPU^ddkvSYIPQ~w(Xf@ z!kEFW9Zsz>8E>pYB!qe84yemdwKU=LLvw)?9AX-h*!MHi?0Qw}-nqd-$bvx(SfL8R z$-~4tGJ+ zfN{<6tTiV$rF5g@X5b`9!Vsc8@{n5AT&jTf#5!sDGwR(@r(M=o%?`G*bK8 z7&Gcm{x>qv_R-jxaNo$gVPt)z6IS@vt)@?HRs0N=T!i}0l4@gjP*ynJGICK))~e1O zw~Xf8Ac#t6pW`0*C*&BMUSz!hb%R5)5$tf`DU_dH)QD;qc<9_%QvS+bWFrYJeM`y( zNZ2^#)|{SYz~8vj74|WNB!W^VxZLIj0e!w|+&yCSv%TZV?j82c@l}2!!lOTuc{(bk zO59EyXn&Alq<6VF~6b$+EVOPvX;8|^-&E&g6B+X#1vbO<}Pjnc?i(XK06>5r^ZsuEb zxA2l_y<2lwR`$_7PJ~owRFplca1BnFW16IyvDE%nmHaOrYPmS7IfFU23>g)!k|@Kc2jnr|Zp!38+=$TG_7rd1&7tRqBqZ4187;Bp zo{seOcJ#6NT2nlPSB16=)b%_%MD|Av-JOuLgNlH_%#*Iwh)n9gO=tH%JLF$2dY!gl zt$THlhDY*2&!Ppk#0Xe8GxHajJ*raV)An$P$DpC<{^vTg2aeIG|E|zpGLfIy5L%&t zekW=d3UvvQ(4Esi258rOd!?^+%V!LmkKU(yY)++FW11M@2!0O!u*G1}W@nD5VSxyH z9Z7e=?1NzLkedcbJS8--u!Pt3xMe8Tbr?EjVBOM(p4qBjPU&R4`XP9)i^U0pM2EaO z?KR!`tYL{@q4rP~HFre?qF|FpTj>zksd+=&xOUXVR6D_xDtuZaGwaz%&<&Bm6;3Pl7n062y+cfr3 zeA3BEJ57q=kJWISW{GV)${5zIxRDUd$ogOb^>J4ij0I2(fro*m)Fihk12Q6XmnlZ* zgQEw>;+97oCDXfMElDY#bUd5E&4sAXjFtSX4p)rJ1K=&UYKh`*T-5|#zMUip^vs1F zU~<4uJ8#IU$4wKz&af~>#xa8&z>(*y;&+4WE7i=KCsjn`Xh{q4`b&q8BE{bzzWXl^aA3{Q@E&?AwX?P zA5fDF*;=QX1uJx~G7=aKHff@fItlK&vF4^hD+XEqgllIeiH<}1N8Ci9RFSs(NseKm+S{q1 zTgdCT;Gx7)SMhRIGG`_PnkRBfh7B1qsx@0X74|#`;DgdLLVk0B`K}=iLDQ1D&joqj zhZ>kwS%MC;b)&$SDVgeXvU0ELXGk)*g;|u4V7Ee}?cjNW^GYX|y^~H^F^%8Hq}uCu&Gd}e^>RjYlFI3}=H(W89)8HJS_il4 zCw8jTSiLAG`6>7oz+(eIT?F}y&>Ze+0&BNj;fkC9kcv!_pcg=sj{wSH7zq;mZD51S zsGvjQ0~h;%>u<}6tBv&axeGGZ?YLP+oo4LTQ7(Y2B9KJOrO>mVz?~|on!=5r)_u*l zx=M2Jg-wqGs>2;U0H0^y=4~1kh-v8@(T&}_PBjz98cp%lYUlyIi?^Q7>bf9=DM>^t zQXwM3BH5zPO-xknU^~?M;Gu6VX4Tsh(lD;+1?*{5FTa5OLP_d4ZwM976gKLybI8?_ zZ8{1}_EU>EweHzLH4)a+wdU6Me9x6LsZPzZnC=@AW-?|_yvr>@lyiZe&}kGR@;|Xh zHzB^HM8pXEpPv;xXODp~+SY$%PSbj0o^RYMc=9N$el9Mreawz{nsU;-OJ|O;2 zA!6$?aG#W#!18EWXpmAbub{sH86nd{T^*ye)Zhz!hG!Y|63ifNJhHEMl)`pyTJ@Gq zThCeU(%K`H@aa=LKJMyxTuIbX9+Pps5ShVyc;-33sx;IOiVFlZDMdW9qp$Xy>xhu=&kaK+unDc%BzTM;4zaNekrv?`?V$2FY-r>ex2}8(|sNzn!ndRr#JUi)*2x zW>lz>#q1U^ug%EnP7LNk3U(ANwP;sbJG|{ z#Ub&HW#6bTp=O7R6h5&gTy23f5fhIP`Mie$>RrV^VHsdWkuU1uA^7!8EM)&qSiXSy zw!ZYd5(0N8TI=oM`;pIM5u8(h#6)*MhUef{u}pp4dF~+k8EtF`4QnG$bFj-34%?~z zPl15^ogZd!h#oaUwq9cWe3AX%p)u--ZooMJoTwxS0CBy7r&p!M{bCoiy-V09nLK42 zKQf-n*SAh|1w7O`Lc_fB6X;j)!Ey+T6oRnbBWw@VRh*C|)DH*pi}(ajsLu*ln{1K< z!i;i7GSE@+BiVRQCJ;9v(g?Y5h5AgR?B*aVp}xTo5V?YA-UVT+6;gkLaDp#yZy728 zs*7C!_CoHg8o_Y+eJD8%Vw+Pd0Dx*(HRPXaC^^M?@VQ5&hIYdRjzljgnHq!t=~sw5 z5nTrHm)=QUxn9sNJCnv&B0aBb$3Q_oI7?yz(Jbt_0A7~XZ?Z%d_$wIf7L2*mR1npI zU!yA|Oj;=cA@!aL5HkNn^DIu}<)}Uw9ABPwdJyy@+{RvZw@+$Q;^*~XdxiWwp&el> zT$T6tsG=vcf?%5)1utX8ZT~dNaw(;4wXk}=MHQ`%ry~<&c%@i3Vm$}bCvNmIA9aM_ zDmSlJ`-lL+L4e|KzBo)Iw;^@M%;h$a>cKt$UfL5a z@D2Qbu2&Kf(0tpr!s~Z+ice1=)*R>WA6LNS z0n5gRZPv7#;-Pe{&b#!Ei2J_!kf<+%h#JxaG)b^NyuRpOb1?ss9V*?ZaJN3b&QsML zkMyZP_=_>)4%zd2X1!`kE9csY5>r5j%^V8jNTj}0%59TR!A61QNF`yorx>xMfe%U6 z>gF5Y80tj^KZUnD?2gEsAgH2IGmsSztrFZm@Rns3;Q*If2eHc&w2vn7Vzppk$@enX z-D%kmmEMExR+YN?9Kxn!RN-+@!+M9~bFFl-U}&8KnH*SFL$>9JM@V^awR&u|3|hk- zoJC)X8kuwwY;iG9c#D4N!oJ}6qhald`tMw#s~vyD)Ee%Vgcu)3#Xx48KqFkY4;MhQ z32H~c8TMmw1I;ey)Op^b(}%dKrsj!++bx%$Qj8KjZh)CYD)2Ch4TsP&BtHtJBGcJF3EUM3;0b2xK)d z0yBQ+X#kA?QW(g_BVgMdriy0pSrsB3N9h|t*dey*J;m*@^O)8GVDw-=jT84aCl8AE zS2}f{${7O_%$N}nrg^=!OC*^uN?q3=HHto#XNryD!$d0kU z6n)o*N~aUlgTJMHid?E~BQ(O7dott89CiJi=WAW|T52oBJ8mn@UCN^-4{agU zO2_U%*Fk53M^{p4yAy0sD1QtpR&yK`k*0n+!EA`NftD2%-b&5Kf|(J=U5+~9)@un3 ztV)ik)mh3Q39Dd1ACMa=$?I1oi7_ z+*ky6Q2A5JLQqesQ$g?-BhCcbg9U<7CBqZ?>@lrK&k_6J8c!PVApCdtCwfxQM9##H zS+QR4hCR-%XFU5s*b7k%g4F^aR>+=KweN&9eL$~u+^VmYCK=8-lUNsAC*+&Z=AB08 zYTaarTXA;Ju&4vFNMj~-NUw)2C7CLnbo2S^|i%&!KuzFkmf? zAs(%uPYq()22~FS%fCAV8r<1-=Wye9sBWJ_m^Qpbw(u3;h85b-S5heTVXaPs7Nfdf zELY!QpQl!G{G-;w@C&PzJ8q_|B|5#hhTFF zMtWk1!CGW>G@Qa9-lR&4dIx`m_5n99BIx>W$9WS&gcI;swQF5UZ?eG#335OhE6Kz| zcC3&RPCE&fbIe1mY9#NJ>7md)c;kwc)YJOMqDSP1+mi&zusJXb)@-+{i*2GPfD3=K z8pV>Al7$eb#{6K0Mh3HhM-ql_4R}qe7*w^99y>Fur1r-U88u9Up@$t%d^W>O*q`3e zahL&PCl<{lx7Ui!9)Pqu-1gV9aR+Fwzx!t=IBAhHnz5)C$}9KvUma^4Xs~oJ4En87 zk7Q1R9ngm->3_dBkUQT*ZO?Ip$_Y*~*zP^q>>CymWUQEX(Z#CIW}Ep=GUYb9EgAY(3?1TIsi<3Bt|K7sl#N0I zdls%+$X_eBu}7diVMY~Az?AZ3hJ$edoBkf`=|XaLAxmbt({JiZSf_+RPMW= zrU`B~)2Jq+u_AigjwE-%Oz(!jojcsK%@Y?^*E15Kr~N3SDL)ge`n5Jfz|(fr zJD}zkJzz4glXCHO6)3`P-Hi8DuKOdwttx(3Mzd00Jddo;fj6qG(*p#`hc!AhyC5>r zBO}pwn?+Eea9%Iun(c>i1CsivP=%Gf>jVcQ%p3j%p85YB5rZVKYCsHYF?k0e)L^e+ zu_vTqeD2IM=tT5X*yw_~culXht!n9`%a?ka@R}Vrk8J+z=G>=8y;)_DpC8<%r3wt zwe&eNc0SAL#_KxNR1{ClodioGbbB3g@0f5dy}iW@d*dyl9Lj-Wu|);%Z56AYm;rsD z5hw^BMYQcw+K0qv_l)S<+;L?zZ|p!aC$#?-c-$3jFQ9s~wX2K=;d+CgSxp>t0AM?TYmzKF;xsPl9&s#BSZuod z2npAsF`P(TSXqL6bT0e9Xx(!O_?NqBQFwV$`{F`ZCn`d#mHm-u4i z?`>xuj(?Z=>DKhCwz*66`tf{DGN$VgOt*yx`xH?YquWb?jY!g7k-(JLu3%?BBq7*? zogN;GxhUOQE*Jd0)CRklZ&z#XT+?}|h&HF{G?Hr$`GwlmCNa11yJp$})Y_1HyC4_^ zZlnN$Q?{}wSpXw*!B~Yen}y~k)4soiVipF@9At;Qu?Mx7kFD5T(_HhuNRnL_L0BJ* zQ`b+`POkh$8=^5=I4zN0v{lhT{x z$2z6H_y-22{QOsP~QHUpv zV?l>i(m>%bt-vR0h0$vN3;wCo?+MKWmynO7$=6o9(X#eMi4U$-U0*!+LodW~v(@s$ zpqYiq&_B~d38f5AR83YTPN)(_94YkGb`2!W4#;@AzT}uV`M?03iOH!TqOu`Z^B?<} z@h~X`K3~Tole-VREn9XJnN-)HWtaIC^!zg2StpaIP>!$X4QMtYZ98@sN(^rQJ-*Ff z?#fc4!9>vkgU@n9r%-#<@p=>Z!X@USm;PJr<7Dy0g(gHhBRPcxx z=i5CxA%`H)Nic2?z>#wNz#-D|=kMg+)9`1JUDN$REMMTyKc~Z1ylc2J4c+VYg3Z5K z2dWRyEF!WiWN-}I;k{;9e}iptMPJ3zA@Y46{H8q6M4{DBdR`AnoSc9|r0ek8)i;;q z58e)hISz>N0tS0?#B2e_5Av6Wx&<}OT2~q7IU3USK_RKVk6)=5JOYRKX4~*VKJ|Y( zUF4VS0jS1pZaLJ!pYP>{Y(p@fsty`ZJj2W^@og6cgnnZFVIm@2#_J+&?lFTMeWli` z9(zmwlexoVwD`csg<}z3$oy-Chs|qGi9psMuRqO^E&kHnd`7IZHbv zPKS$G_D+v3Q+nsiJZk>CjPsgvLzMK_vN~8XTz$@MHDE|iF@2gwJ}L>N#xDA_){;R+09MTc zD$>t_KQlOS>?02np;e@T;yoOqH{I3q2#yzlyu)aRL{^JL5>5aBV2h;{f)d1!0Feu#E2((5Jv45+Rz}d}Uy< z7i!mMU4%4B@IrjJeK{vpVV!ZHgn|OvanRLS(YKK!2oT;MeT*VbuZ+?+e&tq987&4gMF{+i)$%z&;S; zI;0V6on5DAO!L0sRSAas(tKC!mO+aZKtd?&Q(ckF+36>V82f4Mx)VKe8q)>u8=)2Bm2*0mk!fp)S}S;6#6Gsc($?5u`=3)=p2Nm-U-MGu}1}|t=C-VggHDY z)Gjq3#2s=SZNz@%hanc%>1=ShlKy-j*PoC8UL@=eR%EWkTTrVzQ#H6tx(UYn0#oaq zS7C0qb!29hT9QsHW{s*nP@}VGGm|!B?@pV43LKIf)0_Rue5nY?U?97;>z91rW)YN; zMreCa!Ej?!fPler9yCEBgh7IunjHNaAUX;u>3M8Vieyf)1iYwFse>=FLEk(xl3{T& zK?x}H#ZQH!5Imu5!bvt43sGlr3si@p-eukp$7$XPx3zGh(Fb_J#7lLCN}K%R;Qig> zXMLn0zKUOop|z(LmR9Oa`)|CcvjSdOpV^7Or=x#QD6&+H(RQBBoT}}*yXB~em-sxb z?_!xFW~H3tKkZ_JplX}WtTDVi?9D9!_+7Qr3QV1e2Q~(#ngqVc(;T`gYKnp}BF&g| z%VL4MT-;h;?iJgm9-+ZCYy;Qu7DqG}^?dbyB{?*f(@za^F;t%`Zye)v_^~no7t#2Y zp-mXz-mF_PpW10!u2$HDZ@E2K@IMNGQ}x5sn>j2FPLO)5Ja}Wjl~Q|5Dbz5f95er$ zc$88>JeQfczY)UR;(DLdo_BM1Tal2bow*rqTCgCTz4) zDuUc&lF54qRf;}zgqTZT6^Nu;54n8om_m0C+s$~ecJI#cydthLiI9F^gVgIhT>TI; zjSqQ(3y48RFa>K8bklH=Z4+FZsiArvp%60OWMyNZ|EjA%~Z83Ea`L}+R$ictZ7%6=;@&Nkc_A%S)#BoVcboph; zYJ;jG)ZMPDdohN!1dEJ_Ah>=#pU24}IuSiR%$^mn$^EZp1HK9Iknf4qQc8~qIVz(D zafwb zi!gzJ0>G^Om5?KZh{6B=iy5+*U`Wp10*Y5+`apSa3>T|Q$?2?se`(HQx$5d)b~cW3 zSC<29AZW5y`-!P!La-pQ3js>(;eA{^o{k0NN!xVe?<+^oa&W+^JFv$d z+%2F@%g>{3!UrsKr-|;PMx;qQSLO76S2^R-yNUMQV1!H92rIxNzTTBqmP@P#OE84& zIK*}^Z-$##6IHth5LlBIO#FLNd@8xX;NvoEB*z}M=xyl*@BzC3^|6w%XJmIp*Mik) zjA0Q)W4OY9&DH!kC$g&V2aZ*;jja|C4{a?kBtULc_?|JXVA3wz|Cg~h4QTS**1u&4 zLKupegeh94M3Ml8fC$xg%cKwjGKDEo83KZULaa0GVG2VSB!NHzv;~9+0U{s-aG=bD zL8~BwQY)h!Y^%`rsJlI9zw5!;-E+?W!~3P$QW3(Fd);fT>-t^&peuK-geM4O*DNJM z1B2bVpBI>1ka_8RZIt6kGK8$2=xGJ8bS6+ZAC|KH;Rd)lkU(?htW-fsw|UMXg5*}) z`Dlb32h-+xW(&*!!DXz%M`UBjLk9xa>`>K^C72P8YDBeXlpM>b1ZOz#a6Ex~S{RNR z!sNW9B}D?nFI}OYw$+TasI-kJoACK8@9MqE!h*I@W^uT|>PRx}VN{zva@8^b9_Wqb z)@t;`|qD@WUje7T()?Ei72!!T5g!H_4;iPd7cqDo+s{yP)rXx zl34_VBdSaaUMSf=lQSrKFwDG(4GSH@1hJMOQpeaNQ`|MxFx|y<9hMvz=Zy(<+d47( zz+qJfcNZ}5ji}t1!z%iJ;=3(U$y3%W0rGZYx(Y~VJ$Prhbq4bjS53VUIGO#KrFmeB4i{39KMkX-7wh94L(u3TJGciBe%BgeCi zq|nc-`5mmi6^AQ4W!?;MZ3rv8XfGf5({vQJu^MmUq4qLhhjk|WOEJG5&$*Q%{79QFe!0+{r;UwmMZ(;(=F{ICX ziMH_b2w94RCrmaC?#tw4@B)3^UcbNw2Sq760i?=j9b1EAyt=n%qq-zkju7iPnB#5`n3$iy9?y;-SYAg-N0(xiLoj%LQDqbMUcW*!VZNXuEV6a-Kou9~thSz)C z>a*YDg4HftRCql~(e&rWNMOarPOcOR^uHoe@^zzIc7}Ghw4ig7)O$863WfrBZ0V$k2dLz#%?WEt=OKy zp90I!sOb2I>-7;ofATUg8jMSW%Vl4+HUKrRC zO63krWq4L>rRH&?L&~LZOp;Io|BN&jhp1QcXa@tY;7%~goI|Q?$28G_RZWy7Nmy1e zpN*HtZh7Ey*gnd=v@p!8rXCn6<9gi18DM=!wRFv?*q}QDHe5V50=F&egXZu7$z$~9 zvztzdG4X}N1n`!GVj=CS!QTXd9V7}kKax_Arx2nyXMpLa!pzk+0A;>g>1rm1A4-U4 z(ecSl%7Q8wa47lkdhHo>i=gy5HE(%}Zk&kMr3FO$tGco5q+0ctUWnJ)KUG&4#dsgp z9G1?0u!_}A>H4YM73GKB@>7ndTXe5C2iFef$2#agviW-MU0h`+qyCkz;!$a#vkPZT z{=Pi!zz2VH#LT@n=U}!YE}XzMdFJ7sMay-e-nWpZbP2n)HQH*Svj9&oc6dhhaXIe) z_Pw=r3H+9Em`ndTv{k&9l#iBC$>jZw4Mvi1*v@2ldD=y+F zKQa^GfBo~q31@FibS7v7q1avpr*hMX9jtmG2**weCQk75q+&)8@E72MsD$|d-)PxM zYBq+lR?`l_9jf}LHVf@q2EW|9;LmU2fAqe8{Vkip`jonxj4t_dc@yi*qS*+q=N2ZYj*V)LQFRd zi0{J6y%nOVpRfZ?!X0Vzn)T;P4WMlvEm7zWvg3buwKC2eoDw^b&$2o5NQ%A&-rRSU zl_iB}$ZOZqw2+oVZe+|`QKKS1lfv+v_08Eb&~`!g_L8QjY&hRPWwCsMqn(64lT3t; zsG_jL^a2g@W2!e*qt}exQ-SZq-D7K*att&0p}&F_0p>H%+cW}*`-DoC5V2K0EOHZf~ zU6U|3>SFj`4a(q4ZK~v|qA?3*ONQ3T-f*puAn_XurKR~r{9rY!E?l?z0l>Nhr&E0^ z9)0ESz$(}K3iF82@?gqFaMtMAXG6qjx*eNSs4JR@wS2MJqFvtwY0ZC=a6=vNEjEua{y8vbffOs ze$hO3b|kc^KM;HH4W`Zz#L^V$fwDUP*jRH@lex6U@+gxmGNHtyW@3_>f8?JWZNeFF zv@@Z_g;SKtxtJ_47h8+cGIRz!#|X3z(-^CdxzOEy%n^GMgRHwpy$iXaBy+`2Y$rE? z&roE2P;7fc12N6aO5F;k;}DNP`{;;KmsbsY&Q{m+kkvp8tZ>n#oGfe)AQUJm?I*=D z!E<5spU-JX{*SZ+7LV0ee3BLBP;O83z;!R#9x?*qM_7zyqd-8?!vvELIY~zyMDLz$ zyZh(SrPU=YSLLH5ch$i0waF!1lkepthvyvZ2S%&sXP?}mNah!pR+YoMPc{d!=a)wp zIm+Q{cI1RAd`b$T?Y5r(cUV}o>HOj{?D$HRmKGL*b{Iu#2bLlC%rvrDa48MgA;h+7PJI@Q&*&eTueid&A*C9PMNbF;Z?@ zagfoP%>8aWIl(xMBia?ffM(Y;llHpI`a{16v`BAPU-&R@wq_*6P^!NznTvsmm>-6e z#jrjl_`ZlRWa7ov^JqAql&>>*V_YOwc*wTOL)_Cig*kzGk35zkf;w}`dy2HetqUOk zHvi0p2e-BRZS%1lr_8)YWy*EU?2-`i3?+G9KV@2&dDaYiAM`Q1m9#HS!(FLUjpZG6 z-2RA((&BGz@)Lv^>`rgF4}-HD_aL->YhTd*2mNJ(j+)jEM=CySV@{FoJ`lw&SQDV0 zEPTA6+ElqJf|S_Qdih#r`NA4IWR9flK=rwsu#SNEjhUQM_jT7UgPccA>FSNYjoTlg zNAq^guV%Im@>~TR)0k`&h1rxYyJmwC^}Vi*X+5aV)c)Rot(uWw@TJ~?pGC8y5)JTR zt}>aXu;zY-Z_DzIXl8i3{de#Hy-i`_N`{~Slls8#>wkg_D?4w3A?+s#RZq{`e)4L= zX+&YgDVdm^>d8GK^uAQxd_yV{_DdevfWF^%UcH_9@0z12FSX(@+sRQvIK3 zkUM{mMjRsW$MX6O+Q1k0oZl*?_(>_gQSMC{30HU_89W#c`0iyeuka1k*jh<^@ERg} zn#~5QH7^)1NeU~|UaQRjb!84gj~gFx5TmaU*L}v*`*%TqR}9`3;vrS{7zh9cKFB;8 zW@CjqgXb`YKg2R}^NpI|u0DW#$!8m}XND^07O*y~ZbJK~_teaAM?Q@s{%aB6|2fP=I6_O?D zt?@gOIeZ*54iKHBtGgj;rq3P1XM`HQ8}~rN^CmJuXk5(Lg9VTPs+I>@2~D8c&!G)< zP;Xi8|IfLB$Be#yTjgr{zf100pHHw-3X=JK*t1yM&7npFIDV%8_}7-@);WwEmYl4S z()JL9;db7@5b|E&wf|CW&+F^(cR++;a}Ed#fkb1c>;QQ(x&vS2A`Pte(dl^uoOi`4z|qpKyc6&zTp|x}oz|JXoxkB@39M+L7eoJr`HKSg8?< z5%I*=trmt!`5f5KV>0tW2P@E;GYi~qR0LT<>JQb#za$q}btaESt?GGb?`}_exOUpU z{8;=Ghhx-dF*cQIikJ#JQuQYtwo3+jxTqjZ&k|k2)}cSx!xt6 z8DYZJ5SkJM28XOa!;FNr=2GoMiO*_UiJeV-R*W!p)&EDXZ=m}}xVozaC@G)~dH`&3 zE3&h53?>u)-@psU5?Cii`FxT>xSlEeF+VicgA&!zo1Muox~_r5-E@^pa)ovwF0@Oe z?_JsblUt+sNz&FOO_fmuANlQj6j8Vy|Cjm6HDw^!$ zc-A*`#I7g8wN*_uH%S~_GuR6eKC2}VB)?13Jq%GNZ(bZCH}!3sSBU0;er8B71_HI% zA1orXWDEaL3c-An$TN&}3Af4o5GV1r5yZoEnUcXMfJt&$rc4jw1$0a5AHPf?&S}8A zLWFR{F`FE5I{=%3URiS$WXmhi;I19C9rS7tpPQHpU*G*(`U{9N@-bP|vCv`MdvUr{ zKay*{GYtWW=}pfO)zk2GWvR%@YPi8JxQsuYQTw5>c5 zyIv4p>3#Y`io`$bvP*l_LS?8!yX)|{Ape`MA6{{Cu33b#`@-&nXG0jgO=*l_&>5F6 zitX^LV<#J0{o+;%@?GrTtTjz3Jx9xg5@_~v@-cmfChK6Gdo#9%YJGMeXGj$pCWAUO z|9fHdRURnUbk~`M)NC6g=JgDghPdonzX%9)K&{RaqLTZj zn%iAizOck@*B4;xEGP2 zPVtEZ^54<*(!E_gx5kY3US+ z(aK4px0S;k_Eh1KPrd6yX~Fo3(H&>Qtu~xE>5EgfMr#=(flPrcmzb8XmZBq!;7FNNyhe8;25ae6 zRk!7YIZSH|AN?4gjA_cCm}#;hoppvB;J{kQ;%*$lH z|2L?x6dvf7k=6jvFy2L6SIux*l;*B|2($8Tgi)FOcx?-Lg@-z+VH@2E=A^&h6#nXJ zLk9YReHaJ#LeICwR1EG-ZJ1FqxderEV}}8^Mm$#LJ(YRxzRbWwhn!}Bpjn5(swCRifXbVif2(}+9vJRA1ysul9?E4X znBCf-wGGDWQ~(GPXhWsRzYH)cp<{*r%!1gNuQzKc(#C?UH!2A#sdEC`Hc<0zzUdeAhyKhob}Pt#AZMhf=~(+=K_n8z^IS9gJ4G2uPjBlM2!TEKQS` zM%mrGh>?&Wi_yydoV-C`O3XKZ45JpPL6I{_Ohg;ec@Ue3=>k2aQR^;ME!U|>hVPsE zJb@^>Wo4tw0>nq{D8|**jA^sW)@^WjH@KqWQE-HU`d=#|VF6=12$+OJ2CW<@Y@gax zw$cbb><)GN?od*R7n%QH%6p`QluZk=--Ubay}#>=eB%P4-AR{@Dxcz9D$R91x{BEK zdFPZ)$?5ew`*Mz4#|4}=w+cJ4_VL%+9?Mvi{ZHj1*}_LoSeLX2emL={3A~5GjLIBf zY3P3vDr@T?jH6bHYUR!#DKZC>9uF=Hs&c7z$oH^x*+fV|#7=)furUV4b>@(MXO7FF z*a++kTNy`f&MAO(-OKORsKOG zu0O!**NMDjl)-h#VW091eQvx{gPrI*KGdHjaQ*rAhP!FFGAB{?n@i+q|7y@)%dz)z z#)gyp-gCdKdVVfH1(;#~L0*)GJlfL__Oylb4P#gqBft-$5Ljcn&B4x(O)PGwJf6Pm zoQ(O|65G(@4~7Yrrfay+{gA4Ol)Pxq97v5g<~QH)U?em-qm6ME*H>im&1{Z!lO*DEOD=@#&`@Ao9C97zE;nAO;bxr*NUx}dL=wYBl z)-3xa9sMC<&S8CIq+0ybgI-CooegYjCjl+BJmHOhA@SEWASD$+0*6(N)rqy5J!KSp zn?2cv>zW^cT$l}J$J+x>>6oM7y$No~nhc)q_H1Zu>*f|Aco61Q%ZZ!onET_Z%>aGs zX5XOsw!Em&YHh9^Po<$cxNhEEEihDe%Klhavh6c-*IFr?W>IEh)8>oPTd!{OM!C

=l+5 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 ME8X|t(3+|L02((0(f|Me literal 0 HcmV?d00001 diff --git a/img/email_128x128.png b/img/email_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..67b61dec5cb10f233ae3ee91825c5b8918a120dc GIT binary patch literal 4718 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 WC|GtNCaY-x literal 0 HcmV?d00001 diff --git a/img/glyphicons-halflings-white.png b/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf6484a29d8da269f9bc874b25493a45fae3bae GIT binary patch literal 8777 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 B7G?kd literal 0 HcmV?d00001 diff --git a/img/glyphicons-halflings.png b/img/glyphicons-halflings.png new file mode 100644 index 0000000000000000000000000000000000000000..a9969993201f9cee63cf9f49217646347297b643 GIT binary patch literal 12799 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 literal 0 HcmV?d00001 diff --git a/img/icon_irc_400x400.png b/img/icon_irc_400x400.png new file mode 100644 index 0000000000000000000000000000000000000000..58154ec3e3318ba820978af8252b1f46d67ae96c GIT binary patch literal 9756 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 Hd&0i}OtaT1 literal 0 HcmV?d00001 diff --git a/img/logoJIRAPNG.png b/img/logoJIRAPNG.png new file mode 100644 index 0000000000000000000000000000000000000000..968ae8886b1f07ad490f9505167e4e746bb9c01b GIT binary patch literal 10274 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 literal 0 HcmV?d00001 diff --git a/index.md b/index.md new file mode 100644 index 00000000..eaf7aaf7 --- /dev/null +++ b/index.md @@ -0,0 +1,36 @@ +--- +layout: site +title: Home +--- +Cloud Haskell: Erlang-style concurrent and distributed programming in Haskell. +The Cloud Haskell Platform consists of a +[generic network transport API](https://github.com/haskell-distibuted/distributed-process/network-transport), +libraries for sending [static closures](https://github.com/haskell-distibuted/distributed-process/distributed-process-static) to remote nodes, a rich [API for distributed programming](https://github.com/haskell-distibuted/distributed-process/distributed-process) and a +set of [Platform Libraries](https://github.com/haskell-distibuted/distributed-process-platform), +modelled after Erlang's [Open Telecom Platform](http://www.erlang.org/doc/). + +Generic network transport backends have been developed for +[TCP](https://github.com/haskell-distibuted/distributed-process/network-transport-tcp) and +[in-memory](https://github.com/haskell-distibuted/distributed-process/network-transport-inmemory) +messaging, and several other implementations are available including a transport for +[Windows Azure](https://github.com/haskell-distibuted/distributed-process/network-transport-azure). The [Overview](https://github.com/haskell-distibuted/distributed-process/wiki/Overiview) provides links to a number of resources for learning about the conceptual underpinnings of Cloud Haskell, and some [examples](https://github.com/haskell-distibuted/distributed-process/distributed-process-demos). + +Documentation is available on this site for HEAD, or +[hackage](http://hackage.haskell.org/package/distributed-process) for the current and preceding versions of +each library. + +### Recent Activity + +

+ +
+ + diff --git a/js/bootstrap.js b/js/bootstrap.js new file mode 100644 index 00000000..6c15a583 --- /dev/null +++ b/js/bootstrap.js @@ -0,0 +1,2159 @@ +/* =================================================== + * bootstrap-transition.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $(function () { + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-alert.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT + * ================= */ + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + /* ALERT DATA-API + * ============== */ + + $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}(window.jQuery);/* ============================================================ + * bootstrap-button.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT + * ================== */ + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + /* BUTTON DATA-API + * =============== */ + + $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-carousel.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.options = options + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , to: function (pos) { + var $active = this.$element.find('.item.active') + , children = $active.parent().children() + , activePos = children.index($active) + , that = this + + if (pos > (children.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activePos == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle() + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + e = $.Event('slide', { + relatedTarget: $next[0] + }) + + if ($next.hasClass('active')) return + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT + * ==================== */ + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + /* CAROUSEL DATA-API + * ================= */ + + $(document).on('click.carousel.data-api', '[data-slide]', function (e) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = $.extend({}, $target.data(), $this.data()) + $target.carousel(options) + e.preventDefault() + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-collapse.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSE PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = typeof option == 'object' && option + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT + * ==================== */ + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + /* COLLAPSE DATA-API + * ================= */ + + $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-dropdown.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + $parent.toggleClass('open') + } + + $this.focus() + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) return $this.click() + + $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + $(toggle).each(function () { + getParent($(this)).removeClass('open') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* DROPDOWN NO CONFLICT + * ==================== */ + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(document) + .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) + .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('touchstart.dropdown.data-api', '.dropdown-menu', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery);/* ========================================================= + * bootstrap-modal.js v2.2.2 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : + that.$element.focus().trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function (that) { + this.$element + .hide() + .trigger('hidden') + + this.backdrop() + } + + , removeBackdrop: function () { + this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(' \ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async-AsyncSTM.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async-AsyncSTM.html new file mode 100644 index 00000000..c9e95995 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async-AsyncSTM.html @@ -0,0 +1,90 @@ +Control.Distributed.Process.Platform.Async.AsyncSTM

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Portabilitynon-portable (requires concurrency)
Stabilityexperimental
MaintainerTim Watson <watson.timothy@gmail.com>
Safe HaskellNone

Control.Distributed.Process.Platform.Async.AsyncSTM

Description

This module provides a set of operations for spawning Process operations + and waiting for their results. It is a thin layer over the basic + concurrency operations provided by Control.Distributed.Process. +

The difference between AsyncSTM and + AsyncChan is that handles of the + former (i.e., returned by this module) can be used by processes other + than the caller of async, but are not Serializable. +

As with AsyncChan, workers can be + started on a local or remote node. +

Portions of this file are derived from the Control.Concurrent.Async + module, written by Simon Marlow. +

Synopsis

Exported types +

type AsyncRef = ProcessId

A reference to an asynchronous action +

data AsyncTask a

A task to be performed asynchronously. +

Constructors

AsyncTask 

Fields

asyncTask :: Process a

the task to be performed +

AsyncRemoteTask 

Fields

asyncTaskDict :: Static (SerializableDict a)

the serializable dict required to spawn a remote process +

asyncTaskNode :: NodeId

the node on which to spawn the asynchronous task +

asyncTaskProc :: Closure (Process a)

the task to be performed, wrapped in a closure environment +

data AsyncSTM a

An handle for an asynchronous action spawned by async. + Asynchronous operations are run in a separate process, and + operations are provided for waiting for asynchronous actions to + complete and obtaining their results (see e.g. wait). +

Handles of this type cannot cross remote boundaries, nor are they + Serializable. +

Instances

Eq (AsyncSTM a) 

data AsyncResult a

Represents the result of an asynchronous action, which can be in one of + several states at any given time. +

Constructors

AsyncDone a

a completed action and its result +

AsyncFailed DiedReason

a failed action and the failure reason +

AsyncLinkFailed DiedReason

a link failure and the reason +

AsyncCancelled

a cancelled action +

AsyncPending

a pending action (that is still running) +

Instances

Typeable1 AsyncResult 
Eq a => Eq (AsyncResult a) 
Show a => Show (AsyncResult a) 
Binary a_1627460136 => Binary (AsyncResult a_1627460136) 

data Async a

An opaque handle that refers to an asynchronous operation. +

Spawning asynchronous operations +

async :: Serializable a => AsyncTask a -> Process (AsyncSTM a)

Spawns an asynchronous action in a new process. +

asyncLinked :: Serializable a => AsyncTask a -> Process (AsyncSTM a)

This is a useful variant of async that ensures an AsyncChan is + never left running unintentionally. We ensure that if the caller's process + exits, that the worker is killed. Because an AsyncChan can only be used + by the initial caller's process, if that process dies then the result + (if any) is discarded. +

newAsync :: Serializable a => (AsyncTask a -> Process (AsyncSTM a)) -> AsyncTask a -> Process (Async a)

Create a new AsyncSTM and wrap it in an Async record. +

Used by Async. +

Cancelling asynchronous operations +

cancel :: AsyncSTM a -> Process ()

Cancel an asynchronous operation. +

See Control.Distributed.Process.Platform.Async. +

cancelWait :: Serializable a => AsyncSTM a -> Process (AsyncResult a)

Cancel an asynchronous operation and wait for the cancellation to complete. +

See Control.Distributed.Process.Platform.Async. +

cancelWith :: Serializable b => b -> AsyncSTM a -> Process ()

Cancel an asynchronous operation immediately. +

See Control.Distributed.Process.Platform.Async. +

cancelKill :: String -> AsyncSTM a -> Process ()

Like cancelWith but sends a kill instruction instead of an exit. +

See Async. +

Querying for results +

poll :: Serializable a => AsyncSTM a -> Process (AsyncResult a)

Check whether an AsyncSTM has completed yet. +

See Control.Distributed.Process.Platform.Async. +

check :: Serializable a => AsyncSTM a -> Process (Maybe (AsyncResult a))

Like poll but returns Nothing if (poll hAsync) == AsyncPending. +

See Control.Distributed.Process.Platform.Async. +

wait :: AsyncSTM a -> Process (AsyncResult a)

Wait for an asynchronous action to complete, and return its + value. The result (which can include failure and/or cancellation) is + encoded by the AsyncResult type. +

wait = liftIO . atomically . waitSTM

See Control.Distributed.Process.Platform.Async. +

waitAny :: Serializable a => [AsyncSTM a] -> Process (AsyncSTM a, AsyncResult a)

Wait for any of the supplied AsyncSTMs to complete. If multiple + Asyncs complete, then the value returned corresponds to the first + completed Async in the list. +

NB: Unlike AsyncChan, AsyncSTM does not discard its AsyncResult once + read, therefore the semantics of this function are different to the + former. Specifically, if asyncs = [a1, a2, a3] and (AsyncDone _) = a1 + then the remaining a2, a3 will never be returned by waitAny. +

Waiting with timeouts +

waitAnyTimeout :: Serializable a => TimeInterval -> [AsyncSTM a] -> Process (Maybe (AsyncResult a))

Like waitAny but times out after the specified delay. +

waitTimeout :: Serializable a => TimeInterval -> AsyncSTM a -> Process (Maybe (AsyncResult a))

Wait for an asynchronous operation to complete or timeout. +

See Control.Distributed.Process.Platform.Async. +

waitCheckTimeout :: Serializable a => TimeInterval -> AsyncSTM a -> Process (AsyncResult a)

Wait for an asynchronous operation to complete or timeout. +

See Control.Distributed.Process.Platform.Async. +

STM versions +

pollSTM :: AsyncSTM a -> STM (Maybe (AsyncResult a))

A version of poll that can be used inside an STM transaction. +

waitTimeoutSTM :: Serializable a => TimeInterval -> AsyncSTM a -> Process (Maybe (AsyncResult a))

As waitTimeout but uses STM directly, which might be more efficient. +

waitAnyCancel :: Serializable a => [AsyncSTM a] -> Process (AsyncSTM a, AsyncResult a)

Like waitAny, but also cancels the other asynchronous + operations as soon as one has completed. +

waitEither :: AsyncSTM a -> AsyncSTM b -> Process (Either (AsyncResult a) (AsyncResult b))

Wait for the first of two AsyncSTMs to finish. +

waitEither_ :: AsyncSTM a -> AsyncSTM b -> Process ()

Like waitEither, but the result is ignored. +

waitBoth :: AsyncSTM a -> AsyncSTM b -> Process (AsyncResult a, AsyncResult b)

Waits for both AsyncSTMs to finish. +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html new file mode 100644 index 00000000..74b842c2 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html @@ -0,0 +1,118 @@ +Control.Distributed.Process.Platform.Async

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Portabilitynon-portable (requires concurrency)
Stabilityexperimental
MaintainerTim Watson <watson.timothy@gmail.com>
Safe HaskellNone

Control.Distributed.Process.Platform.Async

Description

The async APIs provided by distributed-process-platform provide means + for spawning asynchronous operations, waiting for their results, cancelling + them and various other utilities. The two primary implementation are + AsyncChan which provides a handle which is scoped to the calling process, + and AsyncSTM, whose async mechanism can be used by (i.e., shared across) + multiple local processes. +

Both abstractions can run asynchronous operations on remote nodes. The STM + based implementation provides a slightly richer API. The API defined in + this module only supports a subset of operations on async handles, + and (specifically) does not support mixing handles initialised via + different implementations. +

There is an implicit contract for async workers; Workers must exit + normally (i.e., should not call the exit, die or terminate + Cloud Haskell primitives), otherwise the AsyncResult will end up being + AsyncFailed DiedException instead of containing the result. +

See Control.Distributed.Process.Platform.Async.AsyncSTM, + Control.Distributed.Process.Platform.Async.AsyncChan. +

See Control.Distributed.Platform.Task for a high level layer built + on these capabilities. +

Exported Types +

data Async a

An opaque handle that refers to an asynchronous operation. +

type AsyncRef = ProcessId

A reference to an asynchronous action +

data AsyncTask a

A task to be performed asynchronously. +

Constructors

AsyncTask 

Fields

asyncTask :: Process a

the task to be performed +

AsyncRemoteTask 

Fields

asyncTaskDict :: Static (SerializableDict a)

the serializable dict required to spawn a remote process +

asyncTaskNode :: NodeId

the node on which to spawn the asynchronous task +

asyncTaskProc :: Closure (Process a)

the task to be performed, wrapped in a closure environment +

data AsyncResult a

Represents the result of an asynchronous action, which can be in one of + several states at any given time. +

Constructors

AsyncDone a

a completed action and its result +

AsyncFailed DiedReason

a failed action and the failure reason +

AsyncLinkFailed DiedReason

a link failure and the reason +

AsyncCancelled

a cancelled action +

AsyncPending

a pending action (that is still running) +

Instances

Typeable1 AsyncResult 
Eq a => Eq (AsyncResult a) 
Show a => Show (AsyncResult a) 
Binary a_1627460136 => Binary (AsyncResult a_1627460136) 

Spawning asynchronous operations +

async :: Serializable a => Process a -> Process (Async a)

Spawn an AsyncTask and return the Async handle to it. + See asyncSTM. +

asyncLinked :: Serializable a => Process a -> Process (Async a)

Spawn an AsyncTask (linked to the calling process) and + return the Async handle to it. + See asyncSTM. +

asyncSTM :: Serializable a => AsyncTask a -> Process (Async a)

Spawn an AsyncTask and return the Async handle to it. + Uses the STM implementation, whose handles can be read by other + processes, though they're not Serializable. +

See AsyncSTM. +

asyncLinkedSTM :: Serializable a => AsyncTask a -> Process (Async a)

Spawn an AsyncTask (linked to the calling process) and return the + Async handle to it. Uses the STM based implementation, whose handles + can be read by other processes, though they're not Serializable. +

See AsyncSTM. +

asyncChan :: Serializable a => AsyncTask a -> Process (Async a)

Spawn an AsyncTask and return the Async handle to it. + Uses a channel based implementation, whose handles can only be read once, + and only by the calling process. +

See AsyncChan. +

asyncLinkedChan :: Serializable a => AsyncTask a -> Process (Async a)

Linked version of asyncChan. +

See AsyncChan. +

task :: Process a -> AsyncTask a

Wraps a regular Process a as an AsyncTask. +

remoteTask :: Static (SerializableDict a) -> NodeId -> Closure (Process a) -> AsyncTask a

Wraps the components required and builds a remote AsyncTask. +

Cancelling asynchronous operations +

cancel :: Async a -> Process ()

Cancel an asynchronous operation. Cancellation is asynchronous in nature. + To wait for cancellation to complete, use cancelWait instead. The notes + about the asynchronous nature of cancelWait apply here also. +

See Process +

cancelWait :: Serializable a => Async a -> Process (AsyncResult a)

Cancel an asynchronous operation and wait for the cancellation to complete. + Because of the asynchronous nature of message passing, the instruction to + cancel will race with the asynchronous worker, so it is entirely possible + that the AsyncResult returned will not necessarily be AsyncCancelled. For + example, the worker may complete its task after this function is called, but + before the cancellation instruction is acted upon. +

If you wish to stop an asychronous operation immediately (with caveats) + then consider using cancelWith or cancelKill instead. +

cancelWith :: Serializable b => b -> Async a -> Process ()

Cancel an asynchronous operation immediately. + This operation is performed by sending an exit signal to the asynchronous + worker, which leads to the following semantics: +

  1. If the worker already completed, this function has no effect. +
  2. The worker might complete after this call, but before the signal arrives. +
  3. The worker might ignore the exit signal using catchExit. +

In case of (3), this function has no effect. You should use cancel + if you need to guarantee that the asynchronous task is unable to ignore + the cancellation instruction. +

You should also consider that when sending exit signals to a process, the + definition of immediately is somewhat vague and a scheduler might take + time to handle the request, which can lead to situations similar to (1) as + listed above, if the scheduler to which the calling process' thread is bound + decides to GC whilst another scheduler on which the worker is running is able + to continue. +

See exit +

cancelKill :: String -> Async a -> Process ()

Like cancelWith but sends a kill instruction instead of an exit signal. +

See kill +

Querying for results +

poll :: Serializable a => Async a -> Process (AsyncResult a)

Check whether an Async handle has completed yet. The status of the + action is encoded in the returned AsyncResult. If the action has not + completed, the result will be AsyncPending, or one of the other + constructors otherwise. This function does not block waiting for the result. + Use wait or waitTimeout if you need blocking/waiting semantics. +

check :: Serializable a => Async a -> Process (Maybe (AsyncResult a))

Like poll but returns Nothing if (poll hAsync) == AsyncPending. + See poll. +

wait :: Async a -> Process (AsyncResult a)

Wait for an asynchronous action to complete, and return its + value. The result (which can include failure and/or cancellation) is + encoded by the AsyncResult type. +

Waiting with timeouts +

waitTimeout :: Serializable a => TimeInterval -> Async a -> Process (Maybe (AsyncResult a))

Wait for an asynchronous operation to complete or timeout. Returns + Nothing if the AsyncResult does not change from AsyncPending within + the specified delay, otherwise Just asyncResult is returned. If you want + to wait/block on the AsyncResult without the indirection of Maybe then + consider using wait or waitCheckTimeout instead. +

waitCancelTimeout :: Serializable a => TimeInterval -> Async a -> Process (AsyncResult a)

Wait for an asynchronous operation to complete or timeout. If it times out, + then cancelWait the async handle instead. +

waitCheckTimeout :: Serializable a => TimeInterval -> Async a -> Process (AsyncResult a)

Wait for an asynchronous operation to complete or timeout. This variant + returns the AsyncResult itself, which will be AsyncPending if the + result has not been made available, otherwise one of the other constructors. +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Call.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Call.html new file mode 100644 index 00000000..92c56894 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Call.html @@ -0,0 +1,34 @@ +Control.Distributed.Process.Platform.Call

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Safe HaskellNone

Control.Distributed.Process.Platform.Call

Description

Maintainers : Jeff Epstein, Tim Watson + Stability : experimental + Portability : non-portable (requires concurrency) +

This module provides a facility for Remote Procedure Call (rpc) style + interactions with Cloud Haskell processes. +

Clients make synchronous calls to a running process (i.e., server) using the + callAt, callTimeout and multicall functions. Processes acting as the + server are constructed using Cloud Haskell's receive family of primitives + and the callResponse family of functions in this module. +

Synopsis

Documentation

callAt :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Process (Maybe b)

Like callTimeout, but with no timeout. + Returns Nothing if the target process dies. +

callTimeout :: (Serializable a, Serializable b) => ProcessId -> a -> Tag -> Timeout -> Process (Maybe b)

Sends a message of type a to the given process, to be handled by a + corresponding callResponse... function, which will send back a message of + type b. The tag is per-process unique identifier of the transaction. If the + timeout expires or the target process dies, Nothing will be returned. +

multicall :: forall a b. (Serializable a, Serializable b) => [ProcessId] -> a -> Tag -> Timeout -> Process [Maybe b]

Like callTimeout, but sends the message to multiple + recipients and collects the results. +

callResponse :: (Serializable a, Serializable b) => (a -> Process (b, c)) -> Match c

Produces a Match that can be used with the receiveWait family of + message-receiving functions. callResponse will respond to a message of + type a sent by callTimeout, and will respond with a value of type b. +

callResponseIf :: (Serializable a, Serializable b) => (a -> Bool) -> (a -> Process (b, c)) -> Match c

callResponseDefer :: (Serializable a, Serializable b) => (a -> (b -> Process ()) -> Process c) -> Match c

callResponseDeferIf :: (Serializable a, Serializable b) => (a -> Bool) -> (a -> (b -> Process ()) -> Process c) -> Match c

callForward :: Serializable a => (a -> (ProcessId, c)) -> Match c

Produces a Match that can be used with the receiveWait family of + message-receiving functions. When calllForward receives a message of type + from from callTimeout (and similar), it will forward the message to another + process, who will be responsible for responding to it. It is the user's + responsibility to ensure that the forwarding process is linked to the + destination process, so that if it fails, the sender will be notified. +

callResponseAsync :: (Serializable a, Serializable b) => (a -> Maybe c) -> (a -> Process b) -> Match c

The message handling code is started in a separate thread. It's not + automatically linked to the calling thread, so if you want it to be + terminated when the message handling thread dies, you'll need to call + link yourself. +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Client.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Client.html new file mode 100644 index 00000000..a3ac0e7f --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Client.html @@ -0,0 +1,36 @@ +Control.Distributed.Process.Platform.ManagedProcess.Client

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Safe HaskellNone

Control.Distributed.Process.Platform.ManagedProcess.Client

Description

Client API +

Synopsis

API for client interactions with the process +

shutdown :: ProcessId -> Process ()

Send a signal instructing the process to terminate. The receive loop which + manages the process mailbox will prioritise Shutdown signals higher than + any other incoming messages, but the server might be busy (i.e., still in the + process of excuting a handler) at the time of sending however, so the caller + should not make any assumptions about the timeliness with which the shutdown + signal will be handled. If responsiveness is important, a better approach + might be to send an exit signal with Shutdown as the reason. An exit + signal will interrupt any operation currently underway and force the running + process to clean up and terminate. +

call :: forall a b. (Serializable a, Serializable b) => ProcessId -> a -> Process b

Make a synchronous call - will block until a reply is received. + The calling process will exit with TerminateReason if the calls fails. +

safeCall :: forall a b. (Serializable a, Serializable b) => ProcessId -> a -> Process (Either TerminateReason b)

Safe version of call that returns information about the error + if the operation fails. If an error occurs then the explanation will be + will be stashed away as (TerminateOther String). +

tryCall :: forall s a b. (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Maybe b)

Version of safeCall that returns Nothing if the operation fails. If + you need information about *why* a call has failed then you should use + safeCall or combine catchExit and call instead. +

callAsync :: forall s a b. (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Async b)

Performs a synchronous call to the the given server address, however the + call is made out of band and an async handle is returned immediately. This + can be passed to functions in the Async API in order to obtain the result. +

See Control.Distributed.Process.Platform.Async +

callTimeout :: forall s a b. (Addressable s, Serializable a, Serializable b) => s -> a -> TimeInterval -> Process (Maybe b)

Make a synchronous call, but timeout and return Nothing if the reply + is not received within the specified time interval. +

If the result of the call is a failure (or the call was cancelled) then + the calling process will exit, with the AsyncResult given as the reason. +

cast :: forall a m. (Addressable a, Serializable m) => a -> m -> Process ()

Sends a cast message to the server identified by ServerId. The server + will not send a response. Like Cloud Haskell's send primitive, cast is + fully asynchronous and never fails - therefore casting to a non-existent + (e.g., dead) server process will not generate an error. +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Server.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Server.html new file mode 100644 index 00000000..eac5c290 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess-Server.html @@ -0,0 +1,94 @@ +Control.Distributed.Process.Platform.ManagedProcess.Server

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Safe HaskellNone

Control.Distributed.Process.Platform.ManagedProcess.Server

Description

Server process API +

Synopsis

Server actions +

condition :: forall a b. (Serializable a, Serializable b) => (a -> b -> Bool) -> Condition a b

Creates a Conditon from a function that takes a process state a and + an input message b and returns a Bool indicating whether the associated + handler should run. +

state :: forall s m. Serializable m => (s -> Bool) -> Condition s m

Create a Condition from a function that takes a process state a and + returns a Bool indicating whether the associated handler should run. +

input :: forall s m. Serializable m => (m -> Bool) -> Condition s m

Creates a Condition from a function that takes an input message m and + returns a Bool indicating whether the associated handler should run. +

reply :: Serializable r => r -> s -> Process (ProcessReply s r)

Instructs the process to send a reply and continue running. +

replyWith :: Serializable m => m -> ProcessAction s -> Process (ProcessReply s m)

Instructs the process to send a reply and evaluate the ProcessAction. +

noReply :: Serializable r => ProcessAction s -> Process (ProcessReply s r)

Instructs the process to skip sending a reply and evaluate a ProcessAction +

noReply_ :: forall s r. Serializable r => s -> Process (ProcessReply s r)

Continue without giving a reply to the caller - equivalent to continue, + but usable in a callback passed to the handleCall family of functions. +

haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason)

Halt process execution during a call handler, without paying any attention + to the expected return type. +

continue :: s -> Process (ProcessAction s)

Instructs the process to continue running and receiving messages. +

continue_ :: s -> Process (ProcessAction s)

Version of continue that can be used in handlers that ignore process state. +

timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s)

Instructs the process to wait for incoming messages until TimeInterval + is exceeded. If no messages are handled during this period, the timeout + handler will be called. Note that this alters the process timeout permanently + such that the given TimeInterval will remain in use until changed. +

timeoutAfter_ :: TimeInterval -> s -> Process (ProcessAction s)

Version of timeoutAfter that can be used in handlers that ignore process state. +

 action (\(TimeoutPlease duration) -> timeoutAfter_ duration)
+

hibernate :: TimeInterval -> s -> Process (ProcessAction s)

Instructs the process to hibernate for the given TimeInterval. Note + that no messages will be removed from the mailbox until after hibernation has + ceased. This is equivalent to calling threadDelay. +

hibernate_ :: TimeInterval -> s -> Process (ProcessAction s)

Version of hibernate that can be used in handlers that ignore process state. +

 action (\(HibernatePlease delay) -> hibernate_ delay)
+

stop :: TerminateReason -> Process (ProcessAction s)

Instructs the process to terminate, giving the supplied reason. If a valid + terminateHandler is installed, it will be called with the TerminateReason + returned from this call, along with the process state. +

stop_ :: TerminateReason -> s -> Process (ProcessAction s)

Version of stop that can be used in handlers that ignore process state. +

 action (\ClientError -> stop_ TerminateNormal)
+

Server handler/callback creation +

handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s

handleCallIf

Arguments

:: forall s a b . (Serializable a, Serializable b) 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (s -> a -> Process (ProcessReply s b))

a reply yielding function over the process state and input message +

-> Dispatcher s 

Constructs a call handler from an ordinary function in the Process + monad. Given a function f :: (s -> a -> Process (ProcessReply s b)), + the expression handleCall f will yield a Dispatcher for inclusion + in a Behaviour specification for the GenProcess. Messages are only + dispatched to the handler if the supplied condition evaluates to True +

handleCallFrom :: forall s a b. (Serializable a, Serializable b) => (s -> Recipient -> a -> Process (ProcessReply s b)) -> Dispatcher s

As handleCall but passes the Recipient to the handler function. + This can be useful if you wish to reply later to the caller by, e.g., + spawning a process to do some work and have it replyTo caller response + out of band. In this case the callback can pass the Recipient to the + worker (or stash it away itself) and return noReply. +

handleCallFromIf

Arguments

:: forall s a b . (Serializable a, Serializable b) 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (s -> Recipient -> a -> Process (ProcessReply s b))

a reply yielding function over the process state, sender and input message +

-> Dispatcher s 

As handleCallFrom but only runs the handler if the supplied Condition + evaluates to True. +

handleCast :: Serializable a => (s -> a -> Process (ProcessAction s)) -> Dispatcher s

handleCastIf

Arguments

:: forall s a . Serializable a 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (s -> a -> Process (ProcessAction s))

an action yielding function over the process state and input message +

-> Dispatcher s 

Constructs a cast handler from an ordinary function in the Process + monad. Given a function f :: (s -> a -> Process (ProcessAction s)), + the expression handleCall f will yield a Dispatcher for inclusion + in a Behaviour specification for the GenProcess. +

handleInfo :: forall s a. Serializable a => (s -> a -> Process (ProcessAction s)) -> DeferredDispatcher s

Creates a generic input handler (i.e., for recieved messages that are not + sent using the cast or call APIs) from an ordinary function in the + Process monad. +

handleDispatch :: Serializable a => (s -> a -> Process (ProcessAction s)) -> Dispatcher s

Constructs a handler for both call and cast messages. + handleDispatch = handleDispatchIf (const True) +

handleExit :: forall s a. Serializable a => (s -> ProcessId -> a -> Process (ProcessAction s)) -> ExitSignalDispatcher s

Creates an exit handler scoped to the execution of any and all the + registered call, cast and info handlers for the process. +

Stateless handlers +

action

Arguments

:: forall s a . Serializable a 
=> (a -> s -> Process (ProcessAction s))

a function from the input message to a stateless action, cf continue_ +

-> Dispatcher s 

Constructs an action handler. Like handleDispatch this can handle both + cast and call messages and you won't know which you're dealing with. + This can be useful where certain inputs require a definite action, such as + stopping the server, without concern for the state (e.g., when stopping we + need only decide to stop, as the terminate handler can deal with state + cleanup etc). For example: +

action (MyCriticalErrorSignal -> stop_ TerminateNormal)

handleCall_ :: (Serializable a, Serializable b) => (a -> Process b) -> Dispatcher s

Constructs a call handler from a function in the Process monad. + The handler expression returns the reply, and the action will be + set to continue. +

 handleCall_ = handleCallIf_ (const True)
+

handleCallIf_

Arguments

:: forall s a b . (Serializable a, Serializable b) 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (a -> Process b)

a function from an input message to a reply +

-> Dispatcher s 

Constructs a call handler from an ordinary function in the Process + monad. This variant ignores the state argument present in handleCall and + handleCallIf and is therefore useful in a stateless server. Messges are + only dispatched to the handler if the supplied condition evaluates to True +

See handleCall +

handleCast_ :: Serializable a => (a -> s -> Process (ProcessAction s)) -> Dispatcher s

Version of handleCast that ignores the server state. +

handleCastIf_

Arguments

:: forall s a . Serializable a 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (a -> s -> Process (ProcessAction s))

a function from the input message to a stateless action, cf continue_ +

-> Dispatcher s 

Version of handleCastIf that ignores the server state. +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html new file mode 100644 index 00000000..5b70f931 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html @@ -0,0 +1,287 @@ +Control.Distributed.Process.Platform.ManagedProcess

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Portabilitynon-portable (requires concurrency)
Stabilityexperimental
MaintainerTim Watson <watson.timothy@gmail.com>
Safe HaskellNone

Control.Distributed.Process.Platform.ManagedProcess

Description

This module provides a high(er) level API for building complex Process + implementations by abstracting out the management of the process' mailbox, + reply/response handling, timeouts, process hiberation, error handling + and shutdown/stop procedures. It is modelled along similar lines to OTP's + gen_server API - http://www.erlang.org/doc/man/gen_server.html. +

API Overview
+

Once started, a generic process will consume messages from its mailbox and + pass them on to user defined handlers based on the types received (mapped + to those accepted by the handlers) and optionally by also evaluating user + supplied predicates to determine which handlers are valid. + Each handler returns a ProcessAction which specifies how we should proceed. + If none of the handlers is able to process a message (because their types are + incompatible) then the process unhandledMessagePolicy will be applied. +

The ProcessAction type defines the ways in which a process can respond + to its inputs, either by continuing to read incoming messages, setting an + optional timeout, sleeping for a while or by stopping. The optional timeout + behaves a little differently to the other process actions. If no messages + are received within the specified time span, the process timeoutHandler + will be called in order to determine the next action. +

Generic processes are defined by the ProcessDefinition type, using record + syntax. The ProcessDefinition fields contain handlers (or lists of them) + for specific tasks. In addtion to the timeoutHandler, a ProcessDefinition + may also define a terminateHandler which is called just before the process + exits. This handler will be called whenever the process is stopping, i.e., + when a callback returns stop as the next action or if an unhandled exit + signal or similar asynchronous exception is thrown in (or to) the process + itself. +

The other handlers are split into two groups: apiHandlers and infoHandlers. + The former contains handlers for the cast and call protocols, whilst the + latter contains handlers that deal with input messages which are not sent + via these API calls (i.e., messages sent using bare send or signals put + into the process mailbox by the node controller, such as + ProcessMonitorNotification and the like). +

The Cast/Call Protocol
+

Deliberate interactions with the process will usually fall into one of two + categories. A cast interaction involves a client sending a message + asynchronously and the server handling this input. No reply is sent to + the client. On the other hand, a call interaction is a kind of rpc + where the client sends a message and waits for a reply. +

The expressions given to apiHandlers have to conform to the cast|call + protocol. The details of this are, however, hidden from the user. A set + of API functions for creating apiHandlers are given instead, which + take expressions (i.e., a function or lambda expression) and create the + appropriate Dispatcher for handling the cast (or call). +

The castcall protocol handlers deal with expected/ inputs. These form + the explicit public API for the process, and will usually be exposed by + providing module level functions that defer to the cast/call API. For + example: +

+ add :: ProcessId -> Double -> Double -> Double
+ add pid x y = call pid (Add x y)
+
Handling Info Messages
+

An explicit protocol for communicating with the process can be + configured using cast and call, but it is not possible to prevent + other kinds of messages from being sent to the process mailbox. When + any message arrives for which there are no handlers able to process + its content, the UnhandledMessagePolicy will be applied. Sometimes + it is desireable to process incoming messages which aren't part of the + protocol, rather than let the policy deal with them. This is particularly + true when incoming messages are important to the process, but their point + of origin is outside the developer's control. Handling signals such as + ProcessMonitorNotification is a typical example of this: +

 handleInfo_ (\(ProcessMonitorNotification _ _ r) -> say $ show r >> continue_)
+
Handling Process State
+

The ProcessDefinition is parameterised by the type of state it maintains. + A process that has no state will have the type ProcessDefinition () and can + be bootstrapped by evaluating statelessProcess. +

All call/cast handlers come in two flavours, those which take the process + state as an input and those which do not. Handlers that ignore the process + state have to return a function that takes the state and returns the required + action. Versions of the various action generating functions ending in an + underscore are provided to simplify this: +

+   statelessProcess {
+       apiHandlers = [
+         handleCall_   (\(n :: Int) -> return (n * 2))
+       , handleCastIf_ (\(c :: String, _ :: Delay) -> c == "timeout")
+                       (\("timeout", Delay d) -> timeoutAfter_ d)
+       ]
+     , timeoutHandler = \_ _ -> stop $ TerminateOther "timeout"
+   }
+
Handling Errors
+

Error handling appears in several contexts and process definitions can + hook into these with relative ease. Only process failures as a result of + asynchronous exceptions are supported by the API, which provides several + scopes for error handling. +

Catching exceptions inside handler functions is no different to ordinary + exception handling in monadic code. +

+   handleCall (\x y ->
+                catch (hereBeDragons x y)
+                      (\(e :: SmaugTheTerribleException) ->
+                           return (Left (show e))))
+

The caveats mentioned in Control.Distributed.Process.Platform about + exit signal handling obviously apply here as well. +

Structured Exit Signal Handling
+

Because Control.Distributed.Process.ProcessExitException is a ubiquitous + signalling mechanism in Cloud Haskell, it is treated unlike other + asynchronous exceptions. The ProcessDefinition exitHandlers field + accepts a list of handlers that, for a specific exit reason, can decide + how the process should respond. If none of these handlers matches the + type of reason then the process will exit with DiedException why. In + addition, a default exit handler is installed for exit signals where the + reason == Shutdown, because this is an exit signal used explicitly and + extensively throughout the platform. The default behaviour is to gracefully + shut down the process, calling the terminateHandler as usual, before + stopping with TerminateShutdown given as the final outcome. +

Example: How to annoy your supervisor and end up force-killed: +

 handleExit  (\state from (sigExit :: Shutdown) -> continue s)
+

That code is, of course, very silly. Under some circumstances, handling + exit signals is perfectly legitimate. Handling of other forms of + asynchronous exception is not supported by this API. +

If any asynchronous exception goes unhandled, the process will immediately + exit without running the terminateHandler. It is very important to note + that in Cloud Haskell, link failures generate asynchronous exceptions in + the target and these will NOT be caught by the API and will therefore + cause the process to exit without running the termination handler + callback. If your termination handler is set up to do important work + (such as resource cleanup) then you should avoid linking you process + and use monitors instead. +

Synopsis

Starting server processes +

data InitResult s

Return type for and InitHandler expression. +

Constructors

InitOk s Delay 
forall r . Serializable r => InitFail r

denotes failed initialisation and the reason +

type InitHandler a s = a -> Process (InitResult s)

An expression used to initialise a process with its state. +

start :: a -> InitHandler a s -> ProcessDefinition s -> Process (Either (InitResult s) TerminateReason)

Starts a gen-process configured with the supplied process definition, + using an init handler and its initial arguments. This code will run the + Process until completion and return Right TerminateReason *or*, + if initialisation fails, return Left InitResult which will be + InitFail why. +

Client interactions +

shutdown :: ProcessId -> Process ()

Send a signal instructing the process to terminate. The receive loop which + manages the process mailbox will prioritise Shutdown signals higher than + any other incoming messages, but the server might be busy (i.e., still in the + process of excuting a handler) at the time of sending however, so the caller + should not make any assumptions about the timeliness with which the shutdown + signal will be handled. If responsiveness is important, a better approach + might be to send an exit signal with Shutdown as the reason. An exit + signal will interrupt any operation currently underway and force the running + process to clean up and terminate. +

statelessProcess :: ProcessDefinition ()

A basic, stateless process definition, where the unhandled message policy + is set to Terminate, the default timeout handlers does nothing (i.e., the + same as calling continue () and the terminate handler is a no-op. +

statelessInit :: Delay -> InitHandler () ()

A basic, state unaware InitHandler that can be used with + statelessProcess. +

call :: forall a b. (Serializable a, Serializable b) => ProcessId -> a -> Process b

Make a synchronous call - will block until a reply is received. + The calling process will exit with TerminateReason if the calls fails. +

safeCall :: forall a b. (Serializable a, Serializable b) => ProcessId -> a -> Process (Either TerminateReason b)

Safe version of call that returns information about the error + if the operation fails. If an error occurs then the explanation will be + will be stashed away as (TerminateOther String). +

tryCall :: forall s a b. (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Maybe b)

Version of safeCall that returns Nothing if the operation fails. If + you need information about *why* a call has failed then you should use + safeCall or combine catchExit and call instead. +

callAsync :: forall s a b. (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Async b)

Performs a synchronous call to the the given server address, however the + call is made out of band and an async handle is returned immediately. This + can be passed to functions in the Async API in order to obtain the result. +

See Control.Distributed.Process.Platform.Async +

callTimeout :: forall s a b. (Addressable s, Serializable a, Serializable b) => s -> a -> TimeInterval -> Process (Maybe b)

Make a synchronous call, but timeout and return Nothing if the reply + is not received within the specified time interval. +

If the result of the call is a failure (or the call was cancelled) then + the calling process will exit, with the AsyncResult given as the reason. +

cast :: forall a m. (Addressable a, Serializable m) => a -> m -> Process ()

Sends a cast message to the server identified by ServerId. The server + will not send a response. Like Cloud Haskell's send primitive, cast is + fully asynchronous and never fails - therefore casting to a non-existent + (e.g., dead) server process will not generate an error. +

Defining server processes +

data ProcessDefinition s

Stores the functions that determine runtime behaviour in response to + incoming messages and a policy for responding to unhandled messages. +

Constructors

ProcessDefinition 

Fields

apiHandlers :: [Dispatcher s]

functions that handle call/cast messages +

infoHandlers :: [DeferredDispatcher s]

functions that handle non call/cast messages +

exitHandlers :: [ExitSignalDispatcher s]

functions that handle exit signals +

timeoutHandler :: TimeoutHandler s

a function that handles timeouts +

terminateHandler :: TerminateHandler s

a function that is run just before the process exits +

unhandledMessagePolicy :: UnhandledMessagePolicy

how to deal with unhandled messages +

type TerminateHandler s = s -> TerminateReason -> Process ()

An expression used to handle process termination. +

type TimeoutHandler s = s -> Delay -> Process (ProcessAction s)

An expression used to handle process timeouts. +

data ProcessAction s

The action taken by a process after a handler has run and its updated state. + See continue + timeoutAfter + hibernate + stop +

Constructors

ProcessContinue s

continue with (possibly new) state +

ProcessTimeout TimeInterval s

timeout if no messages are received +

ProcessHibernate TimeInterval s

hibernate for delay +

ProcessStop TerminateReason

stop the process, giving TerminateReason +

data ProcessReply s a

Returned from handlers for the synchronous call protocol, encapsulates + the reply data and the action to take after sending the reply. A handler + can return NoReply if they wish to ignore the call. +

type CallHandler a s = s -> a -> Process (ProcessReply s a)

type CastHandler s = s -> Process ()

data UnhandledMessagePolicy

Policy for handling unexpected messages, i.e., messages which are not + sent using the call or cast APIs, and which are not handled by any of the + handleInfo handlers. +

Constructors

Terminate

stop immediately, giving TerminateOther UnhandledInput as the reason +

DeadLetter ProcessId

forward the message to the given recipient +

Drop

dequeue and then drop/ignore the message +

handleCall :: (Serializable a, Serializable b) => (s -> a -> Process (ProcessReply s b)) -> Dispatcher s

handleCallIf

Arguments

:: forall s a b . (Serializable a, Serializable b) 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (s -> a -> Process (ProcessReply s b))

a reply yielding function over the process state and input message +

-> Dispatcher s 

Constructs a call handler from an ordinary function in the Process + monad. Given a function f :: (s -> a -> Process (ProcessReply s b)), + the expression handleCall f will yield a Dispatcher for inclusion + in a Behaviour specification for the GenProcess. Messages are only + dispatched to the handler if the supplied condition evaluates to True +

handleCallFrom :: forall s a b. (Serializable a, Serializable b) => (s -> Recipient -> a -> Process (ProcessReply s b)) -> Dispatcher s

As handleCall but passes the Recipient to the handler function. + This can be useful if you wish to reply later to the caller by, e.g., + spawning a process to do some work and have it replyTo caller response + out of band. In this case the callback can pass the Recipient to the + worker (or stash it away itself) and return noReply. +

handleCallFromIf

Arguments

:: forall s a b . (Serializable a, Serializable b) 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (s -> Recipient -> a -> Process (ProcessReply s b))

a reply yielding function over the process state, sender and input message +

-> Dispatcher s 

As handleCallFrom but only runs the handler if the supplied Condition + evaluates to True. +

handleCast :: Serializable a => (s -> a -> Process (ProcessAction s)) -> Dispatcher s

handleCastIf

Arguments

:: forall s a . Serializable a 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (s -> a -> Process (ProcessAction s))

an action yielding function over the process state and input message +

-> Dispatcher s 

Constructs a cast handler from an ordinary function in the Process + monad. Given a function f :: (s -> a -> Process (ProcessAction s)), + the expression handleCall f will yield a Dispatcher for inclusion + in a Behaviour specification for the GenProcess. +

handleInfo :: forall s a. Serializable a => (s -> a -> Process (ProcessAction s)) -> DeferredDispatcher s

Creates a generic input handler (i.e., for recieved messages that are not + sent using the cast or call APIs) from an ordinary function in the + Process monad. +

handleDispatch :: Serializable a => (s -> a -> Process (ProcessAction s)) -> Dispatcher s

Constructs a handler for both call and cast messages. + handleDispatch = handleDispatchIf (const True) +

handleExit :: forall s a. Serializable a => (s -> ProcessId -> a -> Process (ProcessAction s)) -> ExitSignalDispatcher s

Creates an exit handler scoped to the execution of any and all the + registered call, cast and info handlers for the process. +

Stateless handlers +

action

Arguments

:: forall s a . Serializable a 
=> (a -> s -> Process (ProcessAction s))

a function from the input message to a stateless action, cf continue_ +

-> Dispatcher s 

Constructs an action handler. Like handleDispatch this can handle both + cast and call messages and you won't know which you're dealing with. + This can be useful where certain inputs require a definite action, such as + stopping the server, without concern for the state (e.g., when stopping we + need only decide to stop, as the terminate handler can deal with state + cleanup etc). For example: +

action (MyCriticalErrorSignal -> stop_ TerminateNormal)

handleCall_ :: (Serializable a, Serializable b) => (a -> Process b) -> Dispatcher s

Constructs a call handler from a function in the Process monad. + The handler expression returns the reply, and the action will be + set to continue. +

 handleCall_ = handleCallIf_ (const True)
+

handleCallIf_

Arguments

:: forall s a b . (Serializable a, Serializable b) 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (a -> Process b)

a function from an input message to a reply +

-> Dispatcher s 

Constructs a call handler from an ordinary function in the Process + monad. This variant ignores the state argument present in handleCall and + handleCallIf and is therefore useful in a stateless server. Messges are + only dispatched to the handler if the supplied condition evaluates to True +

See handleCall +

handleCast_ :: Serializable a => (a -> s -> Process (ProcessAction s)) -> Dispatcher s

Version of handleCast that ignores the server state. +

handleCastIf_

Arguments

:: forall s a . Serializable a 
=> Condition s a

predicate that must be satisfied for the handler to run +

-> (a -> s -> Process (ProcessAction s))

a function from the input message to a stateless action, cf continue_ +

-> Dispatcher s 

Version of handleCastIf that ignores the server state. +

Constructing handler results +

condition :: forall a b. (Serializable a, Serializable b) => (a -> b -> Bool) -> Condition a b

Creates a Conditon from a function that takes a process state a and + an input message b and returns a Bool indicating whether the associated + handler should run. +

state :: forall s m. Serializable m => (s -> Bool) -> Condition s m

Create a Condition from a function that takes a process state a and + returns a Bool indicating whether the associated handler should run. +

input :: forall s m. Serializable m => (m -> Bool) -> Condition s m

Creates a Condition from a function that takes an input message m and + returns a Bool indicating whether the associated handler should run. +

reply :: Serializable r => r -> s -> Process (ProcessReply s r)

Instructs the process to send a reply and continue running. +

replyWith :: Serializable m => m -> ProcessAction s -> Process (ProcessReply s m)

Instructs the process to send a reply and evaluate the ProcessAction. +

noReply :: Serializable r => ProcessAction s -> Process (ProcessReply s r)

Instructs the process to skip sending a reply and evaluate a ProcessAction +

noReply_ :: forall s r. Serializable r => s -> Process (ProcessReply s r)

Continue without giving a reply to the caller - equivalent to continue, + but usable in a callback passed to the handleCall family of functions. +

haltNoReply_ :: TerminateReason -> Process (ProcessReply s TerminateReason)

Halt process execution during a call handler, without paying any attention + to the expected return type. +

continue :: s -> Process (ProcessAction s)

Instructs the process to continue running and receiving messages. +

continue_ :: s -> Process (ProcessAction s)

Version of continue that can be used in handlers that ignore process state. +

timeoutAfter :: TimeInterval -> s -> Process (ProcessAction s)

Instructs the process to wait for incoming messages until TimeInterval + is exceeded. If no messages are handled during this period, the timeout + handler will be called. Note that this alters the process timeout permanently + such that the given TimeInterval will remain in use until changed. +

timeoutAfter_ :: TimeInterval -> s -> Process (ProcessAction s)

Version of timeoutAfter that can be used in handlers that ignore process state. +

 action (\(TimeoutPlease duration) -> timeoutAfter_ duration)
+

hibernate :: TimeInterval -> s -> Process (ProcessAction s)

Instructs the process to hibernate for the given TimeInterval. Note + that no messages will be removed from the mailbox until after hibernation has + ceased. This is equivalent to calling threadDelay. +

hibernate_ :: TimeInterval -> s -> Process (ProcessAction s)

Version of hibernate that can be used in handlers that ignore process state. +

 action (\(HibernatePlease delay) -> hibernate_ delay)
+

stop :: TerminateReason -> Process (ProcessAction s)

Instructs the process to terminate, giving the supplied reason. If a valid + terminateHandler is installed, it will be called with the TerminateReason + returned from this call, along with the process state. +

stop_ :: TerminateReason -> s -> Process (ProcessAction s)

Version of stop that can be used in handlers that ignore process state. +

 action (\ClientError -> stop_ TerminateNormal)
+
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Test.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Test.html new file mode 100644 index 00000000..64c9a723 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Test.html @@ -0,0 +1,17 @@ +Control.Distributed.Process.Platform.Test

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Portabilitynon-portable (requires concurrency)
Stabilityexperimental
MaintainerTim Watson
Safe HaskellNone

Control.Distributed.Process.Platform.Test

Description

This module provides basic building blocks for testing Cloud Haskell programs. +

Documentation

type TestResult a = MVar a

A mutable cell containing a test result. +

noop :: Process ()

Does exactly what it says on the tin, doing so in the Process monad. +

stash :: TestResult a -> a -> Process ()

Stashes a value in our TestResult using putMVar +

data Ping

A simple Ping signal +

Constructors

Ping 

data TestProcessControl

Control signals used to manage test processes +

startTestProcess :: Process () -> Process ProcessId

Starts a test process on the local node. +

runTestProcess :: Process () -> Process ()

Runs a test process around the supplied proc, which is executed + whenever the outer process loop receives a Go signal. +

testProcessGo :: ProcessId -> Process ()

Tell a test process to continue executing +

testProcessStop :: ProcessId -> Process ()

Tell a test process to stop (i.e., terminate) +

testProcessReport :: ProcessId -> Process ()

Tell a test process to send a report (message) + back to the calling process +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Time.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Time.html new file mode 100644 index 00000000..c8f6c667 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Time.html @@ -0,0 +1,27 @@ +Control.Distributed.Process.Platform.Time

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Portabilitynon-portable (requires concurrency)
Stabilityexperimental
MaintainerTim Watson
Safe HaskellNone

Control.Distributed.Process.Platform.Time

Description

This module provides facilities for working with time delays and timeouts. + The type Timeout and the timeout family of functions provide mechanisms + for working with threadDelay-like behaviour operates on microsecond values. +

The TimeInterval and TimeUnit related functions provide an abstraction + for working with various time intervals and the Delay type provides a + corrolary to timeout that works with these. +

Documentation

milliSeconds :: Int -> TimeInterval

given a number, produces a TimeInterval of milliseconds +

seconds :: Int -> TimeInterval

given a number, produces a TimeInterval of seconds +

minutes :: Int -> TimeInterval

given a number, produces a TimeInterval of minutes +

hours :: Int -> TimeInterval

given a number, produces a TimeInterval of hours +

asTimeout :: TimeInterval -> Int

converts the supplied TimeInterval to milliseconds +

after :: Int -> TimeUnit -> Int

Convenience for making timeouts; e.g., +

 receiveTimeout (after 3 Seconds) [ match (\"ok" -> return ()) ]
+

within :: Int -> TimeUnit -> TimeInterval

Convenience for making TimeInterval; e.g., +

 let ti = within 5 Seconds in .....
+

timeToMs :: TimeUnit -> Int -> Int

converts the supplied TimeUnit to microseconds +

data TimeUnit

Defines the time unit for a Timeout value +

Constructors

Days 
Hours 
Minutes 
Seconds 
Millis 
Micros 

type Timeout = Maybe Int

Represents a timeout in terms of microseconds, where Nothing stands for + infinity and Just 0, no-delay. +

data TimeoutNotification

Send to a process when a timeout expires. +

Constructors

TimeoutNotification Tag 

timeout :: Int -> Tag -> ProcessId -> Process ()

Sends the calling process TimeoutNotification tag after time microseconds +

infiniteWait :: Timeout

Constructs an inifinite Timeout. +

noWait :: Timeout

Constructs a no-wait Timeout +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Timer.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Timer.html new file mode 100644 index 00000000..0baf0df5 --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Timer.html @@ -0,0 +1,33 @@ +Control.Distributed.Process.Platform.Timer

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Portabilitynon-portable (requires concurrency)
Stabilityexperimental
MaintainerTim Watson <watson.timothy@gmail.com>
Safe HaskellNone

Control.Distributed.Process.Platform.Timer

Description

Provides an API for running code or sending messages, either after some + initial delay or periodically, and for cancelling, re-setting and/or + flushing pending timers. +

Documentation

type TimerRef = ProcessId

an opaque reference to a timer +

data Tick

represents a tick event that timers can generate +

Constructors

Tick 

sleep :: TimeInterval -> Process ()

blocks the calling Process for the specified TimeInterval. Note that this + function assumes that a blocking receive is the most efficient approach to + acheiving this, however the runtime semantics (particularly with regards + scheduling) should not differ from threadDelay in practise. +

sendAfter :: Serializable a => TimeInterval -> ProcessId -> a -> Process TimerRef

starts a timer which sends the supplied message to the destination + process after the specified time interval. +

runAfter :: TimeInterval -> Process () -> Process TimerRef

runs the supplied process action(s) after t has elapsed +

exitAfter :: Serializable a => TimeInterval -> ProcessId -> a -> Process TimerRef

calls exit pid reason after t has elapsed +

killAfter :: TimeInterval -> ProcessId -> String -> Process TimerRef

kills the specified process after t has elapsed +

startTimer :: Serializable a => TimeInterval -> ProcessId -> a -> Process TimerRef

starts a timer that repeatedly sends the supplied message to the destination + process each time the specified time interval elapses. To stop messages from + being sent in future, cancelTimer can be called. +

ticker :: TimeInterval -> ProcessId -> Process TimerRef

sets up a timer that sends Tick repeatedly at intervals of t +

periodically :: TimeInterval -> Process () -> Process TimerRef

runs the supplied process action(s) repeatedly at intervals of t +

resetTimer :: TimerRef -> Process ()

resets a running timer. Note: Cancelling a timer does not guarantee that + all its messages are prevented from being delivered to the target process. + Also note that resetting an ongoing timer (started using the startTimer or + periodically functions) will only cause the current elapsed period to time + out, after which the timer will continue running. To stop a long-running + timer permanently, you should use cancelTimer instead. +

cancelTimer :: TimerRef -> Process ()

permanently cancels a timer +

flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Delay -> Process ()

cancels a running timer and flushes any viable timer messages from the + process' message queue. This function should only be called by the process + expecting to receive the timer's messages! +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/Control-Distributed-Process-Platform.html b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform.html new file mode 100644 index 00000000..a6c2220b --- /dev/null +++ b/static/doc/distributed-process-platform/Control-Distributed-Process-Platform.html @@ -0,0 +1,54 @@ +Control.Distributed.Process.Platform

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Safe HaskellNone

Control.Distributed.Process.Platform

Description

Cloud Haskell Platform
+

It is important not to be too general when catching exceptions in +handler code, because asynchonous exceptions provide cloud haskell with +its process termination mechanism. Two exceptions in particular, signal +the instigators intention to stop a process immediately, these are raised +in response to the kill and exit primitives provided by +the base distributed-process package. +

Exported Types +

class Addressable a where

Provides a unified API for addressing processes +

Methods

sendTo :: Serializable m => a -> m -> Process ()

Send a message to the target asynchronously +

resolve :: a -> Process (Maybe ProcessId)

Resolve the reference to a process id, or Nothing if resolution fails +

data TerminateReason

Provides a reason for process termination. +

Constructors

TerminateNormal

indicates normal exit +

TerminateShutdown

normal response to a Shutdown +

TerminateOther !String

abnormal (error) shutdown +

type Tag = Int

Tags provide uniqueness for messages, so that they can be + matched with their response. +

type TagPool = MVar Tag

Generates unique Tag for messages and response pairs. + Each process that depends, directly or indirectly, on + the call mechanisms in Control.Distributed.Process.Global.Call + should have at most one TagPool on which to draw unique message + tags. +

Utilities and Extended Primitives +

spawnLinkLocal :: Process () -> Process ProcessId

Node local version of spawnLink. + Note that this is just the sequential composition of spawn and link. + (The Unified semantics that underlies Cloud Haskell does not even support + a synchronous link operation) +

spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef)

Like spawnLinkLocal, but monitor the spawned process +

linkOnFailure :: ProcessId -> Process ()

CH's link primitive, unlike Erlang's, will trigger when the target + process dies for any reason. This function has semantics like Erlang's: + it will trigger ProcessLinkException only when the target dies abnormally. +

times :: Int -> Process () -> Process ()

Apply the supplied expression n times +

matchCond :: Serializable a => (a -> Maybe (Process b)) -> Match b

An alternative to matchIf that allows both predicate and action + to be expressed in one parameter. +

Call/Tagging support +

newTagPool :: Process TagPool

Create a new per-process source of unique + message identifiers. +

getTag :: TagPool -> Process Tag

Extract a new identifier from a TagPool. +

Registration and Process Lookup +

whereisOrStart :: String -> Process () -> Process ProcessId

Returns the pid of the process that has been registered + under the given name. This refers to a local, per-node registration, + not global registration. If that name is unregistered, a process + is started. This is a handy way to start per-node named servers. +

whereisOrStartRemote :: NodeId -> String -> Closure (Process ()) -> Process (Maybe ProcessId)

A remote equivalent of whereisOrStart. It deals with the + node registry on the given node, and the process, if it needs to be started, + will run on that node. If the node is inaccessible, Nothing will be returned. +

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/distributed-process-platform.haddock b/static/doc/distributed-process-platform/distributed-process-platform.haddock new file mode 100644 index 0000000000000000000000000000000000000000..c43ccbab9ec1efc546da1c0f0f4b0aeb4e365024 GIT binary patch literal 134679 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 Ojj>5Q^^;8P(*FU96uG(p literal 0 HcmV?d00001 diff --git a/static/doc/distributed-process-platform/doc-index-95.html b/static/doc/distributed-process-platform/doc-index-95.html new file mode 100644 index 00000000..b279f845 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-95.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - _)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-A.html b/static/doc/distributed-process-platform/doc-index-A.html new file mode 100644 index 00000000..64696746 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-A.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - A)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Index - A

actionControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
AddressableControl.Distributed.Process.Platform
afterControl.Distributed.Process.Platform.Time
apiHandlersControl.Distributed.Process.Platform.ManagedProcess
asTimeoutControl.Distributed.Process.Platform.Time
AsyncControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
async 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
AsyncCancelledControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncChanControl.Distributed.Process.Platform.Async.AsyncChan
asyncChanControl.Distributed.Process.Platform.Async
AsyncDoneControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncFailedControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncLinked 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
asyncLinkedChanControl.Distributed.Process.Platform.Async
asyncLinkedSTMControl.Distributed.Process.Platform.Async
AsyncLinkFailedControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncPendingControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncRefControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncRemoteTaskControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncResultControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncSTMControl.Distributed.Process.Platform.Async.AsyncSTM
asyncSTMControl.Distributed.Process.Platform.Async
AsyncTask 
1 (Type/Class)Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
2 (Data Constructor)Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskDictControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskNodeControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskProcControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncWorkerControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-All.html b/static/doc/distributed-process-platform/doc-index-All.html new file mode 100644 index 00000000..a05bf55c --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-All.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Index

actionControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
AddressableControl.Distributed.Process.Platform
afterControl.Distributed.Process.Platform.Time
apiHandlersControl.Distributed.Process.Platform.ManagedProcess
asTimeoutControl.Distributed.Process.Platform.Time
AsyncControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
async 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
AsyncCancelledControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncChanControl.Distributed.Process.Platform.Async.AsyncChan
asyncChanControl.Distributed.Process.Platform.Async
AsyncDoneControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncFailedControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncLinked 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
asyncLinkedChanControl.Distributed.Process.Platform.Async
asyncLinkedSTMControl.Distributed.Process.Platform.Async
AsyncLinkFailedControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncPendingControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncRefControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncRemoteTaskControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncResultControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
AsyncSTMControl.Distributed.Process.Platform.Async.AsyncSTM
asyncSTMControl.Distributed.Process.Platform.Async
AsyncTask 
1 (Type/Class)Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
2 (Data Constructor)Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskDictControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskNodeControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncTaskProcControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
asyncWorkerControl.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async
callControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
callAsyncControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
callAtControl.Distributed.Process.Platform.Call
callForwardControl.Distributed.Process.Platform.Call
CallHandlerControl.Distributed.Process.Platform.ManagedProcess
callResponseControl.Distributed.Process.Platform.Call
callResponseAsyncControl.Distributed.Process.Platform.Call
callResponseDeferControl.Distributed.Process.Platform.Call
callResponseDeferIfControl.Distributed.Process.Platform.Call
callResponseIfControl.Distributed.Process.Platform.Call
callTimeout 
1 (Function)Control.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
2 (Function)Control.Distributed.Process.Platform.Call
cancel 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
cancelKill 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
cancelTimerControl.Distributed.Process.Platform.Timer
cancelWait 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
cancelWith 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
castControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
CastHandlerControl.Distributed.Process.Platform.ManagedProcess
check 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
conditionControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
continueControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
continue_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
DaysControl.Distributed.Process.Platform.Time
DeadLetterControl.Distributed.Process.Platform.ManagedProcess
defaultProcessControl.Distributed.Process.Platform.ManagedProcess
Delay 
1 (Type/Class)Control.Distributed.Process.Platform.Time
2 (Data Constructor)Control.Distributed.Process.Platform.Time
DropControl.Distributed.Process.Platform.ManagedProcess
exitAfterControl.Distributed.Process.Platform.Timer
exitHandlersControl.Distributed.Process.Platform.ManagedProcess
flushTimerControl.Distributed.Process.Platform.Timer
getTagControl.Distributed.Process.Platform
haltNoReply_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallFromControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallFromIfControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallIfControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallIf_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCall_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCastControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCastIfControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCastIf_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCast_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleDispatchControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleExitControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleInfoControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
hibernateControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
hibernate_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
HoursControl.Distributed.Process.Platform.Time
hoursControl.Distributed.Process.Platform.Time
infiniteWaitControl.Distributed.Process.Platform.Time
InfinityControl.Distributed.Process.Platform.Time
infoHandlersControl.Distributed.Process.Platform.ManagedProcess
InitFailControl.Distributed.Process.Platform.ManagedProcess
InitHandlerControl.Distributed.Process.Platform.ManagedProcess
InitOkControl.Distributed.Process.Platform.ManagedProcess
InitResultControl.Distributed.Process.Platform.ManagedProcess
inputControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
killAfterControl.Distributed.Process.Platform.Timer
linkOnFailureControl.Distributed.Process.Platform
matchCondControl.Distributed.Process.Platform
MicrosControl.Distributed.Process.Platform.Time
microSecondsControl.Distributed.Process.Platform.Time
MillisControl.Distributed.Process.Platform.Time
milliSecondsControl.Distributed.Process.Platform.Time
MinutesControl.Distributed.Process.Platform.Time
minutesControl.Distributed.Process.Platform.Time
multicallControl.Distributed.Process.Platform.Call
newAsync 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
newTagPoolControl.Distributed.Process.Platform
noopControl.Distributed.Process.Platform.Test
noReplyControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
noReply_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
noWaitControl.Distributed.Process.Platform.Time
periodicallyControl.Distributed.Process.Platform.Timer
PidControl.Distributed.Process.Platform
Ping 
1 (Type/Class)Control.Distributed.Process.Platform.Test
2 (Data Constructor)Control.Distributed.Process.Platform.Test
pingControl.Distributed.Process.Platform.Test
poll 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
pollSTMControl.Distributed.Process.Platform.Async.AsyncSTM
ProcessActionControl.Distributed.Process.Platform.ManagedProcess
ProcessContinueControl.Distributed.Process.Platform.ManagedProcess
ProcessDefinition 
1 (Type/Class)Control.Distributed.Process.Platform.ManagedProcess
2 (Data Constructor)Control.Distributed.Process.Platform.ManagedProcess
ProcessHibernateControl.Distributed.Process.Platform.ManagedProcess
ProcessReplyControl.Distributed.Process.Platform.ManagedProcess
ProcessStopControl.Distributed.Process.Platform.ManagedProcess
ProcessTimeoutControl.Distributed.Process.Platform.ManagedProcess
RecipientControl.Distributed.Process.Platform
RegisteredControl.Distributed.Process.Platform
RemoteRegisteredControl.Distributed.Process.Platform
remoteTaskControl.Distributed.Process.Platform.Async
replyControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
replyToControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
replyWithControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
resetTimerControl.Distributed.Process.Platform.Timer
resolveControl.Distributed.Process.Platform
runAfterControl.Distributed.Process.Platform.Timer
runProcessControl.Distributed.Process.Platform.ManagedProcess
runTestProcessControl.Distributed.Process.Platform.Test
safeCallControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
SecondsControl.Distributed.Process.Platform.Time
secondsControl.Distributed.Process.Platform.Time
sendAfterControl.Distributed.Process.Platform.Timer
sendToControl.Distributed.Process.Platform
shutdownControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
sleepControl.Distributed.Process.Platform.Timer
spawnLinkLocalControl.Distributed.Process.Platform
spawnMonitorLocalControl.Distributed.Process.Platform
startControl.Distributed.Process.Platform.ManagedProcess
startTestProcessControl.Distributed.Process.Platform.Test
startTimerControl.Distributed.Process.Platform.Timer
stashControl.Distributed.Process.Platform.Test
stateControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
statelessInitControl.Distributed.Process.Platform.ManagedProcess
statelessProcessControl.Distributed.Process.Platform.ManagedProcess
stopControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
stop_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
TagControl.Distributed.Process.Platform
TagPoolControl.Distributed.Process.Platform
taskControl.Distributed.Process.Platform.Async
TerminateControl.Distributed.Process.Platform.ManagedProcess
TerminateHandlerControl.Distributed.Process.Platform.ManagedProcess
terminateHandlerControl.Distributed.Process.Platform.ManagedProcess
TerminateNormalControl.Distributed.Process.Platform
TerminateOtherControl.Distributed.Process.Platform
TerminateReasonControl.Distributed.Process.Platform
TerminateShutdownControl.Distributed.Process.Platform
TestProcessControlControl.Distributed.Process.Platform.Test
testProcessGoControl.Distributed.Process.Platform.Test
testProcessReportControl.Distributed.Process.Platform.Test
testProcessStopControl.Distributed.Process.Platform.Test
TestResultControl.Distributed.Process.Platform.Test
Tick 
1 (Type/Class)Control.Distributed.Process.Platform.Timer
2 (Data Constructor)Control.Distributed.Process.Platform.Timer
tickerControl.Distributed.Process.Platform.Timer
TimeIntervalControl.Distributed.Process.Platform.Time
TimeoutControl.Distributed.Process.Platform.Time
timeoutControl.Distributed.Process.Platform.Time
timeoutAfterControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
timeoutAfter_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
TimeoutHandlerControl.Distributed.Process.Platform.ManagedProcess
timeoutHandlerControl.Distributed.Process.Platform.ManagedProcess
TimeoutNotification 
1 (Type/Class)Control.Distributed.Process.Platform.Time
2 (Data Constructor)Control.Distributed.Process.Platform.Time
TimerRefControl.Distributed.Process.Platform.Timer
timesControl.Distributed.Process.Platform
timeToMsControl.Distributed.Process.Platform.Time
TimeUnitControl.Distributed.Process.Platform.Time
tryCallControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
tryForkProcessControl.Distributed.Process.Platform.Test
tryRunProcessControl.Distributed.Process.Platform.Test
UnhandledMessagePolicyControl.Distributed.Process.Platform.ManagedProcess
unhandledMessagePolicyControl.Distributed.Process.Platform.ManagedProcess
wait 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
waitAny 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
waitAnyCancel 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
waitAnyTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
waitBothControl.Distributed.Process.Platform.Async.AsyncSTM
waitCancelTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
2 (Function)Control.Distributed.Process.Platform.Async
waitCheckTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
waitEitherControl.Distributed.Process.Platform.Async.AsyncSTM
waitEither_Control.Distributed.Process.Platform.Async.AsyncSTM
waitTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
waitTimeoutSTMControl.Distributed.Process.Platform.Async.AsyncSTM
whereisOrStartControl.Distributed.Process.Platform
whereisOrStartRemoteControl.Distributed.Process.Platform
withinControl.Distributed.Process.Platform.Time
workerControl.Distributed.Process.Platform.Async.AsyncChan
_asyncWorkerControl.Distributed.Process.Platform.Async.AsyncSTM
__remoteTableControl.Distributed.Process.Platform
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-C.html b/static/doc/distributed-process-platform/doc-index-C.html new file mode 100644 index 00000000..34a1d659 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-C.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - C)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Index - C

callControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
callAsyncControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
callAtControl.Distributed.Process.Platform.Call
callForwardControl.Distributed.Process.Platform.Call
CallHandlerControl.Distributed.Process.Platform.ManagedProcess
callResponseControl.Distributed.Process.Platform.Call
callResponseAsyncControl.Distributed.Process.Platform.Call
callResponseDeferControl.Distributed.Process.Platform.Call
callResponseDeferIfControl.Distributed.Process.Platform.Call
callResponseIfControl.Distributed.Process.Platform.Call
callTimeout 
1 (Function)Control.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
2 (Function)Control.Distributed.Process.Platform.Call
cancel 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
cancelKill 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
cancelTimerControl.Distributed.Process.Platform.Timer
cancelWait 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
cancelWith 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
castControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
CastHandlerControl.Distributed.Process.Platform.ManagedProcess
check 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
conditionControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
continueControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
continue_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-D.html b/static/doc/distributed-process-platform/doc-index-D.html new file mode 100644 index 00000000..3ba91c82 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-D.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - D)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-E.html b/static/doc/distributed-process-platform/doc-index-E.html new file mode 100644 index 00000000..9e8d3a32 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-E.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - E)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-F.html b/static/doc/distributed-process-platform/doc-index-F.html new file mode 100644 index 00000000..1647cc06 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-F.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - F)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-G.html b/static/doc/distributed-process-platform/doc-index-G.html new file mode 100644 index 00000000..aa2a0b6e --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-G.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - G)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-H.html b/static/doc/distributed-process-platform/doc-index-H.html new file mode 100644 index 00000000..c7faf215 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-H.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - H)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Index - H

haltNoReply_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallFromControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallFromIfControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallIfControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCallIf_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCall_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCastControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCastIfControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCastIf_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleCast_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleDispatchControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleExitControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
handleInfoControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
hibernateControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
hibernate_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
HoursControl.Distributed.Process.Platform.Time
hoursControl.Distributed.Process.Platform.Time
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-I.html b/static/doc/distributed-process-platform/doc-index-I.html new file mode 100644 index 00000000..a55aef72 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-I.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - I)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-K.html b/static/doc/distributed-process-platform/doc-index-K.html new file mode 100644 index 00000000..2177cb28 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-K.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - K)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-L.html b/static/doc/distributed-process-platform/doc-index-L.html new file mode 100644 index 00000000..41e10474 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-L.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - L)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-M.html b/static/doc/distributed-process-platform/doc-index-M.html new file mode 100644 index 00000000..461ed686 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-M.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - M)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-N.html b/static/doc/distributed-process-platform/doc-index-N.html new file mode 100644 index 00000000..9818f98c --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-N.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - N)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-P.html b/static/doc/distributed-process-platform/doc-index-P.html new file mode 100644 index 00000000..bd3d5bbc --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-P.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - P)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-R.html b/static/doc/distributed-process-platform/doc-index-R.html new file mode 100644 index 00000000..9ae094a4 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-R.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - R)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-S.html b/static/doc/distributed-process-platform/doc-index-S.html new file mode 100644 index 00000000..96818c3d --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-S.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - S)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-T.html b/static/doc/distributed-process-platform/doc-index-T.html new file mode 100644 index 00000000..a34167ae --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-T.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - T)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Index - T

TagControl.Distributed.Process.Platform
TagPoolControl.Distributed.Process.Platform
taskControl.Distributed.Process.Platform.Async
TerminateControl.Distributed.Process.Platform.ManagedProcess
TerminateHandlerControl.Distributed.Process.Platform.ManagedProcess
terminateHandlerControl.Distributed.Process.Platform.ManagedProcess
TerminateNormalControl.Distributed.Process.Platform
TerminateOtherControl.Distributed.Process.Platform
TerminateReasonControl.Distributed.Process.Platform
TerminateShutdownControl.Distributed.Process.Platform
TestProcessControlControl.Distributed.Process.Platform.Test
testProcessGoControl.Distributed.Process.Platform.Test
testProcessReportControl.Distributed.Process.Platform.Test
testProcessStopControl.Distributed.Process.Platform.Test
TestResultControl.Distributed.Process.Platform.Test
Tick 
1 (Type/Class)Control.Distributed.Process.Platform.Timer
2 (Data Constructor)Control.Distributed.Process.Platform.Timer
tickerControl.Distributed.Process.Platform.Timer
TimeIntervalControl.Distributed.Process.Platform.Time
TimeoutControl.Distributed.Process.Platform.Time
timeoutControl.Distributed.Process.Platform.Time
timeoutAfterControl.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
timeoutAfter_Control.Distributed.Process.Platform.ManagedProcess.Server, Control.Distributed.Process.Platform.ManagedProcess
TimeoutHandlerControl.Distributed.Process.Platform.ManagedProcess
timeoutHandlerControl.Distributed.Process.Platform.ManagedProcess
TimeoutNotification 
1 (Type/Class)Control.Distributed.Process.Platform.Time
2 (Data Constructor)Control.Distributed.Process.Platform.Time
TimerRefControl.Distributed.Process.Platform.Timer
timesControl.Distributed.Process.Platform
timeToMsControl.Distributed.Process.Platform.Time
TimeUnitControl.Distributed.Process.Platform.Time
tryCallControl.Distributed.Process.Platform.ManagedProcess.Client, Control.Distributed.Process.Platform.ManagedProcess
tryForkProcessControl.Distributed.Process.Platform.Test
tryRunProcessControl.Distributed.Process.Platform.Test
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-U.html b/static/doc/distributed-process-platform/doc-index-U.html new file mode 100644 index 00000000..2885c609 --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-U.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - U)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index-W.html b/static/doc/distributed-process-platform/doc-index-W.html new file mode 100644 index 00000000..de4b277a --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index-W.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index - W)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

Index - W

wait 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
waitAny 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
waitAnyCancel 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
waitAnyTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
waitBothControl.Distributed.Process.Platform.Async.AsyncSTM
waitCancelTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
2 (Function)Control.Distributed.Process.Platform.Async
waitCheckTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
waitEitherControl.Distributed.Process.Platform.Async.AsyncSTM
waitEither_Control.Distributed.Process.Platform.Async.AsyncSTM
waitTimeout 
1 (Function)Control.Distributed.Process.Platform.Async.AsyncSTM
2 (Function)Control.Distributed.Process.Platform.Async.AsyncChan
3 (Function)Control.Distributed.Process.Platform.Async
waitTimeoutSTMControl.Distributed.Process.Platform.Async.AsyncSTM
whereisOrStartControl.Distributed.Process.Platform
whereisOrStartRemoteControl.Distributed.Process.Platform
withinControl.Distributed.Process.Platform.Time
workerControl.Distributed.Process.Platform.Async.AsyncChan
\ No newline at end of file diff --git a/static/doc/distributed-process-platform/doc-index.html b/static/doc/distributed-process-platform/doc-index.html new file mode 100644 index 00000000..a7ddf2db --- /dev/null +++ b/static/doc/distributed-process-platform/doc-index.html @@ -0,0 +1,4 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform (Index)

distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

\ No newline at end of file diff --git a/static/doc/distributed-process-platform/frames.html b/static/doc/distributed-process-platform/frames.html new file mode 100644 index 00000000..1b4e38d4 --- /dev/null +++ b/static/doc/distributed-process-platform/frames.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/static/doc/distributed-process-platform/haddock-util.js b/static/doc/distributed-process-platform/haddock-util.js new file mode 100644 index 00000000..9a6fccf7 --- /dev/null +++ b/static/doc/distributed-process-platform/haddock-util.js @@ -0,0 +1,344 @@ +// Haddock JavaScript utilities + +var rspace = /\s\s+/g, + rtrim = /^\s+|\s+$/g; + +function spaced(s) { return (" " + s + " ").replace(rspace, " "); } +function trim(s) { return s.replace(rtrim, ""); } + +function hasClass(elem, value) { + var className = spaced(elem.className || ""); + return className.indexOf( " " + value + " " ) >= 0; +} + +function addClass(elem, value) { + var className = spaced(elem.className || ""); + if ( className.indexOf( " " + value + " " ) < 0 ) { + elem.className = trim(className + " " + value); + } +} + +function removeClass(elem, value) { + var className = spaced(elem.className || ""); + className = className.replace(" " + value + " ", " "); + elem.className = trim(className); +} + +function toggleClass(elem, valueOn, valueOff, bool) { + if (bool == null) { bool = ! hasClass(elem, valueOn); } + if (bool) { + removeClass(elem, valueOff); + addClass(elem, valueOn); + } + else { + removeClass(elem, valueOn); + addClass(elem, valueOff); + } + return bool; +} + + +function makeClassToggle(valueOn, valueOff) +{ + return function(elem, bool) { + return toggleClass(elem, valueOn, valueOff, bool); + } +} + +toggleShow = makeClassToggle("show", "hide"); +toggleCollapser = makeClassToggle("collapser", "expander"); + +function toggleSection(id) +{ + var b = toggleShow(document.getElementById("section." + id)); + toggleCollapser(document.getElementById("control." + id), b); + rememberCollapsed(id, b); + return b; +} + +var collapsed = {}; +function rememberCollapsed(id, b) +{ + if(b) + delete collapsed[id] + else + collapsed[id] = null; + + var sections = []; + for(var i in collapsed) + { + if(collapsed.hasOwnProperty(i)) + sections.push(i); + } + // cookie specific to this page; don't use setCookie which sets path=/ + document.cookie = "collapsed=" + escape(sections.join('+')); +} + +function restoreCollapsed() +{ + var cookie = getCookie("collapsed"); + if(!cookie) + return; + + var ids = cookie.split('+'); + for(var i in ids) + { + if(document.getElementById("section." + ids[i])) + toggleSection(ids[i]); + } +} + +function setCookie(name, value) { + document.cookie = name + "=" + escape(value) + ";path=/;"; +} + +function clearCookie(name) { + document.cookie = name + "=;path=/;expires=Thu, 01-Jan-1970 00:00:01 GMT;"; +} + +function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) { + return unescape(c.substring(nameEQ.length,c.length)); + } + } + return null; +} + + + +var max_results = 75; // 50 is not enough to search for map in the base libraries +var shown_range = null; +var last_search = null; + +function quick_search() +{ + perform_search(false); +} + +function full_search() +{ + perform_search(true); +} + + +function perform_search(full) +{ + var text = document.getElementById("searchbox").value.toLowerCase(); + if (text == last_search && !full) return; + last_search = text; + + var table = document.getElementById("indexlist"); + var status = document.getElementById("searchmsg"); + var children = table.firstChild.childNodes; + + // first figure out the first node with the prefix + var first = bisect(-1); + var last = (first == -1 ? -1 : bisect(1)); + + if (first == -1) + { + table.className = ""; + status.innerHTML = "No results found, displaying all"; + } + else if (first == 0 && last == children.length - 1) + { + table.className = ""; + status.innerHTML = ""; + } + else if (last - first >= max_results && !full) + { + table.className = ""; + status.innerHTML = "More than " + max_results + ", press Search to display"; + } + else + { + // decide what you need to clear/show + if (shown_range) + setclass(shown_range[0], shown_range[1], "indexrow"); + setclass(first, last, "indexshow"); + shown_range = [first, last]; + table.className = "indexsearch"; + status.innerHTML = ""; + } + + + function setclass(first, last, status) + { + for (var i = first; i <= last; i++) + { + children[i].className = status; + } + } + + + // do a binary search, treating 0 as ... + // return either -1 (no 0's found) or location of most far match + function bisect(dir) + { + var first = 0, finish = children.length - 1; + var mid, success = false; + + while (finish - first > 3) + { + mid = Math.floor((finish + first) / 2); + + var i = checkitem(mid); + if (i == 0) i = dir; + if (i == -1) + finish = mid; + else + first = mid; + } + var a = (dir == 1 ? first : finish); + var b = (dir == 1 ? finish : first); + for (var i = b; i != a - dir; i -= dir) + { + if (checkitem(i) == 0) return i; + } + return -1; + } + + + // from an index, decide what the result is + // 0 = match, -1 is lower, 1 is higher + function checkitem(i) + { + var s = getitem(i).toLowerCase().substr(0, text.length); + if (s == text) return 0; + else return (s > text ? -1 : 1); + } + + + // from an index, get its string + // this abstracts over alternates + function getitem(i) + { + for ( ; i >= 0; i--) + { + var s = children[i].firstChild.firstChild.data; + if (s.indexOf(' ') == -1) + return s; + } + return ""; // should never be reached + } +} + +function setSynopsis(filename) { + if (parent.window.synopsis) { + if (parent.window.synopsis.location.replace) { + // In Firefox this avoids adding the change to the history. + parent.window.synopsis.location.replace(filename); + } else { + parent.window.synopsis.location = filename; + } + } +} + +function addMenuItem(html) { + var menu = document.getElementById("page-menu"); + if (menu) { + var btn = menu.firstChild.cloneNode(false); + btn.innerHTML = html; + menu.appendChild(btn); + } +} + +function adjustForFrames() { + var bodyCls; + + if (parent.location.href == window.location.href) { + // not in frames, so add Frames button + addMenuItem("Frames"); + bodyCls = "no-frame"; + } + else { + bodyCls = "in-frame"; + } + addClass(document.body, bodyCls); +} + +function reframe() { + setCookie("haddock-reframe", document.URL); + window.location = "frames.html"; +} + +function postReframe() { + var s = getCookie("haddock-reframe"); + if (s) { + parent.window.main.location = s; + clearCookie("haddock-reframe"); + } +} + +function styles() { + var i, a, es = document.getElementsByTagName("link"), rs = []; + for (i = 0; a = es[i]; i++) { + if(a.rel.indexOf("style") != -1 && a.title) { + rs.push(a); + } + } + return rs; +} + +function addStyleMenu() { + var as = styles(); + var i, a, btns = ""; + for(i=0; a = as[i]; i++) { + btns += "
  • " + + a.title + "
  • " + } + if (as.length > 1) { + var h = "
    " + + "Style ▾" + + "
      " + btns + "
    " + + "
    "; + addMenuItem(h); + } +} + +function setActiveStyleSheet(title) { + var as = styles(); + var i, a, found; + for(i=0; a = as[i]; i++) { + a.disabled = true; + // need to do this always, some browsers are edge triggered + if(a.title == title) { + found = a; + } + } + if (found) { + found.disabled = false; + setCookie("haddock-style", title); + } + else { + as[0].disabled = false; + clearCookie("haddock-style"); + } + styleMenu(false); +} + +function resetStyle() { + var s = getCookie("haddock-style"); + if (s) setActiveStyleSheet(s); +} + + +function styleMenu(show) { + var m = document.getElementById('style-menu'); + if (m) toggleShow(m, show); +} + + +function pageLoad() { + addStyleMenu(); + adjustForFrames(); + resetStyle(); + restoreCollapsed(); +} + diff --git a/static/doc/distributed-process-platform/hslogo-16.png b/static/doc/distributed-process-platform/hslogo-16.png new file mode 100644 index 0000000000000000000000000000000000000000..0ff8579fbd897417b0d6dad6e920f8882138a7c0 GIT binary patch literal 1684 zcmV;F25b3=P)4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY4c7nw4c7reD4Tcy00T@(L_t(I z5sj2vNEA^R$7gqDc6T=2^@fUA2(c`MltuL5<|KW>RWz$&YbU@|M|{$E*8Tu-Ux!w z1Y*Dr&Ubfr&v-nZaaB{3ilRumrjPmk{sZvQEWlW+{o~IH|8)=s6c#X9S5s5d%J z4@)&QH5|xQY-)^L1n0pTRu0Lx9`08YTjTwn^6 z0;b1+aQ@)n;Em$q;=7BBi)v0zj&o^g>0Whp^_^5IbxIUP8C@y9;R?*Ouu}rmfxbU= zwtWVNke-m!=`7bYEhWpcI5#)9qp`8E0lr6IQ)ARL3Ui}Af@grj8aN1=r>Cb+prlzO zNfJs*N_tUm2ZL%5* zPmL2??da$TR904gL(VDAQ-Fv_Dk}Pdw*4T(%*f4MKLRg=4ekMjhe2mW zMFsBwg%ftWT}0kxRaIk1k7qJ8*#cKB;Ft{i`zVIs-Nqge;!!Ld7#O&Qqu7e0sJmP) z$MW*>L$vSB&dxp@iA3U9fo)-7!Czlr{|o7Hv{1oyg3xsu%gn@(b1>$;SM-ZaQ`HV=V0s;lr%d8bd;xY zGwNvm3=Iu=tyXIgtJnf@A(2S@M140N ew{UA~tMxaJq;$xaSSi*30000distributed-process-platform-0.1.0: The Cloud Haskell Application Platform \ No newline at end of file diff --git a/static/doc/distributed-process-platform/index.html b/static/doc/distributed-process-platform/index.html new file mode 100644 index 00000000..85cabeb0 --- /dev/null +++ b/static/doc/distributed-process-platform/index.html @@ -0,0 +1,8 @@ +distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

    distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

    distributed-process-platform-0.1.0: The Cloud Haskell Application Platform

    Modelled after Erlang's OTP, this framework provides similar +facilities for Cloud Haskell, grouping essential practices +into a set of modules and standards designed to help you build +concurrent, distributed applications with relative ease. +

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncChan.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncChan.html new file mode 100644 index 00000000..03849c59 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncChan.html @@ -0,0 +1,9 @@ +Control.Distributed.Process.Platform.Async.AsyncChan

    Control.Distributed.Process.Platform.Async.AsyncChan

    Exported types +

    type AsyncRef

    data AsyncTask a

    data AsyncChan a

    data Async a

    Spawning asynchronous operations +

    Cancelling asynchronous operations +

    Querying for results +

    Waiting with timeouts +

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncSTM.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncSTM.html new file mode 100644 index 00000000..92f7cc73 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async-AsyncSTM.html @@ -0,0 +1,10 @@ +Control.Distributed.Process.Platform.Async.AsyncSTM

    Control.Distributed.Process.Platform.Async.AsyncSTM

    Exported types +

    type AsyncRef

    data AsyncTask a

    data AsyncSTM a

    data Async a

    Spawning asynchronous operations +

    Cancelling asynchronous operations +

    Querying for results +

    Waiting with timeouts +

    STM versions +

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async.html new file mode 100644 index 00000000..2b56eba8 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Async.html @@ -0,0 +1,9 @@ +Control.Distributed.Process.Platform.Async

    Control.Distributed.Process.Platform.Async

    Exported Types +

    data Async a

    type AsyncRef

    data AsyncTask a

    Spawning asynchronous operations +

    Cancelling asynchronous operations +

    Querying for results +

    Waiting with timeouts +

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Call.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Call.html new file mode 100644 index 00000000..e9136fbb --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Call.html @@ -0,0 +1,4 @@ +Control.Distributed.Process.Platform.Call

    Control.Distributed.Process.Platform.Call

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Client.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Client.html new file mode 100644 index 00000000..3f2eacbf --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Client.html @@ -0,0 +1,5 @@ +Control.Distributed.Process.Platform.ManagedProcess.Client

    Control.Distributed.Process.Platform.ManagedProcess.Client

    API for client interactions with the process +

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Server.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Server.html new file mode 100644 index 00000000..e1254760 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess-Server.html @@ -0,0 +1,7 @@ +Control.Distributed.Process.Platform.ManagedProcess.Server

    Control.Distributed.Process.Platform.ManagedProcess.Server

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess.html new file mode 100644 index 00000000..5bf8442c --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-ManagedProcess.html @@ -0,0 +1,9 @@ +Control.Distributed.Process.Platform.ManagedProcess

    Control.Distributed.Process.Platform.ManagedProcess

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Test.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Test.html new file mode 100644 index 00000000..03394ace --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Test.html @@ -0,0 +1,4 @@ +Control.Distributed.Process.Platform.Test

    Control.Distributed.Process.Platform.Test

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Time.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Time.html new file mode 100644 index 00000000..4fc0e813 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Time.html @@ -0,0 +1,4 @@ +Control.Distributed.Process.Platform.Time

    Control.Distributed.Process.Platform.Time

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Timer.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Timer.html new file mode 100644 index 00000000..c77445d1 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform-Timer.html @@ -0,0 +1,4 @@ +Control.Distributed.Process.Platform.Timer

    Control.Distributed.Process.Platform.Timer

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform.html b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform.html new file mode 100644 index 00000000..79f142d3 --- /dev/null +++ b/static/doc/distributed-process-platform/mini_Control-Distributed-Process-Platform.html @@ -0,0 +1,8 @@ +Control.Distributed.Process.Platform

    Control.Distributed.Process.Platform

    Exported Types +

    class Addressable a

    data Recipient

    type Tag

    type TagPool

    Utilities and Extended Primitives +

    Call/Tagging support +

    Registration and Process Lookup +

    \ No newline at end of file diff --git a/static/doc/distributed-process-platform/minus.gif b/static/doc/distributed-process-platform/minus.gif new file mode 100644 index 0000000000000000000000000000000000000000..1deac2fe1a42e35b994f1b855488f392c50f6a89 GIT binary patch literal 56 zcmZ?wbhEHb * { + font-size: 93%; /* 12pt */ +} + +#mini #module-list .caption, +#mini #module-header .caption { + font-size: 125%; /* 15pt */ +} + +#mini #interface h1, +#mini #interface h2, +#mini #interface h3, +#mini #interface h4 { + font-size: 109%; /* 13pt */ + margin: 1em 0 0; +} + +#mini #interface .top, +#mini #interface .src { + margin: 0; +} + +#mini #module-list ul { + list-style: none; + margin: 0; +} + +#alphabet ul { + list-style: none; + padding: 0; + margin: 0.5em 0 0; + text-align: center; +} + +#alphabet li { + display: inline; + margin: 0 0.25em; +} + +#alphabet a { + font-weight: bold; +} + +#index .caption, +#module-list .caption { font-size: 131%; /* 17pt */ } + +#index table { + margin-left: 2em; +} + +#index .src { + font-weight: bold; +} +#index .alt { + font-size: 77%; /* 10pt */ + font-style: italic; + padding-left: 2em; +} + +#index td + td { + padding-left: 1em; +} + +#module-list ul { + list-style: none; + margin: 0 0 0 2em; +} + +#module-list li { + clear: right; +} + +#module-list span.collapser, +#module-list span.expander { + background-position: 0 0.3em; +} + +#module-list .package { + float: right; +} + +/* @end */ diff --git a/static/doc/distributed-process-platform/plus.gif b/static/doc/distributed-process-platform/plus.gif new file mode 100644 index 0000000000000000000000000000000000000000..2d15c14173d23f664b955cd24f51c82f5f09d91d GIT binary patch literal 59 zcmZ?wbhEHbgbBX M^XE!9f*2UA0nx1yDgXcg literal 0 HcmV?d00001 diff --git a/static/doc/distributed-process-platform/synopsis.png b/static/doc/distributed-process-platform/synopsis.png new file mode 100644 index 0000000000000000000000000000000000000000..85fb86ec84907bcc86531dc82871948ff4d471fa GIT binary patch literal 11327 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 B5rhB$ literal 0 HcmV?d00001 diff --git a/static/semantics.pdf b/static/semantics.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6f01eb85dc30a0abf201ea29abc1a9bc530cb675 GIT binary patch literal 167226 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 z zqbUA}Oen8M4s%-u0j&sV(K~PS|z2*1-VQV<BdF?TKIr2F>d#9bpr-M)1PMk}zj zn47^(0#*eM`pZ7Htzc;Y@F>;Zik+Kg4rQP+`02XJ>oG-#{WTm|e9dkXhJFmjw1vJl zDcY{*!}lo={b~Nz#qN3jjgIw>q2K7sc5r%h8p+~L3{LwT*IJK~dT}90g{}LH>MU=> z+C52;!G0kN(B%CMx^Xff07x;sh33)&kTHVYer&Los?Wm!n)}I@qrC_nL380CAT%jC zG0jD2#F7-)#1ctVsy|X@A_3KEusc(M$}sdul6yZ)Z6SYdXJmH^}b@%}3e! zX&Vj<1}-hhTelX$ zUdK<=?O4!4A(NW~^zG;eg%I;cW8#S<_<2vj-O@1wGSmrex0~n zxJJ?z=ZmwV5)}p}G*F9k6D^gw=%>k3NYIT+qg=>SI`K{TNenhLMa|-3@b6ABMn_s= zso98hwN-Aiw|}3#O*m98hiFcLy*dem4Tus4@QpeUHq_ROrlkDHd&v7LQrb*_ODMIrPqe77CX*!Iv^7#BhCC|{(L;7F4i7|@7AZ*}cl6%k z*DEF%@l%ZG4QUZtxuLF6=TSxeq+Lp9^Ml&cJKi!hT~^a*y4CZT8)%U+4}SJA1qr06 zIiH2Tnif&K+*=wnO(cRHh&fH~1swgs4P0X*Dv04-sZalqM@!a2)I(({p|-23GHMx2{sWDIml9_ zt;H##>WDAhADQuST1cX4WQ$B=(v!~ioq|HrQ;O)Mk*&Np<@aYg18uM@?Wg$$dQQXn z1~ALOGsuU>thk`wBpK-@=xpiis)JDj`T&-&O#8!CskM-|mq(xhv2;WX?kV=0vl0j_NlxW0td8PW<|)~u9ie{PKbuJM67OVO5! z;vht@I^QE1+-386pa)t)&7LNr{(%p$#c!a$h^o6?*;>e=M-WV7KG1-_+)k&Nx!x29 z`J%wjmXvkWwV6Q$YY^5!ncF0on0gD;M?NHDN6xWCeCBX zj0NheOtF}(!WWKE7p9)|tz_@*RJzyMH`lIHkNTqvjeK%f6>)>iC%ZGKh;Z~)1CDUQ z4m)@Bps6&Qe7b!8J3&b>Njvr9DD)f+{vD{_spgbYJLbk@ z)i+UFl`t)(q4N2RMFenH|_k9jQ!byv^zQ4)@#e^}s)1RS1RplB``TEW54qN#XD+htgU)G#Jc- zdmR-HttGP*vGx9fD{DTjo9z@Pa`>nP$+|T!&B$okikeWbx*8 zsBcn5ZZL+|C-FZdEwoT{#vnRl$Vc$yxfRv3t=P#6%Hi!><*6fdnIDJ>C5A1-Bbi{+ zj|fX&3!uV~Rz-fHtTBBQzSPlGG5YdSueSk@=lW>>Ud~Kk zkpWjUNYTQW^v4_dU^r)6=FFle(d+QGkJu^bK>o!-3zVbZMP-BxB)Oe@NVfxtVZVI^ zF;3SNheXuEy0b&T)D%Er{0Il^a7i;Ag5diMXlHfTJz0D;kqlE^YsK9A)4AiAGc76m z=vtR8=yL>M z)8@VgY+?t}KQ+)b`Pr}llXf66L1dx4_u7E?{5eZK3TD0C<(AvQLT)`|4l>+f#V z(6m-M9zXQx)Khfk3X&&MW%pvq7Mv8^Y1ItIQcc!yQi6G@mI8*3=IqnV?hz;_=sBVldLcBluX4jV)a}~ox%c~) z4o=S8A0B<^x^=$YBMkg7BVGC-3%Te9REH&gager={ z6mBHs>7I4TYgw910L#?}%aRlhNallMi7`1yrKDXP< zJaag$Uc6bL=WI0A*sb$eHv3qdO+Pl+URcHLQiz15%p#v9-ZRrr+Hsa!v-bbgmpums z#H)fVDLne%m;{Kqn-6`^^^F(*wZ!%$6;hr=lN}tkrD;XALgwfMd3Z@kAT?aon8TV? zM^G?Vz``bbw1T)4D_XVI^2-$+`_k|EWVMf;YI6D4Ddq#+lEYKV5GTKyk$wJU2KKoX zh2mU$Wr&ETOeX8I`igMDoZO*Jk zLm$87?;9rEJeZyuXl9|?94Q6Ly4P)ErI-SR#Xtj`G<~_eB=1#by(|#{hneyhCyApa&e(aC+t1bQ@^X#W8xeN3C%t0b zWIq}Xn9~A7FYlw%)zKyoSj<}w3!yhHItFwEcEU$XcT`*pug0@pg0%%f;ig^PWLo@k z%-{}I#EbN2PI(w4xeMnB8A>AK8k7X@UeicnC@*O&dm_#>8s0Z+szmekjxB5Ul{J^x z@qCRf`_OBph;lp;Y^&-*{W}xnFB@}4k1uBlr{h)Vm&QUHUW^Yau6RB6Q;H}`Y7lnJ z>WQJuBbwr6#WaTzS_acHl2UkFVT*I5girptcavKe4Ia6K(ow|Gq1HaVUd)W>43<4I zw_6QAtRzq6>WNAt07$jiSAb~K?^Ux3=Y{4C^B0o;FYVJauv-KA;%dyTY}miOK`KWq zG?e;2`nRY0!Uyo1_IcS7AJ<0C&R^m*TgPuEC5B!;yHCPvpJjl8`es)E1fhwk-DO67 zMR3BfbNHk3oW{4Kp#@N_HPftCNGz7Pbdse68tmhwn1GU+fP7}~MjC4LZH?ekwR`-| z!7i!c4;85(p+HEuch7`F*tI2HlPh)r5iZbRQX&o?V-o`L%K}T#ecM8doqhATO1wgT z2YOtUBTzHP?ai8Gw7qimy#$lm$4{8q67-&UC!652g}=P`lTTr4`u2kKR89-%cHBu- zJLeVCR?H2kAF;<3Z-d&xq0(nIz|#M|?6IVS*hIU!J!iJO7+Xf)r{ZiVEZPIR0IJF- z4$->~7>{>>>J7MS>%7wV1jpdx?fw30fi}v=O)5LrO}Av)FK@KkZ6xzV4d*6NGnFs1 zcI?w9=*2t1G2R)w*H;hUHNkR>1(~f}--?f}K(;*e>LW{`J$VzhuxyB!9BR@UA{G1` zIAI<%Y6m*?-~@RMci&e*QfdD^#hP#5Ohe;awT3z~Wk_pMJH?kM3cS0XcwFApI2q~c zvQQ?}of=yoepX7r*eG1Yst)sbm)nqnTj*F413LB@YCl&>+wnE!yRnN03m$g*QS8-B zbj9<Ksw@+k>(K5=uA>}&{##7gOwMQuT~>C7ERzqgqs?vqhEWw3c^FEy3*bsoV=gG zcjIHKWu5WLS}o=rQTe?f@HI<}N%2Am(xOLV;+)DX<)?wPa6f;&iXhiIYo{U@u1y)C zRf*InW4esV4VZ419_yrKW-H?2KNso8%E#4OBcV%ka}BLdTSRdMuRv(QnjSq`8TW3) zSlDT06%6f5EZ!mh+~gmf#hZV-|y@^l;deScJ}f@{(`Ez$n*b@G=zWY!`jF-5!d zn-a^Yw`OOUHTS~%NVwdj9-BXmwsH@Q9S(&(aHW|s(3~{!1}Ef-9UIEqb%v^DaHV>W z+;<6Of=G(d4%hRlb~G(eO>#uV;yRBWbk5^rj&#fC zr}YU?N;eIINaWxYZZeGmeYAe1q}vnuwovB(QC6mQOnw&~t??S6stY2V9DreRhppZ9qD6A;WX9Le=i!8(}!y z3NFStT1L{et@*y^(5q6rqa0HlzjcT`sSfy&JQ?I*7e<&Sd-UV@!{1t7HCg)PCbE2> zJ{tKXmV0`%)j2$>U5)e8Tuk&pPRu;)+PyHajy(-BALU!yM@#9{70x{gXhjg}ZXtU` zm>y-g+|1qD%UDH$NMzb~wZoCUJ;0p0FLg-%!{yKjUl)AbjP!)5qBUa8_A&5}bHAFX#8j{?P2vwFIm*Jh zXC?CS3}tM??2@>?AHvt=T)o>ESAK?m(qypfUi#{`&GEHMVtD1lle8lhicD6Ltzca< zY){(fHNOPPBQ-4d6pQMjZ^gg(i@H_ys5Z9X1g#_w4EQ{c{@0+FP>z8F zM%gtbij)382;X^uMpTufS#y!iPQ$Lkvxc)~&?iv__h1#BP<_GM!@B{)4l_`Dn;g>`KZ$$=ILN*RDS{?>X+UJ}brvGL|_ zEN_}ZQSf|3^n_ESFTHB&9VsyLiO0f}j7&ih=LFdk1nOdJNU!Ij1ujfG1@3vwxxnk+ zPE1aZj|uUF2u&r?oHT=R@xWI3a@s9^1X`w+F*#9f|t~%B(H?@vP zM?98!?S>_bQ07%2 z%7PVusk*YsO3d^iQWsv8st_IbHnC_-*9)Nz4H!Z<83nCMAHM}z?1fCC|ADZsUB4Wu!heu3+ZodPWZI!YlyPJ- z^bsm<5hmCeon+{v(MQ-jj6kf-&6toB!P%mISCP+QVWY{uSXNYY(uh``3bS1k|57Em z{-(_g8#=Ac?3GM+9$)IqD+ep{n!Fyy#Jb0{8t-@=4*3Vfr1-Yd8|2iNW4;dj!g~ns zC}C$s!fLlk~?2#eaYN9xn~YmAB;moK*p~ z{UNzps+mRyd|^09a&At~^?oN!bWNlIEtjYg9?>`{RMKQ-!>@6J5?Z@Q3h3&dev+lK z)u&~QxM?tCyJf_nku>F4G82|>|3Tq3>?yMQT|Duap5b zC_AUZe=NP}lHbRc6ZK=Gyj;w}kuk{Q!R~;wy$kd43=&E0>PMU}wk3OT-tu4F@F=Q4 z;UdeZiMcUtF)Yh{z?878dQ5DMl+0+ouZ&&2T_1$={9{$2BZOLuoME&};+lQxJ^I0w z_c_XZo5Iz}idkA4Qn-w(KU0F>qo1B2oI$7d?g%miE`3n1`@s#iBoa6n2%T|n88P38 z<7Z5mpHh;tGArScPaEI>aMa9rCZj+L&ytSL&IjzkHX=H~{ot%{TkhaOvIJ96AyKIV zisJ(Njeuvx&Xh`9;P7>KpgmGz7tbrMQ*H#Gvdrf}$nuO-ThjmowVxc3-Sk0=6K8}S zaof-R&uT#rldOuRjRLOLRLH?2j)(Z7PAhjOgAz(!lf=Y~@7W}P%#y~)WUFRWeZP(8 zVF2TCjulAIYN|@oY)ws^V%#KQ&uzQ-2Zs%bpN3y*P}vXJ>5kPGc1P zIho1SU1xBTtez3q6%OsOYb(aOSJWS4TU*~F%O;kJYd}vsdy1WhqI9O73he`;qLXRJ zQ((YP1!G1AK>Hq5Ez^(y-#5ZNi8`u;l##BPkn&ZFA*SwlfA-Hip?qOHXW{xpwT&K< zRW#2lZ({c46y6Wn;Hw-$1+$~8Y}nu9i+F=`)_N#1(dOo2s1QMIY`bKaK<6={! zDB5{bZYl?OcaALLLm%bdPao$>9XHdAgoYT_Dq}I#?U$7fBBafY;*i%MS3p+Ei=)PT zIGg2HgkVmj(|2D~n|)+9bBOq}%**Ajs|Np^QpZ!7P+<4SD8NSOHGIWD6u9)+zUCzp z6Igh6xegW(7MStO5~5W}**&VQ+r3)Pl`aQf@?c)atY_*cb)0I$qcB5ZY4x=`;n;kJ zOe&Lg@gGjFw%+4loMSKEOyG&$QNcw zw(DriWl6etdl6#UCw6Ih%0v(+PBh{Zg{#J3M~K%#m_8~48wf$_7QGZh)RUeL#+8lg zKF_HAs+(tTgi+GD>izoXwKihSt*~?iL8r6Y1Iz065g9LhUF z%*#M@=~?>x0a-VAW!&4KlzI@gTWl!Xh?URmT(8<;mC!A#iHw~qtGJ5#i0p&T;Zbc2 z$Q9R20ye#{sl$9R@z$2ebgpsu?>Pp$gS%7)-C=wYar_A1nO$D-#$}r5C6thSCw681 z?O-kVt!eq^&NkjzrbYFIf?TE@j@gs&_+|Nt^+=Qf)ZUiV!|jc%sTM6N8k7fpVL^a5T*%aM(74VE{>%d5K&a>U5n##TCQF*C3}7A0vl1Fb8EtUsZ~+xi>!O&EUKY!0s)tVm#Od(>Ng{NahZ<288%j8PySwbFq#GB*qj2aZb$P8RA6em>@TP> zDjQ(%JH$%Zy!YI{PH!g78ZQ7pr%8{L*8awXy{_6pTc8m2!R6CKx^7_!*4XSTYXO;XqF_x z$JK(7Woi;5)=Ku2WIk5gs-VPd^VV8p^isgqm8vF3+y6Lf)`YPwJM-vzU8t^QYtoyz zJ7uRXE{5TWO{i8Z)}_fk&fRn+ivtD`U0&_CQAppYYt92{o1-R7(wqte?pX7QKS>^E z5GGl5{ML2n!X$?wVQjsK0)?K8;K(nD6c?r53?HF+uM8mIb*K59C z#RE-4r!cKJ2f+PDz>KkIuVkBqtlayIkU5=c#w%a6pY8ZNB;d(EzQNbH*-Tn*Me!PY z*@>@3oJ*kAC&+`%Y&aa|xh!;&sB6>dH7%hZ2MB+apk@qOtfwE}T z#eQKE-rABJ-T^N%m7O)Q4)Fz_PoGbr1V@MJb)1=suQQOL6~R)4kLs^(_X${UFCHPS zlHIcvfJOF$+#TX?EJ`K|*lxQ)pfBpe#xTG7z<1yTjBoKukN$+OWt-C)ogOMG>cG|N zN3JvLI$%)+6cU`l>%N^@k_;mkz|PMh(8@w!PCBFmAUzw)-;kO0*2q|mS|NRwrtoE) z!bi&aNnvwp_^r&;#cMBRld5gusa3&MdW=EH*v8dvoBC;INFHioYg9B%uesEHk+*NK zZqHloK%+I$L@9BG_`JRZLz*T54h4tx<=Gq6^_@5;nVtZXRKbkLz~UNNIYA zYs0(MeLXQDBWBLa!SsgRf8DQL*%N;QXAlzff)1c6qOxlqfGlcCJwAS9Am4C~GW>!y z!xj8Zq>W6Rd>dS(=JP10OkiGf#kD%veiP5FQLt-O8!)bEq4U#J~#U z7su6T`}#enw6Wl&AUBf#{0YT7e~vNhV;HbpTa*GJ^XeRKt4Vc+U2c6jXB1d($!e+i zc1@H<5ajCt?8+}Efk(9!2NbDPmBzoWrdsD)Zuy|CfBYAdjF>cv4IY2mh@yyD)3VWz z9j4Fr?wbQjQTQ;mrHVqS%9bb+X}w{`1x=uA**Q6i1#%6 zrWlS8G+rY>8ya)QX8mLU3QGhN|Ff@O@1ruuH&KCnEPqH+o@f-0ZkXb`r=%BfkfR$Xe~UpU{tmJe%5c6tJP7+Z1_K`_;{2u%-g0ef zBHksoMw@Uh%4M+%nQ!+@R)X!DgsaBvoxXGe_hySDfiaHTLl0u0BC9>Oy1!0};q}n9 zr$YETi)y5MQMRpzeNPtKggnf)L*+yW&u5iGU0^Mnhy`xagR;M%!DS5QZgJi*UC$TdIz>u+8s=36QQy6cU=A zHd2@)Y{GGVJ()@l?!+PAB5!|a<9zsVNFyUnfYSFAV+HB>J{UX%EA?(}1&~)bvvx|< z&WSk~6);$1dkD`<*6Z}LWubY$kX_Q1oj3EAAkZTizFZWRvN^hKNc>Z!OttOZ-*Sa} zHfO-}@7NG5l8+tXRg8}-s_S#352(Xcni0A$^dZ`%Xb`OHdJ;5q-zu4n(t~k!Vezqj zkjHiUQRPWf@?!;dG-H^y zl6Ehcmy2rcF4c-Vp9el}Ui)deAO4=`BG?8cP`Xz@fwjJ=o!n+-%!+Q$y9*6<(Ay2F zEMps#I>bzU(=lCI?z;T8l@1%*`ZG)igqtHTy%G4+J^y@lMg^}MD}^+CI=uybwP8e{ zM%fguv|T|U+Kfx}(+A)8?-yd>rQ0Y#%?3<644%TZP8}bQujjDfbLiR=yF~VnS9&&| z_ZNl1RITZ&)C1aN!l7|^tf-Ap^X)#k`JDF(kOF_kp*V{muU;t&__anljeM54{raFi z6#sweODF}8P^5(0-Y$vPeFRYY$irqwXN#muw$~$_rSPb)6qU{R2V{2VY7>zVCaVNQ z1DjeIJpI3tHYZwASrBaJb4Fd-54pC8@_Hx!XHdRhhDiM{;_`t-&A}7|l6IWzcF9)H17$@+cUu;rP zM&E%4iWfbX6E@3QdI?rEY&N^jJrO?)`R#BxFQ_r$TGgS3y^kG%r5D2Uhe%r-_#Ggp z4p=+uI)>c}3s}2kd56T36`or0kyfYxi(N|Z%MS)+w%CVm2rTRRu5o1Z%qNDfIY&7P z@0+@#P2cpb{pZWlPRSy+rI>={K|yHy3|LvxBg|)zMPcpkgsWr;2g`Qb56(X*PR!ibNCs}%$U*sfBZL)L}$PT7J5OR?#G84CJLo2ZqN z`u$S36@8qIgv{g+QxTrnot!~SF60yD#D%g_Zt;K_NW+IA?whRtgNQg1O4i!hqse%~Of3K1`brf#|bwOi_Gi!$j1tnc|wOv1_o4>lJv) z_G)sn7PNjZ?5#hE`NdCYA42BudleeGo++{!L9!g|-ORF@6iU0_!HIsq)@L; zEtog#GIwekI8X}PdFLu3_OwChyDmR z;1V$|bo7N~+uC^F6tmIPV9G=_Ce=F2dfZSeazl$D_sI3bPiV9i44&iK7pcDJ?SH-J zvvlPiPo~9Ya5duH>J{N%Qcy>XsXVV}nN8%09O3P>D#Pzp-b_eS=6kS>>nFLKf~m+~ zrDZI1q{w{_hKj3nHb8Q=go7e?3j>QgWcN#NOYB(+pR4r8Qw(-`ci7=So$~jK(&C9V ze@>i~d-oXNU=xs|Q;_T8do6w^?j;l;kuWr$0<|@Zr7t9mAfO#!1J;WEE>HZm8E$_8 zkGgc}q($D(@NB}&`9hZAW_nIeVRW4YDFu>~_4~3LHJ-`IpcmvoraN0T_Zkqg zcZ-GYoAO@Ave1C@+`a=L?(>my=X_bp!cEYNT)VsmR=7Ydbe5}r&x4VIx)K=%vd3+# zlt9geq#-j+_m;7((J{$;Tkoc`3q5bF-KKgc1UGoPn4d?4fQi;uc)nVO3TfUlFh>>? zp$vE;skHc%wqHDY6~WK2lr4@}LFhK=EmK0DeTB}1<$0iXNw@$X1f_z;^X4;oQros8 zvn%_G9;p8&HpH?S!uG=HD7MU%bg8{(S>vAOjJA(Uq9C@QHOe}Ar0`xbVwJrM{2PEA zshV0qGl1O8lR}gAuZ*2c9mE?;`hokt5IPse^QZT79RWd&*dhdq2<)pU)B?|k-*#?a zv3}-b=ZU1IL;;AI5{A5A6s9RJ{BL26g!eF#5r=6TpK2 z~sDYkVJMN4vldQ}(W* zojw+V8@!tpTYMDwUQJbK^cJ7H=bD!1*YYOr6B-!5^)8bQ!Ah=Q%#+&{>zxEct-{PG!c)<0~Uw_m+u;wP`CI_I-uD*e8gWr}M zziG%f`bDkIcJ}ul9oToSOFj;N?yUgX)+-9_EjykXo*mE4fPM-bKU`FidRmbFzwhvU zF=dl{M!NiMedxn)s~HBK#0D0?fL!lD*8Z&&IF<-;ZK(hwzc~|Ezivdo_~KuFM0dXt z-M@YZKV71~$4I}wJEwkOsgKT%icQX7KRO_9r`W*qfM3sJ{=ZtSCCp>vuYME1uhKqy zeBwWG43T)Lc2)5pn?2(L<1_E@bMv3!xg`jdXvZF1kJ~!{ZrweAe#2t@fE~UkRa>6yf!_h5 zcSlZ+KD^$3-@YfAzS%G|>i-O%)GZcX8$h;taOwY^zfoIXpP#&xUhca6*qr%o_~E+{ z%z>N7G&Tb?puxGlr6(OL2&MC}&>3y54uNu^>_3#~mK9SJLoMKbF?t^Ag#2}K0r46N zp376+eJ4&!!zDf0zU2Xth1tgJZxU?e$12?+y883s3Yfb6iesub*A)BA_m($XQ9Xot z?{sl(8*62mk-|$HFW3CoPMEXH95UyQ8nQRP)1eQdVYWhg;HLkFe}4%DA9t|~Z)=wk zCK?7F)>Uut^(MaYh?Hm%QVR*J1Ye=V<4v=Ddr9jz!0+im>cb{wYQ8{kCG*$~O;60I z4No7s3o(RA^h-*sOO&%9NKA@G40dahpHZcs{js&lA@-xzner+_Q2C(HqQ24V7{;2= z*8JRKdY-efEgxvsP6M|u3Ny$Ubj$Gjs6Qg6C*x96BW<+fSir4oiH_YWPZ-o@+@fLw zQpxj)#56{!!Od#@iV>MpRDB1ZGiu5*LEA!&qKRCz<&rGHpi}(cF4^amC`@Gmchj?a znPU0m`BYNt97lJB|Zsy42AmCar zkUvoDx^INOzlh^p7Zkw;?eST`iB%*CzMpb&7=aONQ5;dyA}+XV_+?BDpD&d@5ILGO za+PbwJc74#&gDtE?{sO{)9qivVxwBN0at&x(9SXP#9HBKN3@t-8P)GC+dQ*(s9OuZ zJ6t&2#&%h}agFI~^BzR*(2>$>hP`URiAKs;o}-$5^3&ndCn>=4ph~wowy-TdF~AKHQx^ z&ij|d1dbQ55Z>LYj+w)ZeslJGW%DOY^J@=a+EcRpW-UDuy?on`Sh!0jr%~I|x*j+yKy)btRyGGZ2=PXE$a(P3X}DYED^aE~&$}1e04~&JrIj zY)sHj-2KB2%|0%{oZ)p$Wo2P9RK>LsF}R$3h^;R&c4BAI>{ebnh#}5#rJMbiv;oLb zmk}5g#b^Mh!G{a^U-biSvKIkUm>+#1!#y~mfDa*IP|Y?WJnT{q(&1Ar7{DwVM&dk> zgp1nn_v{;6A=gf`#6hSuQZXh*c%YN@eiS5Hd@hT2zrtzSkPc#zrLW3P#@>ky90Ak%*M+|-r%a35Zd z{tKBPRnOr|Z*p1fs8%KfRdcqIRuSX=Hg&U%{VGx*1f>ZiiNr_R60w`RdrWhP&GwJR z2Yix){{tQlbdwEU_D+H$n#p_&idXMye{;mezCL&efj(jQm&(xJ#U&& zdyVR<7<`D+j+3fcN;tb_kz-4S-1pJ5{1O*cX>98C+tJ!=4{ zz}B-#mP%QfRaLUBOY=S*U8z6^ajgj85|cba_uZ4hD85M7&I)pLp0y#+x6KcbG^TOq;AU3^!WPeNyV{4l=-*e$2%#|E|sDk7WFGkm4uO=E^TM^5E z-<~Q|DplmQ=Syjorl%M_>8T9s6s54qEs5eq1dJ*IUK_(hlM(AX9|0pPrNXnks#I+A!lm$K*U_b4>Vo-#;Sm3TXmL}ySlF|v zSs!px=BU*V-&!?EpZ3xe3p;mZtTOVzt%%FUdpQZ63cnrW)7 zC3ee@b}KZpyXe`Po)DbtC=cGcr7fiykP+n2L&wO}-H_0Y&voD--^iEoMXh&`VT(Cp z2UC9OJ{o!YMvtU2(kEKs=ARi$4RTTONhWflgnSo$L)La@Te@?+4yiS+TU?BK!j@6O zI_XS2wb)15B%z&b3nJ)xIpeh1P>AiyRtR;s~EsASqGQu3ntv)o~5Fg)IlrFkn_O$7| zCSso+;ac2j=^OwV(|@*G^__~U6YPdK2x>mGJ2anuhlimgi+bp3`-W!>bLBu?qoc1G z*_v-m6@VgZy)khVFLJ{x;9EJDeVrnFx5cg{Zqx}M3bj2x3;1UF^Y=1Qy`@Qt$4=h} zpYF|_62XbT-x+qVwMgXPEI{Ohjgs;8Yai^VCQ3pd9MHaeDjj-ZkC;t^{%C{%3&XDC z(EhUh(IPnkM^MVGM@y?o6p_UH$>kxs0!!$3Lh_Mo?#`C&V3VNV$ ze=8hH%4Xg`ZrZWCGbZ^^g6XHs@Uv9Z`8RoiL}t!;Bp#+FYIV3<0k-ZpR%R7+0wV8x zV!oO1n`WBJyu94l2@Q(Co~FM&AQKu*%6v7~4mF#B&7Hu~)10AEuw5Fl;ZY&S0G3|| z6k^m>;5Xt8^ql+@g540=yyJ0u3pefMTJ5q%z@-oT2dvI7V~^Ee#L{2%V5~4V17aJ~ z;&u{)OE-!Rp`VW)nI4Z)Aztf1EalYVY~=Fe=hC0rf@3Ed-}Y0lDV(lI{kuE}LbI!! zD6BOfBVoe#7jUW`QjTFlG-&-HGkis&=vWV&$Kfs|$hk;A$5b0sW4plCC@bG<&VSd$ zHj+lR8E%ja8NG({afNHZtex3oATntrNRu;@SrhN=jihST$BI~kGxm6tDW2W+xdY?4 z^?!W#+>p`)dViG}P~!__bTiclh8~tzsjRIkcx`YMZq1jxJhNC}ljE17pTruy=4BKv zu&T>}J6$4M+uYBr@IQ&CEd}a*k4OVNL0v$_7ChdCKY1C@*6sZ-%UUx9;TJrb>l&#1p^D?3>T~Y#f&dji4-rtk8Fg#BH#kRV$+B!=zHD2dG>xgO#xQix2|h>o}pGe z6@wDKaKpgjm@KeR20&^|$Ekrm`V1umJ`2*JmFB_#*^e$6&{Uht<2V@jlAak){i3|` zY#8PF+8vF>>by?`;93CvnEy7tVJ-@o^m6z?Nm3tXXwb~ud*JbH6glj)4DzXyN)`xC z#TnWz*LpoV(!Iu*3J5&Wl@H4aDb5Q|4$N%qm}@)Ht6Po#AZDq}EAB^E?%+ z%-no-pyLx@d&BD$lIFU@Wzeyips>bqq3cW5%jkZyb!f+2spXRx3`sf~A+CKruDy3x z(Yew!57ixy;rFZ7<_DOrIfkr+B(%c670q93?niw(JYDbe-LYoB_saG2&_8v1otp0IVd_?DU%iC-g zrl<15000Wq$|AN14*=xRuK%d|ebYO+L?TZC!?)`#5sIz8={cTv%3S;L%p`!z*41lc zC(VW{?V2J|4^)^JVX`2ILOUq2&LPl-L+Vp6ooA(@3gyghdu+BjEN$(&(1-Llq=v36 zqMVa4b0?;TPn9P7grB+pCy#~niyTva{9xB4OjWM4hlDwW0~r}rY`PI$Ty9y@#mQ1q z`PbWrOw`>O>ub*vXYweufUb^C4*kJBApy?fMT?40E2%%HES3~e-OM}gv~?H;cSB*! zw#g+>&BJ){-@c0A-vzSs3P>0~n9On;o?wImmd&0vhHaQ*bQ-|Q7}sRGTqD=T3`k0< z?u?Il)E+09X|#hidgYmMXviS&j@eJV+Fxi$KSa74v8z! z_Y!-7tt60An__VSw#rAqI?Pngr>q|O@5m`I( zla$GMc$`QD3E;V0w$z;t>d~TMDQ~922bHtO)rrDtG2JgSFqaK=%U?v-Z z>!KAs1*r_?u^^*6hJb=r8nqT(_#PfE%b`gLcODTQp47ncsH-#y@~#>D0HejkqBJ^G z-;FeeO!q-nGNn|?phcn2-lhGT*Afn6I&QEByn^~1)!xPiQEl`uWUr{kHyIfG_uBss zq1*ooa&D~}#jlC_Wxp4XETny9+|q=^@}@#@%)IRR^`6^?q&7ZRCfGwHi@0yB`!8RQ zpi%#>9qAEMSjEjR%?gr|RDIT0ay3MnPZ*Jdb*U*z6rNgK-Gj`d5xPC}MdlEimBm_8N@kaeRc{r+5-W zdDda_N%0WJr7z}}=+xR{;1^m=CIR4a^la5x_!^CPz1|1pNfbuZ%JB*F$Ib+{Vg^=V zn$w0vhrc-UuqZ_&LVvP3CLV<@dxwzTl-z838pw#Z>{_Ri(TgZ@lb1eyyp=ECdzSs9 zOYR6MGEL!$q4cfOsovIfAywyF_M6icaJR&sU5nUhduY^^3oW&!v<+haD2?n%Txlvj z0_HLeV^AMQ!NIR+rsX^Re5XV&J#06*`4K0))87!ug(o=TaAnQ8KF z1X1`#tLWIzw|br8lK^jU%v|XkyT!MK$i&TYZ++sj$O=puEl1EIS?lhcAyR!|Fx4o4 zbc|&n(m0q`Zbo=(iN~sJTs0GT>y;5?J{=F{A>6>bzb&}3)KLuTKLsmiHYJaNLGL%^ z#LIV8W8)Srwq~eieL&pS)*9JL38H$9Gp<{z{_}@@LAbW&liPb&K^g$tm(hlCkh8FR z_Y!jD(my*WZM}c;;R{wts%@fz;Z}C#eooX$r%u?k{D) zf;{t8Z28_sn1zBR9$*SomkVmj(g5CfrFK~y`3y>7;8ZaQ!V;pwyKYoH>AU)lSV&)1J{D^0mpG3+xt6*_!G^1GJ6UJqs?p@Y^Vsp z)KH4JY)CIQ_$wdJWus!UbrOD4jCwq$*9}!u%GTpg@7b$5 zA+AmY7OlNzw9J?3d`&W5^VZrrtxFSsJ%QD|!mmuFH8yoD-TJ(6du{(TbyBkT5hfl7 zob>Mrn>@TqlbLPa5cYSR(Y>9Ye51nB8I4ah?mSACzuc+`44JV;R5N_NHQn6AesU`n zbu}MxO3KQ{v%C1qjpx2|Wh0*KEM2|DCovBm+8L%=qn|h2SwePiiCqI-OMWI72dlmI zKueb)w7LyU92Z{r4=_Q2PyI32xH+B8rFp%nPq%>kv{pb8NWUYJCOK3dFkLCds;I> z*Z2DCuF+!?v3{to5^?aBei5oBb@pE_b+km(4midMzET(!CJ&wIt;PE*PE&wWRMoxn zfbqQ8ysY{{lx-eh1jcBSXRBlD6RaSNj=FR-?eTL}*vCV;XE{n*lX{=IqNKWJo+w|w z-bmnV|Fy9Sw&&7bv*lLIBX2=7cUi{AmesuIS)ef5Zp@+VHW1zOdaL&@LGAE~oLsu# zPexY0yQ`e`T_Zjz;>-8(i3Et+WKl}>YYxc1!!3Mhb8dEZM)dNi5%a7L$$J<~vd5A7-Ausj+3D~h;} z2={WLKKM$~_KG{YQZ??oIdxG+ozT>6W-kI?JfHNjj%`huq8}$Jn31zsGsuYTuM)(9 z(HM~JK21T8LdCFnqb6o$f|ng_z3Jg#N15F8q0XWKbI&`f_Xw)kHl%1g*stRj3HN#C)?bBWg~EpegA>nOQ;T^a{OGrjA(G z44Z1`tu%TI3Guu4%R#Gro?>YxOFOD-dP|E}Y0IKFu6CXlCl0bC9dkEKkDqPZlu_ZDTHES1SP@ z4mbCvsc0|5XL|wko{Cw@rCYlh-DrS~xqDPX9tz`5qOGf|@T`=;Xnd)B7B!45Jnnn% z@V0itP!LM{8(3yhpH<=st8gY!5wEL;JE63C>9uefwbWOBhE}*r*X!_p-Izv|BffZ1 zRKO%{8QQ5AHIA{o%OA3cS66S^nJwVq*J$_VBxb=p1ycE>b7C-!JVPE^I-AOuG7`=( zjM&$a65G+_mg}_PrVruN$mpoZz%cjl>V|5_!=x47IK1t!%^(XbxB#Dji>q@SIV|T9 zuirN`CM4aHlbzhPE;RK1X8rDJs0=B4X!XJRfbrBJCSu*TB5lzzPUn`>0x{eGE&DKf zhT~AE7ZSQCG-I9{JEKNR-_E%Anl0a1r_q}RWa?+Q`Psx6gI;(1*0}q^#*1Y|F8;+u zg8zf&BSd-Ttd$OaFG@oOWj|$cv%Ietb}dfR&b8b;|Cm@#SXp#Z0f)&KEJYdO?Ahc! z_X=DV^A=^b!qOV9=xsCw?=b{(XOKRNzUVEj9?#dL_`_d36IA>!0u-SnR#($GmYair zc%P-{%`Utrh7Mny%cJP%*;1B(O-GXJ|Gi?&_RzRu|aPA+>^3BvQ2<`wl5RP`#ml` z!MbD}qiJKP^GgM8zs|(DU4Gka%mmR{%-2oI<)I5>!{GRzW!I$8G|{4BN0Bu-qDE*d zN{qv_Z+fLe3#*2?^z*?UYX%FDq*qRJV*fxLSqwI3U*zQURblJjP6U@J1`j%AG6_{` zdz;wy@i&Lx_Q=XQ4GZ4qa(i(zq>}3SMV%e7`YLn{Bvi$buFe(d?g$>d7BUA*@L$Y^hXm^POhH!OABum754y@NzXnCu_;ECWF~a)nLQPk(~pEa2{0-e(EUH^^GFXohv1`)?`?K|a8O4W$%W^0LqJd! zkJ3dl<;0H~Oh&wjTo_q0MRh0%UpiUmM93cut58^_fIlM$WN*OX{3caCGfI_{Nhhyy zI8nwQ=Rs<fxG4m+$7mw3az6I7?@ng-M_Pe5^gwZP({q8O zfcNgX$7zi`QWG|BvP9zwW*N0raROxNNk%;zaia;7dj(|0Lq&L*V)!i1p+lDIw)Fdd z{tE0H;$K*D%=BqhLEMSs9vl)vsO`W_hJhmsIMLK6_gUfN>ghi1+wU88!2t&hR88TX z$pw9CQ8Giy5M9!kC0Mr*=x(J)+*r{^Xa^M&y!Rb>m0$M6>MbQNvj~@46LJ)9-_9i; z6SjmRs}w3rsT|anE+i|w7i4b<@8;UUt%>=;Lk~7tC(bVzxWAJiyHH}cq7)8Fk#p{H zXLp~{a^HS#uoLN7QokK8ucrd5oy z4@jamIK1KB@X&Vx>)y8ah&5Ofm~_9Bx^qqVqVG54TA1BdGwd9???{VpiZnxY)~#=s@*~133$bq=xY@pT*#$v*;D}a28QDtZ2Mi{T zZfJ|Vtr;8E=hk?Kp(tXe_LiT@v0eG<1-#D2DTG84V*=n)3sS zQNZ<-AiC1>JtfU2MOd>&@O$$WIXz4gkBkG&3u95)M=T>)LKj=8JrElnPJ1#!Se6eL zoEp60Lf!MbQ)sHER>P>eB3}_o?YG9r`t1b2gFoK`IA@E0nf_^=WrO}WPu_;j^NZHN zeFGu$ZV9Tor9NZD3V{i}@e&QRJgPD>e~*R&^N_Nny}&G4>@C&2CC3w1WaxP92Zj@A zY4UBSZlhC1mai2WU`t_E*?4QE&vXa)sB@MB674f=R;yd{!M$Q?>d66U zm|Mq6NTcNkfn;w%v#_KFQH6liG$K$Vtxs9eB|2vfkaK%0uwa8}LF0VJpMK9jXCW8T@YZG3XUENA%ywc<(4oW+9`!}Ap4I-& zAVJTAqrTkWMH~3Da2$)|aLO!Gps89V*-4R}PNqicpsStHsQL{8k$;8QG)>KmZe z?VZWM9Tkk?(G{v{kLBMo)V0RJo^Jv`?t|B0%cTbuiHV>^3s3AFc**f1tN;f0z z4PQ>9P-$$-$>ghadf7&LB#eRkaPB^?E~2)wYnO-?0yq}QI+`MW#29TBt*6rZryrPJ zH|?JA$yc_KZQC%Y&ejt`uR#UN+&L{aZ>1_9)O}*LKA{&{;%&~*H#M0LDC3E;F6Z|R z0s_s?FjuFbRL?6Z7e{rY9Ym(uAq?;CFti6?7_kLZ`s|pq=xzAvXxM51ae|bN1wzn9mwlV(nqTqPZ)F0Go zjc17@%LVnBXUN7IMb>3j7AHdOo=`Ovk2ySR{VIL-$a! z^M!ut*2G%fDLp05Rv0am|IP+1mxX9P8T3;{a9@thscYrgO>*B>IOc`3Fr)gM?5D^< z=)V`+?=LzG-c_VcPCZGjrFFvp0CzavwM?&fv@2RnyRQ<;;EZ&!qP|cqHo(SNTvz6B zD_{(a(m#*+U+iSl`CzaIQ`$90US2&C3t*YLy^5~MC*F4sIkAE zT@<^rEL^WdZ=^bR;eT;UHU5J-Ysz|bjK;m;_MXpF9>)=60%*}guB$`tsAtx*ZEDkXZ@ETsX-Xv)kcY){2Rmg!Q z#s_0!5S`cAtb~x)&Fs&7H0Kbp#wQ8|VB=kt#HK!{lv088DraaY0}(>K_jXYG8`De}1z0MP*csd?n?;e`fV0j^O(!rh`+^ypkU8Ke=aDbj6Y-N^j;0SoyJH z8~kF~zD(Im7~%5>QDfLB zCMMQ{6;h_x$hrGUZ4wOR+1M$+_8#&$AIbh`aSK22Ipz5jzoR}!iBUf%JGEw&k{}-% z_wl#ycN-?&WsPHf-o1I${i;^I{?9qd4tPhjb>xOEG=-w}R*Q7=eHE8Q5v&RggG;(! zHW=^;zUPIItC9H_m60Ec|Ly(3VLm499-822-L&fXItueC)3==Fe(l+2uTO9H$goxN za~OYA;f$(D>e`6??ilJtWAy+x?1211S}_}TR0cw&iY~2vXn|Pc5)b2-oEHIA-TzX^ zqNx9kXe#hgn zp}aOen)BirU2GrEzV>cL;sT5`YwBp{fGqZ@fAkwbas*)LG=h&^>&9Ub`VKQHP{!AL zIl^Jt+&dhPcu|`{L*P=AZF+Rr!c6i3v@E^MCN4>4*G*Rf76fXEC}|$z{~4T&V|%iN zgui$vmS!4aG%Z!^{Q&9Ky~^}dHi)eQaskn|d6~N&HSF-nwWvLa;1cIpv_XznqpF1u zr-Cz%>zKo;Q&fB!J`WRn-H-Ke+|9UpEEm!;1#gq%?ZtQ6C~(*>GO!bF!q54YYh!`> zfI0K*#DV}B7Wuf}(SL8tC%`q+BC+mv-@o{d{3|O%pNzW8nZKal^SVt}-uU!JqPpMR zFGK$}uGNP9x}5XZN6?jRul8%9;ek<6Vfd!-n#rGdZaVNQz$1aRH#}*y4;_XX<%c%0 z>n<}d8(TDc*%KLpA49I%H-{bQ_|;Bk2}%eXJw7geJ1z!Vn~x|iy1OV0|M-=^WxNZ~acbxDazA>OkTr#;ke|eeCui0Uv^NGZx}4=|tU3 zjM<^}{)f_MyCuKAXTcE{7@GcLROHvMueIyLskY13gbSVo;zT^|Y@D_FSWXQ(wivP` zX2xEhw?i@2SWw}T(^#vOsDGYv3s3@Z6<r^1?rV+!LIJO0n2Bf~#7Hm3hyR{Fong7$x`bS>m90>yu*G+}#JFc1_)wSAcW zz|Kw(IC(qQ05FJK*goCC(e7K1FOTk9-R-Xe%dDheA1EA_BM^d3{L=wEt~PP;NrPg*7(Xfzbp*tTp_I=c?qfme!ZKO0mIFK7t=oEv;sC$%s?LtBEfK5*f;wcG+fY9MpC zhPD@w^dK5}^%O!nJ4!(3Kc*Co-#g)7zWDb);r%~kk3YBn14~~Z{MpSt^(jrYZ*Ecn z-|+i^1O9r90iXc-=`iXy{T;$J18;f$x&i*(RsesBrCbt7scGde^$q>yqqntrD+Ood zg?!($DTU-ByT4)B$SYQ8P z@xJj9#P@vkrTvjD?Ts{5ic?!zQ2hH{^Lnx4TVearVdlZ*_y5Zi7K1eW#gFP!Cm|ug z1F$o7)nx@>BFBE)YVhK6|JJ_l{gWB(i&+0_Q3dK`58wd+v!=1OzV>hX^ZWg6Vfna~tNq)(C5Q(fhcR)$GbF>X&PmO< zRSe4GVka}1-^9?}J)=;h?n+>Ki=n_=J`k3hvKCo9iGBOT+aD-8r!3 z*4nH^nYZALpeOZ_1N<+ufc;-)fuPD^;_KNchvKeDW5k8s2aok;XNG~r<>=5X7#`Us zTUOl6?fEy^7W9{0xedX4dxP3pv1s*K?2aRt_EioquYEM{}V`z zP~sxaj6rg4O3snfntBTVH)S!e-k#R_x?N z)SO|AaF&8n=}~x*OFIvvP|Mck#$Wo;)r5_6I)9 zIo!c=^$j?)D{jp)4n#DhVV!u@_^THI3Hos}fE9FYg>2-qw~uvC%mo`gpL6_M%B%_) z^JJ0fHcwzI)x(P^Z&hY~mM!YDM8;M2!G>-nXX3qAF4{m-<9cU~0cWr_R!QcD>Ekpp z-AJaOl6S$;VKq&uA8k=!GJb1%Y}4a7Sii%;q#{cbn7i4t4Z&z8k0iQj8t5X!O?SF5 zKzNX9m8^N=)xo%bt`r5%qnK9nN|8yY@!~rRW8uy>kBIQi`q+N^xOWJuJeiHSTaZf_ zfED8UN##|HVg;*&I~=_V#3SId4PWlL`0l0Rrbvem0=9~8+BQ%5;!5Afr z&3VSm;a#fY0)x0pSwxc#FKpQfwhXcDu~s?4v%Q=%4p`lK+o`2z1>gBDb4iDYlpsiul*CDM+xaJvcYXl{k=jjrTXu?- z7o~TD(X>X_ySW3#j>=7S>*Li|QmQD`Z+DYITJ340yr_)H-6CgYGjxMGi=%zn>DQSb zi5k!IbmkN?xXIiKf(>C(Dk(U_^s-{ddof`d7;dug>tK1^U&N{5@(UM@km0CS!cTv? z@&24U%Urco<=$HBXt-iIX>IKuP*+Hd2dl+)b?UB<`fvQQL&;Ir+iodYWZG(DgLacj z`UV5JM}iyDjr!>ruC4c$uv`U4{1mXd#OdxqS-&do1YQKmPlLo|BJ#H1b`|tqm|<{ZM5FVz zKl&|J%H_0;<07;PF>akYHRT${#prUh`f6qL+pW|LT*5Jomip@qqwCJ%&OpO}%G6FE z?}1NO)TMH38LTyptvY>XSAH8Ftr4)_y za^v#mKN~~&q<|0Mll>q;G8<@H&7JD{rHvqbBZ{t#2a1Vw8=HrhlE) z*R%8C={t!7r4|Y7VI*-{q&=vW;ZkSkk#r81>l2PQJJ&)NPSokW2nHWN1N47K3OX&2 z!Ka2`G|ah1%TggQ1tSd|PU1~n+021i;0zOKq5mM|owJW>wk*8rE@5kw6A@WK`h@ZkiehcA9RkMcNNYUnpei){t_*NLyr?Z*T2hA8!QhWCaP{$0+e z$Tjh}kUihJ3atd{VAYU06;b15O?bSNezQn^$$m-erq@kA=Bbcz201(ZbepmPuiTy7 z5W?g4ThcEAs&-6j0;O{lU4-5JaQSX_mCvoyQ9>t)6v#NgYSyE6JLF5wW; z5;59gr>#ii)$w(Tp>_zZ3C7QkK4jobi09F6PbVuJoAFyF_7MHYX>D2U;5I(})?!ZC zk8EOPefgQK+Hiq%Qgt;cv9F?j21hTsnci=QxAzXvt_p;XAE{HWBSGdbjJD?AFNBjz zter|EWXcrqqZ2ZL@2N9999Rmb;Hd4ClgwYY|DGPA!)e3ngo^+c$H}&}n-6##{0m>C zYH2YU+UkwHIa|FHQ!~q)_14d}P1HCSANTc%4Vu3v+k#n4o#5^7(9n6?U4xVJo$L_V z1EZ)vn5S4p-UyGErXW&OfmroS>9b)iNG<>S(%X``zCQL@H4IFUq^W3JS^1G~sO{N} z0|4lEE=HZNYDZ^t2z|HQGZA(~=jKHkUKw>B`wr?P*ayl3>ig9Sm;b)Wd2jqF)GpX4;K1F| zOv;arX*9*bVK!$?rey~gFAXN3s@AO>p@rxtPe^&z^(6SDAELLCFyd-Fq+vQ@$SjtT zK9cLf(cSLmkz|>7o9T6Ocf?lAGF~Zw#w=d-5&hyQZF)pI_N?`GGzUq0IyHT_S2b0K zI9MVssq!){$nDd~OyVw))%5&M*e z-zPj+ik|-5MU-V^_ZWH&Rrq%b%f9{)uAlrW>Nz}MgB1dhqX~-YHQ-VY@Xsmh@}jrc znW9L za|M*_rZ6UE5;8Hf*946>HsWVz&J2VVjv}BNsgXO1t0I^Cz0QC;Nrh7Dqb4$tR=a&t zJ2oQEolQQg#UB%5XW)9g0wc!On>>bwF(A^Gg#n?OC2thN_2e=<@NX`D<&U0?hYMVp zdXno*$`{jpP7x>Lwz6(ZaW+ny&;nTgL2=~Zym}-2vb*AihGw#;ZUyjuck;XK3;{G(|?c4 zILny;`)NK)%v5s+D&0}1*)-?XI3$HNm=TvL-Pl$L)z+xIQg%%DehfTS)bHzAR?=x8 z>C`OCzQ%vs7%x1x6zmOU(=lyuzVPpTjmDKW)6DZ+z|JasN^agpKEq6j9GLCs+wj`` z9;rGJp?Ux0uXAXIbaKM`Y)2zciu2XQGZeFH<)3W8TQ4l(<*!9+zH0XaWTL)BaQ<7E z=&zFubwLW77lWii-!Mz7OLRqXX4uGEBV<&SW>mBgmbmcB3ppeBkS6$gN$eU<_+WEw zcFWcG(G(*mH~}DG8b{f>IF^<2csCSs4%y5p5n%T3Kg&6aMs9t0dJ-hZNdzrZ+OwyvQ0FXq7LcIdhsmJgdXmo0jvAn%-l3_waGL74N0R z9bS4YCQ86CD+^3WKT)!3LLWLIw@fnJTd{p+H}MffB^PuJLpjBJV}`=vZnYqf7-@8K zz1x_Bw9VK|UTf7BGWJ=B7W!$yhUf8(4=gcKDr3miX8#Sh|6Ko2Q^=0@d0Pligvegrxhtz50Rn4ad``am#DnmIUm+mLuAc zrW>t7YqXW_$oW_9hH}=Ru4^9EUpf(11F#-D`4mk3GjoG(&jko%vky^MLsU72x4RGFu92GbX2cnW*e$SH1IiA0BdB z_IAvX=7va{b*k>aIN%y~P$#T{I(>kmqSE5e4W!CrFEF9px*49vPyBUqg^}lS>>2HQ zZ~q`WO(RADxd{r#mPxg6FfbYqDKwA3qHyn!KuQYho!bzUuNM?B>Myk*4T4R=-e5>$ zSZV^M8lt8*VG}dxf9SJ|9;<`YU1rQaqOXtnKIB23bhQ{G(YJ)wtY<9l#D86>Vqga^ zP4IXFuY|w44)!(N9uLnAaQ*9T^8w#LCZdZGBffv9IBn?S3;~SVziP?Q#0%~fr@Ta4T9bk)Wu{I<=CT!wo__8H4jB?&a<5{q3+GKLr=+7=<^#s+fu zo6;_*l4odUvH>*7drYr6)ABVz??5*7(hsGpQYE2uOWB+(LSBWUpQHGe_G6hb-mC1* zvdO{@Lqa{iy#Mvj_i7Sm?5i4#iQ@V@LP$z>rtx3J9(}!n>q66E%1X1*R-d323SHiT zghTr2LD2RaC71>J0dg++{%p5QKT`!OLG95yx}95bpPixgMmr?mYmGaQq-}uNifGw| zCdX*mLtJ-gdI*4>4{zhw3|`WiD@Y1C4#mNk0I#O1S$59$gM2S%rrG0%ej74Kug7wk zD7wP=L;Oi}o0_#&WsP0edGl=|a3r@}nPHN1neLK7486kTHTFVe2^`TY_$;|SS^2hr zynHN(kFJX!Nz?6gGwrlcGU1Y7F4{z&9V0BN$j*Y#LDi=rtFXzb_Sd}T=W)PH1iux( zV_b%^#33*%Sk($zwoiuqz^?P2Vs-Do0-^)2H7O7nAVEpwZiSFB_#ht^9V3rSAS1@)QS!kOL>_CF@JQ6AyJ#=v$LY3x0j;m zL`W6^9D^3F7D9KN#p!MOsNRgAX+w8GkE^5&h}vnOj@mFnK8*%GFB;OyrbbAhs)I+MoIBYR<0ZSa` zPzaW>xfbD7&DElaL3!vXs{R5)`eyyqornIBy#F)zLFbD0p!i~hR6 z$K@r^Vxyu_@ay1H%K1{^K)=NjB$Z3^KLmaQJT&w%|6+8S}KJQ zQbO{B)c;0(o-~*KBkA3Y#_MS{E&2WE5heM@Bl1#S3a>sX)57(FJdjcL+B9IJOD`~o z5(0@QjXmQy+G}P9npo7@%8m41Lpv{tCuoV(en2mI81$oS*J!nmi2)TL9UuL-LNebm{cT{GpKn)AA2NlDyKHv2`P z)9{C1-@f5KGJ8E^VvDHB^uzguwZ9ksrNMBmeCn8tv6@q6Fe!T7+r!r0Wzl7Ny#qjtc!Mk_rHY_)^pLdNt&^ zeXgj9W!#t;;=#a&#I?^TLPQ{$uml?q!@E)rO3~>tsTal*adJ_RSjEP(scbM2VOa2A zDFgycRC=;+LuvwM%}i5Y3^I-0jZqRCGxr;G4Q7S_|Ss%f0N7TE;MN% z`;jS84Ww}7+HjoC1+^}~=p76(WtQo>{2(zLBXPvOZlr8ybwPRILpbWBS2^>`uW zx`>+0dm>|$EmKS-bk`Y*9&vk6bW(IuNpB^@xrC1zAc;Fl|+GcoPiCAJ+TFQ z!bqu^teOhih&WfedVpU-mEYIs+^FKoT?C5bt*^mPP871}D*ldYRC55Mi_!~RX9em;fY0(TTcoRer*XCZ?Emcs3IXS#}|l^rDBn+OSt zpHER!H#@7hJ=|Z-{R+r%1VqeoXo?jXn2fqU&;dJ9%_FwtrAL!pCT=XrMagx0`#=jH z{@Pef;tD^nD9#DseN3i#9B=?LP5Ks=nqD)^9}mDMPOD@~xNlKfadxptz!(U|fF1Zo z;rn5=7?wzVkR)e`Y-7*K!#Fv0O!ClK@5{FXU8{Peir5|IZMvYeYYOWBr5ac1cG6GU1jbbH@OJp?fw^d@5KQsS8?)2t=2zEO#3=%oxU zp#lriOaF)Ru~eQp!5|%U>+K)WO$azgGZf3%N*P@-UPX|1G$_cAGx$u6zKWxbBxVm! zf#fK%@&hmu2?Le--=|r8$jHUq#$O9%y<^f|MMgy1k(IjyoKEC5c}DUDrjhKwXu4T) zZIW(Nd7&Bi=+}DwiZk6fE_Gn;sK6x)g8t)AL^7+nhGOG{P~7f8!&7akNpBxEkLY9B z9`i?)kjyXVf$u^IPfyYE5YtrS#r%=&JY|(Y4Iv-nMSQtoWz1omgy=s{B`PaFLv;AJ z5!Vp~OJzGCN=1ET%vO8G$#a!#;4~C$s@;8AI|4Pf3}AM80^NUn1EGpGc1A?OWOc%+ z--Hw?AIIpD7MaAW9R`MA#iOWgdWjmlLkTKi1HtCoX~c{jhbVQbyjgdoR$^ixDp}gQ zK_Kmh);xizP5Sna383u%Voj!?gH4FI9guF`B--{?2jjnYUT&qmt;QbN=mvm@kywFT zkUxvSo$0QwG6lN&eYz@v>GzF@Ql<8|vgQes9*i6oPexsqCqoig_mBjKh=u9G+Y_GY zXH8TXrrEEp4x*ESqKUVCguBea=c*PIY%wTw1YyFi=$XK@D9^T{@9-^F`8><|lBn&6 zShp|WAm*o!Ru!z6^XSX%wAqwdp;JXwAJuLi7Y3D{9J~>YX~Vpz4)iWqO9Ydw>`gv% zXQIZG)d{bsOSGU{pyeV=e3hAKgtg2}YwV;O`FI=Z6prO-otqWYFhc%TIP{Po~1TTva%dPz*H+?E?6j|lLqW#JWPKAv!WN>@N2p8}V z$TYWNy`6DBJ_vge#byXO@4N!;4+70wBYNxPpJkF!WdrpqN^Q)vw3Q7MB?l7$43xL; zO1VK-DT5X>_{`&HEGwDMtG|0aN=@>rM9nysZU!G=k31=pOXfSgyx@Kk;0msHcDwwm zqD*a;HcVT30yDVxR%UrE0!yjDN^SGvyt1BU&Vl5=y@TpH=p|QYP z1Q^8u1+&%6=+Z@}*jx5m+*6Xaxb~rQW6X)WpMLh1s5ZimL0iPgTo}h87a|45jG`2h z?c~Cpf9}TVgW3A+`~PC>9HKL619cnQw$ZWeblkCR+qToOZQHhO+qRwW_T;X0&RYNA zjH+t>)}(6h{X_^^Ei>0_eQfL;9EqR}5J~YU2_vTrbRm(%m6a{<5jUzWL=F`DYmudG-4UG+x5P`xEp~kYjFWu^XWoP2BL1 zW#BN0RmABRy`pDa0pfz4Exl!+Q3}`geJAb+A2%UA*wD5B*t1pM0Cs&JZ$u;2{o>p6 zCc0TwirElbNey^mYq1y{{#%&u>qu`&+;+NH#VxB+fYv^nsHfY5U%XHvcW5a?J0>Yl zQHz)QDLfnF)TsN*lR!~Y#bO4&XQ7-n&XO8fUY>v9uW;(kK^J=lT+3R_`Jed=Pxv_D zx5q9REVJtb>@lBgFuOddkf_Q939((1$V>ZCCpEvy7%%TBD-cD==h7`i?Z(bunz=8vw3CGfli;VVV z17asvHcS%;2-r#}5*JAEElMf?6QLXTM6Arq8NdIIgKzJe*iNnG`cyu;ApH(NyQa$O zI0kyMIx4qmNTRFs9_sVIrIIO3GOJAS`#2Tj0r9ZazD=<*MN5 zQ!L%`+HzH9LB~{(k-5q+jg=)nn)wKL-a`*mq{+X)2b0I{%&^3RYc@F%=N4w~nvpea zp?V^G2v~Ms=IT9reV*jjB#j4+bSq16W-vZrxMym&Aoa7N1Lt!=^s$gbo?LDdTMP!{ zzH8YaM~Te4GkBx{-W&>61&`sdXUYK{ehBAvbf)${^6EXBU62n)X>Fzr#z(gaG(nxP z7rg@2ePOZE1tAy+HX#J9O=-qRKN_UN8~vq^cp!p(!9(ykd*Hl%WjP>Tr}Fd;r&b84tGIy8~}U%|L%y``@y|w?b5n zHhw7WdUlb`eF!QGI4O(Ii3W7jW}oFf?L8Rty;-?>Urgsss&&-|Jq9uzhrfFAwIvFv zp>4G%k;lq1JbcEq3P}ONqGq1VY~PeH#|J4hS4$}`*&q6Av2**`6nUlmz?o@9Yj3#g zP5n;sJuPdzFrP@tW;{CbDHSJssq3eq-~JTCYt`xHn@_sxw<)7(3(q&d!W;xGr>Chc zaco>}yEmh`_|L9IsXvJAW->($%SiXSqnR7NJ#!y>9$-gHM%+yEH;)i6}%<_+UhdXm+bkLp9!I&J(U)c(+z96(@hc zFFuQdUw)6goFcegL$J^rmzvuaDek$}@1N2m;2~vYN69E1<)(hSX#*Xk#ggeoU3n6x ze`U?H_vSqW*~eIbvemx0o}~{HLbV~w4%ki`^;F@mN@I-}4&+mYG|qG9zN&a#IcsDF z$6kr2Sit+3@=eak3=^V|A~XK-?8K8SNG+5fIGwR7Z6z^2tz*&#!yvAADG$WmSM zXO+Y{t~=!=k?MbdbWtJm`qCCa-y3LF(Agdqnr{sf@+GXd^mHMkPEPhETZ<44KeOS)bXrn3nXJ z(i$qf;lzZFASYd(Cte#j*W)=+Y=hDvy6AJG8wE>`xq5bElTgwKqcJcsjQpZM;x_xA z`*Z4)^g4`E@V7noE8RxC<%WMB9j@1>>EP+J!EqCpu(!ge*vKQAs7gn;rS`}$d<6u> z;C^AYGE_P(qde(-$6Ut)19cZ`_&f)tsuJ^j^=gH6ux0tc36+LSy7qeuYpi!49<$w9 z@&0j8bFA&_BtJFvTt2?2h81J(i&4gDjm)2Is8$QJZ}GFdMWvvOh(ZzCYgL!U|7vCy zsRW)Ys)Hue*3>FD;*pYz&rf#cm5C~A%3-E-iT-5?6io;sOr^(UooC7#Ap*$idW#lt zFRZQ;qjj85n;bv9=*)Qfid~lIgM?y=i&-WVIr8%QFUx-A;ol;9rD&x*xI+tPS%`_U zB~L20KsD3UALJ6lxej>OzESu*C-a5zL@*WPH=bqp%9UNR5eQYRcG0%4r_jKG1B zu0hUYhM?`{Bo#^lvU}Z~D4;A`nCa#hu=E-k-CWg4@t7wm`)PmFCzh+6d5(mL?x(-& zA4waPL;|nP9ld8smJE5VQnQ=egI-?^Ko?7YbrE(Ld5G(q_b@^#?=$}L!>ie1b{a$e zyXg;&X!?{s;VQJ)UdPZ&qWmJ%PB*1w)Ex)&MiimSPw`boKSadSr6w1T8~S|nOEQQs z|ED$CtrclCA6d6WsG}>%YOdW=?3_1uBkk}tY+Tt8YD8zxVBr>y|L<(Me_nz=q}h(K&bN4sL<6~hTM#AGbdj<=VLAX;g;!Mb4V2A z`h7@vgip8hBiF4d@ogMq=~r-d^LNIria<`7owB55LtU>{K8IBklbU~)FIFnbVt#4?I$)GE18s0p_h*2pl|@n)+;NV)g~%axlSJ)PYv4T?P+C_OCF znSaS}LJ?_O!#Ah=)r{~=LkkdKao&koas=Hj`EEbo!IdBjCF1o62v+cd4VYotdJj6R9$=unt0dc?D*2Yn%~Qb*OWj!EPiF&M>!{1BsYwNe1=Z~0ngAi(FAT(2M=pp12V(C4%n1$- zK5q`JI77dKq-BNs%U?)bDKW6Lm#70d1e(3 zs4v98g7e?-2UcxC(DzTu=m-d`!1EW9Re>aebOrhCj{xN21`DZ$B)E4;v2Sj8>`De3N%E`zpntUGu2xLpUk7!ekqYe}5>LK+n_#Q|o=SD|+cMqubHqZ)E z9_mvwq`aAbHR@RZ_5%Y@ts~88FL1kHmL46Ro=a{2QucP9Em3$iEZSX$ijL+BA!k0j z;F(_gwzb|ekXfbLpFd`CpL*oq)n4$A6(Jxt*;_#Jb(l}J+8Fx&#nqXSpmU#WuU3qh z;n%y2Tie#zRe%tGa3EkUP~sE>eU8>u#@c$o#Z2!7*jMUB06jgzj-M9~z~(s@%R1jy zACiWhRkmg|i|EYC)=G<#^`+NTw10d8yKnp+vAwu+Qow~ke{2`t@Xg8;OHT-hE;}2q z3phoehSgQY?956EXb=!((gNU10AT+5(IbBI3)uk(I0CW@0ge#>zuU*&uoM?JdwFo~ z9)vytSGYZwaiDCVS}%w^xHqf$Hl80ndO>k5$Xh__Vz1U^03bkhVZHxh*9As?&*~3Z zR+XN`zW(7`*xZWD$_lJ?xh*XCI#88ALhbj5R?VR?bSUQsw_f`-=f9l zTHsao4~hRPA$48iaqpmC@rB*pRmKK&LITbdVA}C}Q~uipDsN+lzIN@W5DndI62SX~ zE~dD!4!buwF-{4je|)m<-R)KD5BARa1;|YDgFN_J)c}VIS@Q&r(!D&{`|A7vxc|5? zzqc|n%MzJQ_Kt|b!ZnR>xjxG+W8pyEM@ApzRJ0?stu&iW)W}GK~qoZL`$v% z5I`V5URduW`O^j1jLd*vm~~(U9$le2Euh6l@^!0j&zl~o1BnU4ZeBjZLrW~~QBmI2 zVL8B#pHi5@HwuZMVoyfES%99(=?Qw#D}fS0=IaWtIBct$!goXM?=rEP`WSNTC^8sCl&YTJ3S z6^5P=g~ThyIQEn-088L*70{$!1W*Ax&LWX5$v2tFHc#SbWabl>;F7bS58Jmk=_?Sx z=SPH;{F35iig1tz7#(MXAngmz!!M_nCTkO~R77-(@+jM}4lr#FYjj0jC$oOjT*9mz zV=649w#yr~?0cy9aKR_{t-%5v7%*UhonHDYW z=O`UY6tD4>TGCcq&D(m;RB`So+%Y&bSrpObrX(j*W*@k{6kS!!8exy8)m{^r-hR)Z zmc3+E6qY$NN6sDqVr)KO)*6IS$x)^wdrfKxyC~}wTgkyl;Rh$6OV0jbLaAJ4(EbW-K_NZ3JLVRc3*63Rt(YQu5o;pAyY@> zx&*8;B;_Zf>Lk^OgrXytb5D+bG-##^$#3N9P+L4jlQyp0ZkfGB z*=szy|0`StxuB)xXC!KN!9oc@3Jh$DiEJO3eu&Z3zuZh)P;lZ1hhqwgOa~^DY zGt}ayv{2Q{BC#>x3Sbc!6@T8R0wU!pkzQzMXKxWRfhrd5hCI$z@P*!?veu@#CjtJE z8LE2?EsJl}fr#BZwMONJU*%O8f#}Mk^4WZh!Cm+|g7xWqz^KyaUPEjdm8DtwD}@QA zIlQljMf!z&^#(fqxBhlTjUR(NeKMj(F;(yQv6bBeEq_e*=i-x$PI)49ElU@_ALMj8 z!&7vEZVdt-d5*9=qa~l0ZZq(U5clB~R?KSAS5Sg)68y9yuzLmUI{M8K4(p z!9hV1ibc}7F)IGcGQnxtDEMHr8YlMEc}kCdJv7O{{+?4Z{GtR7L0`(fDmz&sHE!Tq zx9Mw#QimyQ3?ri*)R^=Kn^a#Ss45w5|_Vvz%Y$@vmTzxT;zl8l47Szd?ecB$;kr zN+>xlr2`(g*{c*{7m#!oT*rZ;!Pii^nt@v`-gdKxcITx(KcP`4VWWHTs!lD#=0<+n z?CzTmHL)*?%?X~7pXGQnN}W+l`G&TSS`ectBQ5k-tqopxIPj)Jw6 z%|q|zQj>0gX1FE9bB(Qq`q997JS)Q8iv=2@T7xN=naMat(CgR2{+RzEkiB>-XeHMH zcRU9t9QSH{$05tdNGOv0tju7#9CtWAXIlO^r(q$aa{6Cb{R|Ix<#;(Q7& z{5w2}dK-3XDZe--}8S(>AgkbySn^Y7EVz*A87thVej z3=D2a$Q@a9itaY8i0hKJ;OzlV`8Al%Mpx|*N5=*4dI;R(obc)#=Sl3j+)SVS&~E0~ z98>HwC9Sk=+Vw^wYO{9W*q=7W(t%5x4FrO=+hvA2Uu>_JynMPPw!+=|0GHk%$|D9a zVgAYfV1u34k0Z^sTEq&NWwGoh6hBt@NLgNyhKNLHFXWq7=uF~>;r#jV$zB%RHi zTC3*uw^ftiRMyfCS7(aqRnU~3mf8_UEDUrOk+HUeFR?KmWHwCxIQz-~%Ck^32NZ7h z6GMylG33B6Uo6H4_+lNYANPhU@7Yll$?~JT2+q|sP{P$65S9)BUc!Q~kh z@`2@^G+Yo+6M(7mus+Js)Um4ioehz1Ac~hV!=O`&(U+D{Kc-?Ohp&2pV#*OoLb&s* z2VFza1VT75M8hIm5ZygP$p)NJmZp%B-xUvr9M_Mg=(9G;e?%()4 z5#B>pZT)gRpCl3!r*oyK#3!S~le#HK{AmZs=%n!8!C3fdezr~9Ehq0{{dr^Y^pMDh zQ$2G1(8CpxiR5S@tI~r}yy1WyAAh*p^ev<3M$C38U(_EBiZahR81kcP0jY2bye# zuoI(i6}68&lX^Hanz~rLXQ}u{SAK|e>`L)@?|K6Hg8msfWqp5sE}_weoP!O3f zOt(h9%M1=ZA@vt-BGc>E`g!jxDWFfX7peH_si^^TJdl7d70A@djNb;Xc)z3uE)-mF zF!^&8ukApdk5WE7LlcKA@63{{af9>&^MFNLVekSMiHa;=Dq1dl_6I98JKcPz(QHMw{t$&hw8LzMsO;XW* zlT?1-9;|n_*FO=-r zLc_)ToXXI8oC2dxrER~CQEP`jo|NK}(JCT|n4xODiJ`q+bbaoTaO>BKXd13Qbud3T z>($gl%N!|q%iOLi4%Gf-N#QWTJZbDu3NBaPYBvy6t=9$Z&Y)&WpN(O3BrQm>PAcwH zxN%$3T}V#!bD=?fy?bb{j|W^kJ@B0D*4sgp*M!Yj2Um_du!IHt`AIGX3?%Mko?vGD zyH$7fF0AF24?RTCYT^HM)ZVBLf%CytSw(Ni753^JQF-E~{5g}LM=wUTUc)+$yZ=%y zsA!KZ4^y6?)+U}XfYZCwZpA|TG4rOEJ*D3q0_WnQ`xWo;9YtB1v+C@CG^F~<*``-w z$l2s!4DgJg1dH3Rl61q&Nqg4b`nx@&Z`+?ZNMQPl+*6ubiRxqHuGq|nIT&Q-g(0r| zt{{~nM9FjmugiPkpg`5HiWLU@0IGNHHBrI!c&H9q5i($ayVZm1t;kXOPp>?KD4)DM zx5>}}SqA}Au-SWNd78pBu0xTgoLGh4LN^~uLan5}DGyEUwa|iWXK=JD zrnSHpqB%VidQoME5izeS3eDk#v4svc^{V_Kv;CNzM0P=gctw)-c1xNqaxLiC8g$~0 zDeT0Hdf!6kl7=Du5p9*VhUQekW``At^DGr__|?9{&Ao$7ucWBC;qdp&UB0nK=37>@ z6*$$TY#Sg_#mdG;;c4aSuRgXduMH9sDI1{o9*0E=Ao>i~Sw4tb4bQ==!1vXK^H zG#a%L%+UkLlG)09efmU9A8nw~{e6dPI)vOM(dJsE;2;phJnHS)i!q^iFV?Eos>dBU z&-(!68T{(>I-RB%4FH;HKAWG*u!?I>Bm#fILxM+5hAY?i>Y%PvJVPQWBPo4Z+ zzAV3}stUP{XuezI2kWJ;fa)_>XGU0$L-yMKoCyCeUwg>pwDfqm%i9lyB@5fM(Qbvj zl}%p}(Qez^HS1x_u#`4|`L!W^eL{+a!n{NWrwtGRhEmRo`Bd)kB-rRWVOQtbH5u~6 zk~WWGe#^tW@xMv;c&9yrhN|(VQ8(IVAl6jBybBoe9vPCd`2Iqnq9e~ruo#`OEX)i? zn>sY3iQTD9x&StSg1EFX*|}JihA8D4sOLtxKvB?7NE zGN^Qr=C)5x=0XL#LnyM!;D$R{X_c71A3bHEl41i~X~9YUY_AY(ji?5S4H2#`Te*C0 zTQ}-mE@1+F!cThud7tK>Xbkhg9W~(T_%yK%b-jt4lh9n&>7sN z%C;2Snj}A1mT5GRvoBkAg}pcgcsYfb1-E!)q+)w&zdk%e9UnW68f|*|FuqHLh3`e2 z!9LkE&~;F{DzB@+g=;*V9j)`WHToqNUz1q;pq*Be7v}lBafh`=kO>9Gk95EV)hN_p zRc)6)`t&odGOM&8YYB~Tyi_)Jg_BGHlbthK3&xH~m5gp&Zn6`)gSuE~#o<$adk7o} zBJNbE=*4M{&3}dn(sd*VMby>Drzt+oXjOm*X*deUs8YkV>aP2RkU6;ShWT@>FqT(3Kte<)IjufuL+ALARaW|hPo zQPT@p))ht+rikGZMSMC}-0oQwvC5DhB#RsGr`HbbDN&}>eTmb*3Nc%CA{H3{6%C6f z2V_kyU*t7G7ZNnVR5(cf)<1vSUgLCBz~Es%yo!C_b$^OoYV6Eomn(THuDlXWJ|iHr zm&~bn9yi1nAWG|0j&YQ1V(-vSq z_Sbr?3t;NcVDLakwBL34L$h>J<>+%S^^(SzIU}l8?kUFg%vSz5lLqz1#ILrG*}6m% z@?x22Yo1vWat%W*TO*7SPLy*d#1C+h5dJPDLKU19&~34x0>#d9XaDPS_2W7U1BNq* z&G$K}%UrVK(B&l%Jl8zz@xbrUa|(=WRLwq9oqA0bc1lh zAA62coy;y_iS&=0`4?Q;PSwC!y5-@vkn#S|*YB^4i3C`uZupqQ{~&vmy%<>fOt1MN zbN!2T>|Sga0TmeG@hmbyQqlX6vmA7}GCHBF-#DZZfL4)Npo<*F!#Zsqx7Q0)n4@4utj|I&{mghZ})bEe-v0 z2Ujvy4aIcwwVH;A`YBqzb#Rnq`MGdg9)Eaic)x*BieU(Jg&LJNJuXPeT>CTa%+q*mN~`?7iayhTtPWJS)U<`{b*UrvOCFUKq3e3jq-t_ zaA&vp-7YX=N%&)A6LvHLIVzC)xbGRhJeVGGmnMC{b1cvKZ-IUJb)72rI`$34V$SE4 zil)~I{)VPkuB7x3S@EQWffv5ym5;jfO%g2HgnNI9CzPmDo|f_e{6@(gF#8FD4!AV^2yTr7*YL$LExkUNgzGF~AH6IS zu@qw(b>lvr>`@Jo-|9-06J)}mK%yNj-clA?7XhyTV(fdf*PvX?*ZZ1Gz4juN_l4U1 z^jwa;T__BS9OvzPw?l9+3@PrLcOQN;w;Xs;k9n)ed;s&+aQs)DB77E$#&dQ4ZH!u% zLS8ZJjRyLOeS*D!Z=q3J$dIs4!6yB}zi$-(e9AHTg78V?j-DVgTdn=#1Ntp}54mfZ zy?Lf~UyLu7S+O@*kFYj|%5zm_Zyw1j*ybs8S%rdej7-f&CvI(Ot*<6I`Y8LCB2@^$w3$&R zZP!lUYVY(&@Kk-AXjWQG=}uNyK7sMV#lR-D4MZ~we~Uy?&&oo5b0)9eK`^3+?%HQ@%be|qhSDP#9H{~@l*UU`|mC?NyZ>2jT4}Tyq`{dJ7 zm9oo79lY3I{MEAv($#HpxL4;el#vx`>Gh1sDBi(A2jp4E;0V|H543$UAGV>0F`J83i7|K)Kot~ z{I*8VkX{oz2`7O;oQl=9uICIh;Wrz|HUiBm9A+ZSH z>mA@Qhn2vG`Z1hfeO?vbm2d6R=&@8j9DI{9mzAj&F^J=sIg-r3T?{=UyL*if*T0C7 zB-YnuMY5yb>d_o|Xx3f*s!8(A)>tx?I0yw4(w3Q-(%MdzS$5@Phj17egSMC6sUQ>> zv#gMJOIG-Gz>dMcFIIC z_!<2v{iw#q{^^5MIw3zbma3c>(Mfh>u+qoi$Omhl^XralD&qZ(&V-qRp0^uLnRU;D zE=Q?G#jxgZjW&M^1olhLqUf2ZF*vFt1mn^Lzv@f-ejY8HT(G|oETv7KavQ8I4*C&l z?t|G2^ZcUV`CSbQXP@_Zu{5Mpu`}fyGYiBc2`y5RL_q`2c{fUsn=p5Q%BPs~FTK@g zj(k@~z!usQe0q4FH$sd1Em@PppEq*Y_@~N)lJDltFz9?D7C&n=g9^!>p`B?`s&Oe! zBXjQ9G}Nu>+>wRE_N{=!@{2|I>SnrPHtXBWtjQB%W)h{PS}zr<)&=}2dl{wgUCOHu zH!+S`_lwrr8dKmW#N>@&FnydCX|&*;^jO0`lC5qE$8ElgT`J(k+G@_72SOybZRXeP4Wn0!@or&s~?WH*FTJ*yndZEZ(%eli`sC zyeiiVeWgF_xn(0AGOcpcc1f48JcJz5HcV_TT1dUwf|eUEg|BodUDG?`uvAMP=;Hz1 z&nd^e1-a)QMCt0Ne^gkWEPm?tQZ%M@Nn0h`@`B3}bLDeRxL<>PH@G@0j7mChgV4;l zul&BKV3lZ<+DZcFEtK6?xW;Aux(z3CG1C<3&39~?5=|}vK7Wx+Q=g1D8=5Z3pA0-R z*<<`Pt12hIi{v37T8NIay^%UUgmyU$yj7>XZx1Koi$N&u-|x#z&tU-UA71 z>>Ijgp;7V$GFX%h3VkgwMq_TbS?FoCnrrM{G4LGI`Ya+Gr_9)X_S+b4Q=h*qVn-D& zWfW>K!hV`^#nwbJ+R4adH8qkI@INvj>Rw2b-SU>Lvw{(oFLjQT)?2_1`dwx2=6H5h z*;(4Re(_NJoI5bY;Ee6v8hv?Efn5<5K~#xqo$F3L@9aXStS8nBq)}~H*YsWzWgwDCbfSo&g9m@=5NGO(qzvVJqz_R(MMC3c#GlQPMgb|R_&2Z^MJvB?Ps=s+Lih8lIK^VcY(u* z_cEZh5o@yVftE1V!cV{0czWu3nSTFznC(eg-JO|x?k=0}WHA(p;8Bzbn;CWaJ8MZs z-X`XcEBZb+{C!i(_-5i=d#$o*qtq*2FXGe9U2@^XfYl0nyyeYkeCbzN!d90{o00tY z9VaiqSe$aq5m9gFtbb|~`i@254z<}ty`PFFqURca#dajO=p_^eLodoTg{9eR$c zTQ2UxXnz`SQ9X%-BwrvI%_>sWer5P&wMAK`jHFv7j8L_7PFni#Ck+g^qc^JRJ@LfV zRUMM%$K+O(G4A9GX$vJtR$^fx&=A=aEY+EwaL7lR$iK>5G1hjs&&`uG10;n#H1(8G z+}9Ijl9zJrKD5P~Hx<)s8=6ZC`8d-P7Mse>ghM0I8PiHz?+#>zx#ZO=a_YTjy22DLl(Yw{EEH5pBZU9X zwqWzT1`&z6<5rDHqA#>LNl5c=LV$W8lWBIT9wm^8hB3Y4t-B_(KM(Dd(}}af#o@Jw zgq9rXFtHcN;Ni1}8oj;qEAy+Y-oj_RK&lq993mXyj=(1jhfMy)@^+O{6{(~z3rqV3 zo7y85Lj!)L9j=39xvMP+YA+A=x1u^q;~pZqO4^jzX=0e%!o)4TnL;5*Q+W<36ruEk z>e))t)#Vcjbj<+2q^TjB1VK^WVDVB?QonukU5e| zTs7aQ8Kc>Fa!fs2`XT*1g95z1P~V{?ZVB?@L?_wlr;hh?-d^FcQLaL^#{>z`VLH)Knk%Z7g@Y?4Zf6#Efbc_$bj#Ws4@aE9hHN7?Aq8yJxF3GLz{=QL-{=g6yoS+kIzIZQANs*Q;eq2cV~Im zFq^X^)=r>r>pMd3oK64r6DMs;HtmJga3*@jP}`h#`}keJJE@8=EXBJ1bF4 z(qWO2TEyI{7=&7C-qLc<-muYsB8rZdmB*-%o{^ z0=@Y!bQe#mYLUuDJ)UCnwpWoqP^&1Y>cTdvqaGkg6<%ZKF0qzdaPJ%1CJ+{gCyQPX zYE)W9TULf^HkOj)-**kuL z_OlHyd#>F7WDyn0{*Ll0vUi1MubHD(I#}tNO_Y4m!@x@a92?;Hu=qWMcB|nMe#VY9 zEsh%2=xB%ayni|$^!4Gh%b){lr&FR^8X@Vi5T8{ruldX}U|SLt!hNXAaro=xg@wrg zAKlUd=a0Tw=RfLUyKV-PPpYoGE3YtBFnnZElV^vAsy!{f#Udfmj*c!vt(dv)?25*r zwRu%KZH#RwtFGS=%&ZVJ zgRikf&wBgUpmAl##d5rIS6U9;TlouUUvE`Z+~y6r2eddgs9K{134X8()o)ireCrIo zwbrgb$=$P#)YWLSuJnYMAg+0?UwMjwQpdPT`+*!8yRfmvZobBWg$8X67o~lzw`Hb! ztamlpYLyn`POK%+gQApImD5RHxk7IV3!@Z~Eg(j`sbn@&+=ep!sbjG;=DsSuwtG9> zE<#LWB3x2LZW&(_c|G<5To36q8FeDcua!z(BNFCZ?YT=-A;O}Mc|Mimkkn~Lr{P%~ zq~A5)PhK477yGaN3=eJ95Y-^{TrMfI)E~-FY$%`H6p1&*-uThjVVk9+Pi7a&WZZwy zK=CGXxE=Rsrx?pwqKCp_i8=U2Qf@YQDriogNCh%+OyYojLo9|3Xd%h6>5$J3yDd9@ znm289D1H$O{a}ze_Pk2)VcJIoN}qpCnmF2p9afmcwiGzVg>OxP788e^QHK`rkbPdY zw4)j1_8fu;i}w(_sio(Uyh*Cq;3;iO(5sDDkOq-8$pU`!e zexoVfqg7EP9qlr(vYbGOC&p?+1MYuRRt)S8-BKO979-H`lcM(?fo0nl`$smXk5+kF zHCO{$7eIBpt11+q;R6=J|yEYW45Sv9(4ia55gH@?PvzO9egTG=W+ZNxhS z<)go_wU%A4F$LL!XFx3^f0-rv*=NSIh31*$bu|((EI}GQ=%aLx5lJ%}_CNHrm#Q^< ze|%jmE_S%jrwc%iCrC-{8|*q@QGI3x^561I)6{x6UXWfuC;9XaqpV4Ar>PI6Tgp0I zMgTR@%R|s}dG}L&5VzzWyMBpOZUfQo}ODu030dpiASk(65ug%Bv^+5v3xWW*nu`s zQF=rl%q`l9AwXveX;iuy;l+Nl7GeJY-yFZhqhL4$_Bsjt*DXO=Mm)Zs5=-U*WC=|l z+3dc;xW^$~1fA*hD2gJZ6RA1Dwz6>nS6tt_kybzB9VNJ&MO$yanZ34!rsCC=4pSe# z`1x!TuECwJWM=n8dk#1q79UZ?vB*$))Hf=)@ z&g!CnpJU$`Z)<`fN1F^A<;9y}jf;St$1!|`CEM9^eM{|NpSRAADiBLy2d3G{L2$3@ z%J-M{xr0+O4pvxIY^-J6u0}4J8}extxx0ze?)wzsOT=T_O70wl#2aRNUiJj)oaOnV zuWb^-%~iv!(mWh>cQAfn^HjS$tR0J+;buF}BK-+7FpuoSHxZh-J&=OiofDktJZ|7? zR_I8g%|35f;`J2CDooYo#El3E@SPblC2i247k2VZX~tD`l}?y9Pj0jY@mD)7oF2aB z<0@mLFO1@*qq5@SL*;H-@_)=>-0c)a!SBMC!?mTxJ!8P4XLzr$4Osn~lg0Db1k<<> zgO>&S1hHmEt1A}2uM%`5La0kP9}y>lO*sh}=L_%320MNb@k}OOH}vlxcXpF3>e4!4 z>Y`PS`KR~lfkU$=d)Wr|or!3sE}SUJQm&OXysrsgSF1oGn#HJvFRMz}*HlFb<5 zm@NPUJXmfy*nT(wu-}LM9PvMr?PIZs`J$aw7DK>bqa8wx3$KgUiXj&Z=b>x;ktwf3 z(!7`^x8IusV-y#PL^$~66`1Smn`G2UuV`>y)%53>4J@%c#q=%Yw*~0By598eI~iPW zS%!9Bu0Nz%rj7Ss8qrcw#lxh((QC6669B7H1aYpnb32vA6!#)LV|Axo&B}G$7xRdP z)U@5ezH0AIiRl_kIt;#McjxBpS}ef_eBd2zxGSHigdtK`R!!D1eHQzaM;B)7(Zm|B7@T@4uHkmE?E)=?PR+ z+H+y(kyH)+4LPiuDB(7Q3Oen}w0t%$IyH9Ysz84B5pmNk;nt%C9oubKRS+gYADw<; zxZ-|BOw-ajcX;1WewAcjWIg@K-jqM`?^3m&=qKo|!%z}GCs=VLD6Il5ath{A9?U=(s0MA%Jy-6e}W6FQJ(rzmD& z;IizweB8uq5r)nNz6QZ>*~;{A{nYkJe>P=ZM3Fi3$K&@)vR$Im2j{DjXVd)4tA`PS z-B~ikV+(9ym0hgyg359GSs~;2pM_P+d_?onR|C;(I9nxp?^HZ{IO8eHJG6`5Ur#is$KQh)#9IWh||EUge zvM{p$AJu`XHlrkComPsB^yHT4F{#Fd7Msl$o6dh1wT*wbUC+LHx%_-H*FTFszC&1E zGQ6m>TP&(z(D_*;@V)WX0rlb4(Wvlzboh{hBU>AN>k9oVnjGMaY3-KfS#_duI9*qgtU0vAf1$0D1q7=0;$2#(VzXK zewcu<+5llEwWWpa@g!~K&V=f{>#0HK)S-G=@&jvns{d2VnuR}YBMo2ywe%0F zFYgJ8Pp&UK)JyAK12~@wT!MWuEMIP}YqSFsL`C6OP1!^N96xJYK+Mn1&we6h$EUXD7ETCJ(g4-W zEleM^=wHQ`dhYSI%)F0X+BHAyiEn%`N^-Z(-uCZU%OacTpVNIpEMQHKAixjsSomRJ9AFP^ z5d%cU6eRXU&+l4%<_7vLh0Y)#_(}e(g1l)x`(**xUPnf3M?&W+|Fnc=31O6DTgWzJ zas5UW&hX)T5(o{vmcmJbdHk-h>GV+=bX}u);D6 z(HmJdG6i@0B^NI%mTy3JdmXnby=hCCDk=G|PS3qA1};=2fsw1cxl$eGgG}E?(A&j( zI_x2-4&9K@z9B}mkw$4$+IOF`iR`xx+IQROqqNAgww#dRB1kYgiCGsO^gPo%x@CjY(;q>zS z77AP(O)wDsnHy-2kmVyP50<&X_wY1mlGckI@&2SzykSQ>m;92Y#afSYY88x5KwG@E zJ5LUJg~s>vMP7q(9qOy(^5tY`a#7MXh)M|~@04GGLeu(2q|K&B5i7FbvZkGaj$&?L zle#37x)6Kk8PGnlOCf4p3?6XwLv#ZuzWk)2L51(|MTRlS>IYm%Tc_z0l7?&vCnkr8Suax9sDG5%d3nPyv66tDXDJ$XKG5Tjg~ zH6PH{n#-qnS(@CcSD}L|CL2aaT^l!Zbf(=Xe)qfBbyJ>R3o0m{3;9BQM7rUzPEjC*8avGa{PIlW< zBYNTuHQz0$`4!XC>n-uVz)hh~##+h3v&l>UhU59?o-7p&BjifDj~M^1Fv7AQm~yHa zAX8uZS}|Tf^-FvlAK7Tts?mW2i?%*WMB$66;&w<8${))-9F%H*Q(}5dQB+!N7iN0f zGEY_{R52*iv$AmJ-)yvh^1a2rxr&}=G~rDGQ{ePCGNRG1Z=8A8OQrt+84b66cnT z>q6U0gRl}&&To#CxzVLbA<&v3A=-oCh;~m#R+qT<=j3u;EG)j=!t_057Yo4Wo7Ib+3&YA(tg(EF(?zP!lOGqSBu~SWuE;;K@8weK0YuE_O3TM{ z9ZLeZOGBs76e70rhNf6f9PWOo9RLZn(<&zrzUxSR`p&gVsbyin%OBZmujq-j#2yD;+$QfD$2|GJWN=OsU0E9#!* zruFZ?r~Eb`Kp`AJM_(d3;pvUSbC?x)Uiy)vj~1ju_)Y^_e9TFBZLJ@1Uwf2{+P5F_ z-q`3f0!qXt0cvC5%#$*O3xh^uvMaVb7{-yEr>-<)v@!MUmolZ|cR{k+y(ZX@W;WNP zY)rYZE6opTl7m7e9CXY@csDLg?%(b*I((;b{_Zt?3C5$By2l%;xY9*44DuUeF6X8o zxv;Q8vVN+6WhewMd{xHW*L`V3cU4C`w(vm$9&bE#Iy8@iBgGy}9IPnPZEPlCs(tr% zkemW?j9xb>uar=D+3B4ba)y9Cr=BU6NvFsr?VVEGbm6=MYL2;)wPh0+QNQk5 zP1`>1u1_{S9i3H*$s!8mT>cc2)z(uf?N`=k096NNTQ?R?wMM8FE&=Wjb=0^fCgBlS zsT3<3xc-B@sBZ$tfa~5!ljZ&R%9i&{?(YHe40*U+qtHv=Bv3x|q7TVrl|U?sj8F8u z_hEt}s82TaEq`wjblhUL2o%_*8~jMyUp4g=6bmf}!j6(-YHWv*)=Rc!q%WWucK(s8 zIrZ6rEf!9(l}N-_2wf+6IBZ;41U+`+*q}<#cz_>J%#kkLL6{`4m@Dg+%bq^V9J?Wd zqH(xJ4KlDoZBY{xnkag~gnf_qACM&Y@Wx4Xm0-qK; ztXqLO{sKWB30KM5?Shn`0r_}af8f;#|V=L=SfS7k_#uk~xa8<8M=i)5!mzg-S;9K~Y8l`D% zEhFv*-WSph9vW2Udt*N-d;o#)XKxmpK4Zs$xU|NIhGF6!+uMK}26PK|QXo})U+OmP zu$@hg>qxGoLgwIShbz;Z$6GE@n-`1MHJO!zrsLBdW!N9SWX$&zuie!GxGOUyakXc# zy+2lTddK4hGhu>`2}_{w_S7i8mKBYjw-azOd^QPp2Jd1Ki@oI1RS9;AF4?8Q50aJh zai8K%WKDzADI0e#^*7G#6)9S3``-aX1l5;b@&^g9FG4P9{O#tP^q((>=#{s*iOX<9 zy)1Yj!MFz+?~Vdq>$nKZQDb~o;gLMBnJrf|mihYkH2{A?HR*;{^4diP(khx-#Ks6NJSo{H5|HbjJ>e(6pN+=U|N= zbX$CL0Tt4g(9)!fEPee&*mJ0#9)%U+ZI74_itpZ#r>#>n$_z%`r*>O!eNmEpedwlM>eD=TJ>p_PWGqjfca`GwR`jKee!V1%bfel(% zxyGEvYt6@wI(3oUjB6OfU+X3C2+Jldaqvw(REHO9iF!oIJwoS|hqORbK>d+!1Nw|o zo}BKw6K>4BM3p?%0YxEr9o2{rQmDSBlI1Qpt4Y2-g*6a3KZ?D*A-PYzTzZXBzofcK zP<);VVf1KrNV6w0xjNk%h@Fj#; zEy)^Ywzl9hg3u=*>-R)!WyzGzv&mR_k~rLkrzEMy*&}Ta|Fh1W#t8Q3{S(D=GF)TS zLK6pXq6iimrB?K-ztA)Ul*Gdrb(d=QkIgwoEc?SI#P%_R`AQ`gKO;NKkb$`Gf`ivhA5O}PY%o*y zll6LYd4;-YW2v}pEBBDN1;XT)8$2wnMIJGY39HpG?%+2hD~X6X=8|z@^JO6yj{9FZ z*5O#2sEn0!bpB?yL3pre@~I6J?;_k0D4oLZm|Cb-oFrle{<6%tR%46$YmYX;Y&TE% z?Z0o$sKZsCEm=r$p~`P^Xqo*8RGW=2e`=f{587k`Th{{Q7n#MUv%Prk!KsLc_QOw%K2|^Y?Meq5!X5@bwn#@@M_i$qp5?logTEJ;l2M#G(tgN?ThT!{w zH+RBeZg#%~)74s(Z&QVe(y~TF$&P?j5|LJK9z0p)Yxl-d3l&$J?eBNT&nsaT>>ix7 z#LM=u)Tk2Am=9RGv*)F}3yUa`bMo$!+n;mFdlTIvTiSom*{ms%#y-FjUSFCEyE6m; z46==n86yS0OE9$Wal#S?FcmS($0Y^oBJyH{pJGW#WL@eA8yKd<1B!RdKAMtY&6rxJ zM@q#a$7|TaQJ(}jVyO4r#by*S^@ zqKo^w4{I0Aqy5(7_xCBL2lg6Jj)I*etJ*AdCgOE$N)fZ=Z5MgkwAE=H6C$NSwGuX$ zE;8Sz!#_$Mp5$CFAdJ!u5;T+&Ujz23q(&ms4J&+b?xPL>8TeE9xZ+_OpEr&#?k5yF zIn{wt9c|6X&-<*!YZ$JbXuY1gs)E#Sq(vt$lhIOtNRsT}IXEk!kEF!HLM{x=6TN(@ zjG84JMzlHA3Q4wc)a}byGX_P7DR;ZdHo|(`I?&r%cSm1x!bUHyn1wXYsSf9>+#rqNbF(iANa<> zUlYMADp4wW%#K}{$-_@!VFMQGNvTao(b#3dai1n9%40D#tERbI6e&ol>z3D@8+x2` z8lZ9D>G7Ky|4bmJ)rv{GY>V9d;ZRrO2Y-sx1tYF126iIm1ID7H;=^U>Ukt@SyUy=OSCaSECG~XXH5t?N(O&8mycf#Mj_-x!ZR_oab~(SuR@PW?cA#km^|mb zxE1C}Li7+y+LK7KLAW#oHj19GhQCYv#R?Ji2r^I>nT*%SC5-XD?zb-~+`@9Z_a+S6 z@X{oBs@App3znr`v!}x@?!L$x{C(h=at!Sk{;{8fB5yd*1P7*hml&N|p{6;MCNh^q zE3aQKEnRVx*=@*kBr z?W<0<-P+-%UP44;s2OV8YjP_EE8rw#NK)ZJ=(HQPExUqV+o%Hsr{^&$wOPNm&l#`5 zu%{%OBKDX08HRkO``H2UBF(cvzaBZYkP4VUC^%E-) z>+qODR0Bzc{Ya*41tf7}!38UEvL4ZolPu+UuYjkpU!1Pqhc1|HZ029gh` zHsMTx)?{zM!}*3!RZucWtyIpetBc)$(pf|gAC1Rsz?Pnf2i{kJ$7k7ALez4KW`*lF zR6}*?@;r8q<@u7=v5 z9@xkM#XyF&B#(!10%!Bpdfd}SOw}n*o||xwn5nG}1KAxsE=24}1|b^dVZ|6A#&(VU zSa=aKf$38Zbq_PyB0o0tdh{oJe#mp>NrwHXc)zpDMwC0C!-^sR-yzhn+RI~ro>0*+ zgg2AZ3l5i2*hDesirW5wkETIR_vOBfp9Pmo#1u;vIaKGgZ)%E3idKUt%eZNs?tq zgLSIfTI`o&T{rL430pIMLFQ4yM3Rw^1AR&>CAH6VG)RJ`G>x+uZl>h!zHa`@FIB#H zy5Z-A1Mlm_mrph70{tBN>Y%4*MB0Xhb{wa*mNSiMv94HRN#yXxA-k!(gBbrNo3=2x zTf89@(WD-J^6}4goCAhb5B(2N0N%Lz8xtkfwKHagL|GNriGK*mf*zvwpYJo#@~}|s z@?nctK-LYt^y+AvXh-O?(5kU`oqD3*C%Jd)vcML%zwJ4ej&42XL>sD5hR}Qb&=EH} zxGIF&6qB#sd>~NpV}0Yi-z~Obu}KCo>ec)qsBp_kt3RpE0}|pm$Vs=#gc910IT zBKg6woWo!#G?<*zAnH`Fb=ADy3`jQPu5Nlph9G{UT?y7gr~yQ~l>A~G_}13e>3vK| zc(D;;m>2LhNRJLv`}Z@r^H2YN?WT7$epNqJkRHGr){sT)Byca$h-&ob4I#a*=_+?}&WzN1^{ zhjHWCI26NW1Nyu|ZQ^p5CvOdDo2h8&@kTsn>9ti#ydUBbsFk`&fwJn_t%NMG-LB|j z`{qsLAa)v=f`>#U$0$diobK;i2xrL58~6{ic8>G7!9c4Bn)@B1YY_hQZAYWE5ZiFR zGAHT_fg;{F$?Kn0!S=h7BtK@>Bs&bd*xj**QW6d^LTYwe9TNQ1o*LWtGp zB=4R^=%6)Xz5<}K5?t3F0!JMltmD$LRt6rzK7URvIE%pncF-E_Fx*$jE-W7_f}Ymq#Xcrhxm_ee z{O*Jo?NK0HJ@_UFMSg>?0x-+K{@m!<`bltwzprB({`^DgIpbqmnP7M&o;yR)V?{L+xo*}j^X zcpT0Eo5!NTXL-`3WDGXrr)hf-Q1Y$lA__3pi+#Q)zLwsX(MuIZ=Vs>tJYwz2p!KZN z3wg}w%`cfkC(hqEkq1o0X#2rSJgr_-@-)+@_?$od1hmC+Rh>Eb|XV zTwX|*N3M5HkJ7yjw^$kUe`-Ydp?>Air3GLhF(ulfuwcka(j*!*I1Xlo4U9bXW+w0N zBqP6OAf{0x06fbMvG))!S*9OAGJviLg_+1r$x)L$$ay>;PwfiPnR{DW>%geY5(A#M z`Igsrj|DjEKQ$3xnVL!?92-`lP zyH9JH<9_EjVuf+)p_I~M!;i|VX%&ZBHQusu>3&$CbmNTovH;m{-P`LKb_-xuHq(`1a`n>tl`VGosh67NT`_`nx5F~JTw8(P{6p;GC9T{;>~+JIp&9DHZxe>&8sz4W|2l*K_n4;H`YkuzD_;+9TL@0K+ehXg zAw)ux@0bz-5ElYrls3R$hL=y7)U zhtPjtSeNx17seCaIqNyt;tBs2srUZ!6CcGNCUX}~hOTnfx3Y_-EbaK3We1#&SE^A7D|7*3j zH`i!mydsn62X&!ZPG`AzR<7|S`&3H9PGZsGcGUu7g5V)ff$7^!ENrq~f>=`Dl}@y> zvtc3A{pOtX?^eGhnF^2E&Q!+Hg8j%k89g8{`D~D}6%8)U((`dc{hyVPq<4o$PK0VX znRJOv5+#1;eKdSYMSaN%h#e;C0psPv%J7|KNc~VJ`7=2xt9fR5l?dhah3xtLBxM+? zWoK`YS>W^FL}(@G!RTyGMinmumzq(|6sGq1$R@2`fx+>D4o>9-FlfyCFVoeqA5J+H z@(f@%rPDFO>RnZe+*-C}S#(>+0`vq2U8JOX_l$(q({2VP(iIA!99xI}Z=VE=rJDNY z3+|G5j)}0S4#WlDtnk;Ji+t>WnB_GND@-(&yM38!w$C?+uS8&Mm`CJM5@6(tC4cAw zsQkR^l*6WjVa4|cYJwC?P?2H;^e$RQK}{IznC@1`_6UIEc*zzsrt{O}t74+3bNo7v z_L)-I;>w`hcd>fl6^nJPN#<58k$EN`@#lE7m&#AZ;f%qqK{`B(BSm5naRRWBn!>_ZrlmWeb!;Vw z4>6K)YaU{duwF(ElGw9v{I>l@Gy@#!kWl%v<1N1Rf-a`EU@C7U?z&1&c@4XOy|&9r z99;a?Q%n$L;(Pn)a(bMQ&Vh&sC6NbSzT#4{dOyIX2UQ809a>RSeDVq|{TftTW#aea z;1FCGD#zB9Efd@wVU@yYHM2iGd#hYz1KiX})NCZgtOU)4cR=8zFa@gTrc)5pB6-NN zOPxCl?bgp5!_)#HDK^d;jQ?T@qw~gT!+tKpEOhEh)tWz}u-e_->@JSYM#@@mi3;JD zb#F>eD9)hw?`MkiN|k`-dSW0G;ah(23&$6n6G@CQepiH;HY6v|SjrRGp9X+!I`~=O zH2RI}P1Py!c#j<^*aCIj*ksFcp?FxTeKSBmKfZ{I<+l72(twsNtX>*5oYo*T)0%Gg zf(hiqf_1>?gS6}4AL2>$Lxzkw8gI_pId!rUPy-MJ{l9onsyX}k4Y5l2- z)*MY(G-ke`NTjnrU9huiq@eQqX*?K@mtWyJ*ujiSpp$tK;fXhA0x0Ru=v@nVyK&ZJ zO4J<@m5He^RX698R*p)$@jz;R-TBdS{KD)O>-n=x#(HL5it0ng=HAwO!VylD!jvCB zs6rPQqyuuWeJG-`nD$`ylP4VZLyZPxeMPI-_Nvh^ZL1_I2mHgh(Put!ekH&DCmq2; z2HHl4={S0Oqm%9H-r&IDE5gUAluS{d2ORB;iwl{(s~);$-v5SnoaaipwMjTkx&`dq(bq%w96w@>vhn*SMfm#0_^U| zukc)%3l&DJHq{$`5KtUuIv3ZXmSG;W=-ykB*KT^C%w5_A{PmOf2cMnSgjtzhZ{R}3 zrl=$Q>f0C2XwqirP4JdWM8A;O&HV3t(GvCyW=Z(1Rx%prgDZeGHaqh^CuY*iE3-k6 zUp36O1MotBs8HfQSZ~J%L?XHP&T11OJ zu~15;k(s8VN!xl=b2;zQ=?BriwQ{y>^|?OzC<=Bm4`wqXWrH^J4lzU>9O&xe3)eAC zO&`Py^<;qDLp$gqT!ytrOY~0MLCFX~d+sCN2*Hmw|f^)Y7}DME^I4;M67*Aa2l z{@sZ%@#ncT)06}=G{us}Wicx+O9`2o&`VP434c!D?=AY19@fOO#*!^KcDd(an;%hPYbFkPRub3cam))x;|v*> z;!PA%>o^pT@wBW0-Q3(-doAk?96vs^6wnPIb%O{H^)pwh5RsKlU#4W2<&ul76@f_;qU z*Ob>-WcJ<9tzr(raVxA)Wg%f+-mPml55ju^LSjFsh%b%U?htup&MxL7YjLP;MY#-k zeQyn(WR}zo2B|%Mc6;1+io*e94U6?^IwEv@z|G=+;8VlYmG(l>wB{Uo?$so4l`j#- zE#dMqTE&>DX*4})S0DRSDZ$X0D>u135}ONCh8g@)rvNeSpj5sA@f0XeIT*A{!f!SI0@@5?vPO?|onsLOKHWiU7frMj zm#jjC*46y;0iKrxoZ9Nz&&OX&N8oimBYaz14W%yyx`&Z8^&~i2s~DNZ;#-^a7Q!S@ zbAFPVJ5Pf{7jB670BU@ZYxmE=X*^f2OsWLe=4Qar6Zz$xKFKi<16%c0>LPxsWEt7B z3LVRB6KPZ7G{?`+x%_~*5WW7R0xSqC1u~qP9Z{F!JvIwaXp@T&s^4YR-Gs$WIQo(C zB#%}8vfCd5a1Xrs6)^{dycitaf;S<;#jy_Ly7kIsXEe87+a#Llps0a7fwqMR8M0P# zBd1I(Tx}@7^*Fe{$SVauF)cGD)$r@YrS-f)9#BdSeseK2+G#t-m;?p?0h%|@6r&s$ zZHZVURq#79QQL3mVSpLrsAqDIQ$*^eKp>g3$P2DzFd`NEv-aV}$zmWD{W*pgD0xU2vYGqzm+ zW+ZvqC?cKwCp<->g+(r#~|u@1yLJW%LslNhbHCLDxEzW2Xf#oXt-hD`tTt zCsW@9e6^~m?1weHnF{d!_**M$GQg7IW%XDjwH*Vte=kA@M7o}h@jO=<##6%-E%8{8 z8$I&qWUp4BL3!xFDa-Ggq}c0U1o}>-j^P;P#<(@~Gq=+gx!hGZAfV5o#|D>3DzeGq%jiQ7MbKYe3E$o0fP9^4&a(o?d-N^bE zRxoc?j|xEhB8jRT%9g)*&_yO4eL~hodI_b(r1twOTQaQnyETYSg$f3!5k0FKv4Z}& z!})IJh|J+v<8{uTWlR_0DiKZ3Zd^)3l1PG6w89l~GIme$F_xE<{WiDwabP)g@!MB7 z3uRRS_e}guOp2dMFJXl0E}tHKYrGcU=w5wJdE|2V9lEb)4PB=1NqT;-xd44Va8DO5 z^8BuJfxC-q$_|B;xWnJ86T`E)n)-l$95XA9u}pU~o93*MqjA z5?gCh-=L=*1u(P!)h9U!W;o-2i#T5Q!@V?ZDCJxeNR|skqAWty|87kf0mZamcJfzq zmhsA}9Y19>x{JYwx&&#xG}MbMJbR-bX((al1|K!jyCHr6s@6caNc(oVqT5psk-2d@ zrn%6&ISHgrf13YYZ6;TE`00!0?1Br3Iwp_2eN0cRYO2T4Kw%aL0WFq^uhNndxwRHG zP0S~Wjql#wMYjN-yP2uL>9)IN3qG;+L=mv3Kk;yCD*Jb4MR9vkEe~M`rFRYO$(aEh zf8c3l!}XvRg+?7{LVV5F%IQxxgSvroe{h}gtfN1$i!nJt?nF&Phq7lMr?RlO-3k?pb$Ej9O4t-p zl=*lss4-RF`YxJk%hiWlXXcW0Or22?u`6HNXZL+VSx>Em2}wyRuj*K~n)x!%>Y7v; zYeBN(t6?E=%X5Rq151|p+9*02`B@$>MD9Sp+=wiN>=~|{Z>NMD(XA3IAG6F8#-;6^ z=uYrjqJ2qQ-zMwpToSAu}QIH(W2k+uI5q4KFX9gt;e_DbpX->QQWG=A~iBv@^Xg5?- zHxp_$+T~!OqSYE<#s3>rev>p;LZ+6aLVLO3SNa757nlAYzOD<-a>UCyHCFv!LXw0x z{9n}+%RS>l&1VJ70jGgl{0_OlYmND3>>~yF#M^q@o!!t0;-|*s&ruELGi*!(r{+SSc#(5%6p zLYffKk0rbOo>n2kEvIxZr?M4+y1sV7AZ-X3oy;`p zROi^N`1OW3lfcUG`(AdCXt!?3Q(y!i2s{`tmA1_`=*RqoO-NggAvDNI>f=r}9V59D zHf1FN*o)IE!!0&c+f_-YBhFb6N;uK{2WHT*6Tf4TwOhH7w^Z5v;MG!2OSp2-BC-$d zA(c@i{NDI2f7N=-&b56AN?aJn)-Xqc_n5T%yP~qk z#M(os1r2O`C5=eLQ&&)!1biFh3n%I0m=WqO$zJO`*u{A)@^Usb<8Qhr_}pS@3nlli zhUr_{sovR>1Ug6S!m0?YPRqM!j9as9>o_k2>UU5u{y2iq#M_p(JBWJW^C&^rKDIdDol9jf1>-bKGyw zjs!K$zD^<0b5poc@J2kj#u^1nd&r&HSlPlr9R1YQBh9F+k<@^BFS<}6u6Iv?`Ar-a zk@53@f~7{fo{lgY>Ng)4F9LV8pIr9mXK7caKP8Y9dd;*6L5U3T#3PHDyP~^hU?TAn z8k~c^kJAzUtV5u3BYjmsk-O@&@Pl3BLY>X|^8phbV2LeJyX}zeB)&oB!#`!sR3_%; zSJQ!hqjVzgwr|AO({6DUzD$+wZi7Grf}BR#^d_pp4!H`h^C09U7C6XN<{Ioezsq>( zX1>;t%F!14=O?+h0~b8Tz7$X{R)~Y=`9q0X*J&|9aMl#Q;XTG4T8)n4@>*~R%`BVU zki{VHm+$R$FfV8_tkQERl6O>K;M#M8Wy2r4SdpTQ67@8WZ6fX8ZfL$jbiNkzM~x@+ zC(OC9E!{Y3K_AuAb1IaWOp>d=R%6i{K2T*iL^dztymlT)6@m|V6S_~=M_vY)ICg|y zGOA&E_LUn}kXLpwTuBa`t5}TeXyC01)p5t`DXOPasHLi-Y|et0u;x3{D6Hb#`w?)U zv{x#DOOXl+qj%G80ryYwpIT=7WhyG!()6Cc=HT+_xf<7JmJX5Ca1Lf6e73Sg{j4+7 ztyz#SL0pZ#nD_71DJSELgdEOcS2GV@`u(d!tgW{gtq*1)GYf~+<}}GCYcjL zWKQ?f(^gE72ik(uRM;OZ&lIM3jKWy$_4-GWy@Jsm%w}g6OVtsIZKY1yHELU+gL159 zl&12elu#hB&IuH++>ipOr3>D%DK50L47HeJg^zw%E%IyfV`#ueOusEAaT4+g90s1J z9englFK)VI4{F^lCCvIgf(aSlO<<}h+46olNC|E_JSxX6#wLM{d{=XKR!L+56&mjS zsPMJbhK z3ggrL(HzE$SZc~HHBAL+QhtF zcB(5c#F>ldaHV{RurCl8LlUCc7P@m3Vput0%3=yhW^|&T*aq&`mv9Ii$Tvxu(>ahX z6dfn?hW{dCPlXyD!u8Zx-Yv;!3Tla}lV9yIv9u$joD!%n>_FD+bj@dlWeJErMCa6+ z$$hK8^w-+CRMGr&XlZnU_Dnz&F}v5F^swM@>w2JoD4uNRf?^A#d|)fw$x3Lm_bu^c zRl9&o0@pb%T#>@!lZ%d#H@RQd_TG~~uyXodCp4fUStc*oAb;+~-@aL>n(lt{qE3B? zzAEON&gey1@aJ%ix;9Pm;3G~2Vv=dunT;2?V+AX4H~gFvhtNU_Qlx^G=!@cEPOT{^ zvUog`j61w%2R$&*{^7~d5x^Dv2V%WDUdbJX(IOUcjp!Sg-A%eE5`}!^a7_`~m}m0O zynC{hsE~q(J!P_Co}b7$-?&Y%47l_3vzZ%9RMtZ@7MMD27iLx|B5q7U!_C~k!NlsxBXCW+(s{Mh{;LZzaL z-oE@bF{hSe7D(^CtSpG5Ko~2)TD<egBiV^x9sQ$a)mv zXL6QMnTzREj%^7_3=;73P(%u^t>BuC4)mALQCrV|;@U{Yk|Q3s{+x z@1wSrqO1k%L$KGa1wEGM=ehO@H?>fECi_k7BI;GZk+az^ z?qnfbBsyPDRGaW4v*Eb5v^PTzOmn$W*jG-X>4?ZgnBF!1;54;r& zo~bTEL;2qEpBfo3-Xk5rSvk9l0YKF!o;nnaxrJqV;??a2d3D25CFB9)hQGQc0K9bK&rOc`_0 z%o6@k7j{HlHfz2QcCRdR|D$>H_R;8ma>gpjkNchbVJ!q)1%&TKXl#&mbslCm(EjE3lRQpM4^bjj-M;wkH8&3$N&v77ML z@gxeV*AR_k3>CDB%SSVj=FSxPCsuj><$@bTG-KiS!ET<)<_+@sA94t~?)h2S_?-~u zcj6VG*Do%RK{X|8V#@c$yii-^9!D-YyUYiS4h#X^^ekgf0no$N=GQ_~%T^35@^p`Olk0}R_d-W#GKD5ZA!R)3SNZz)IY0>akrBDl0-rbT*!On}LZbuaBTF9f zVF}o-^BL{LX4C7L?-xXNjxz~~fK8NyHXX38NIoaWS3Ji72ersy&l7^dC zTv&@vNm+c4ql-O4WTkkuv~O5Kp*^)^@0eah@ouIG6&L587hF3i-o-YLS^-sage%}9 ziw1wZW|Jsup;S>Y$`bz%#1^c_B z#kMD6N`>yo@_dA~MTqoI8xmfAt{9b2F3fj|oX~LUv7C$6W^67OL^xdIS;AmE8V}jC zqjKtire6^tOet#?tYksk)TwEHPxc^qwk)Tu7{98*+EY%lc8y zZ7WXG(9$wsO<<4QNsY!E-;@*=e%QJ?nKF<*nhV}f=w*s>8w!o!0Bn}L?_dtGIH^1 z+Dv^DwA6R4_Qgh-PYei8u-SR{Z^8O31VQ^L|Kp9@vhb9bYB;{EQ?6W=q(U%7=-kxj z0~C@m@MYEzXjQoPB|X}sz2j%tSZ{hYG<42;nYi&v+MlP)V%-T)CVAFI&<A{n2D_LZHz>VwT5a}m@|McbDbE_$qK z6x(6=Hw;WIG|TOK91d}U%3paBbp$Cm3&coWl@(@}({mQ0ueE{9U0rRzkA8oI%+uGw zKZN4>7K|20peB>av$KBFsc2*}v^4pZU|+(uUQh?RI|(%g$_8qQ5|20ulp%nQW^Eq{ zVwdn;dG2Jk$%G>iNj=rFx7Q-v=SnVhGEJj!((@Dm^B9cx=btf2E{?5dZTj4XB!|%g zQ&>2{LL8FVs}qas7-2`0k5lvj8{h-=#+gE ztr)GdzI5zArj8go!xB2V57mtFl9r4QG@j{Kf@z&L>IEPun*jSI!sJu<}rk|*3e#I*xxMc27om!aWCi?`I; z<_~=TqwRiE@n<4i#e7{lZZ!j5qjFIC#G$^yCOUi~)de6%5EN8EH-n45$GNluwN@Iz z&jGPVA=I#sS~_3XCjWOZ4>Rvng*!Me^Ve;N(sf;}J6(M7LFq(@T&0z($IaS-SOp00 zq#HYS?w|M)jm#V73v}P)Qu#WJi1k{gVu`f5VPjb_rqAMm*Tyr$v298^gF8$$>PZv2 zvO|CD#PJeT`R8^OKr)YH@3(Own104h2#yLt2~F zMD1=q4`h7t--4U@sI&M4rUa(s7)H(LL_bB3%Y7yEjr1=I#js*Q76WV)EI?R8Y!IFD z-P{7^O2`Zu7Fy&Pb~4o7$E1MgYz^<6mgtAtgk;Q@N5{yLtSn>*se_B zx7g`pMf)}Ve*t&~hxxzilr@)l8Z9(E8%+4ZO1cdhhRbF3ih&WZmOFwf&gvUY^%(DU zT(Q>YZ(u)BeRz~2Y8va0hp{}{Z_Hb<6YN*ymg{#fk%qM3TgGB7=M!`Af%~(qb4(9F z`7_ogNddBL|9Ke$l7&z6bNuwm!yc`Uk!P0<9W`TN7XXVYhY$dcLcTAObnXHRC}o|J zG92&bU2`2LP1jEEe0+uKr%!JgAN#wb73gmjIIT`Q-%u06Rxmw}njnBt<^jcj@LC%` zlYTUu$FT454^>$x^Ey5eom>eVRz2Yw99zb{+s#XH{7o! zy2S4hTDB2sfJEjzr$~$d@;DfrS z{5cH~;Nk4b&TAiwv;@%4$8G=JjxMyuubiu8@~yQ&#KN%z*ZaXD2QD%Hy!%W|R@mXY zdx}1aRQ}Re$Mk6;34rKqx5n^)(JGQYNN!VSLTa< z4N2!Nb&l?;`;vFGtcunGRmzQE^JrpV&JQ&1$rqQ7B&5P|FH1RBQBHcZQcs1SS432m zyN;kBMb0d>C`q9Mv@UX6#ls59#{JpSjMI_@-H(aAkq`~7wa+F?UX6~cX3YwLm>Fs5 zXClm&*7C`{AEpcuR(K*TzHpy<^4`lsMpQ5x@>rK4!{~D$`Yd7@^ow+WnDE9*1LY?4 z4^HZV4m%~I;VwBzi8iJ&h}l}p#If`umu~?aYIY9a>VXYF1i+jIadmU6tpXfr5SQXQ zT$CL5v?s25-}GDzlQ*^?YV~DyETk9>y_uJuLJW8lz*xvm0iIgB8Fpb3R1~lnrI!%d zGR}bN8WR3rHrV15{NH+0zKu7iZk~q_hj4G%=%*(M`2f^+L85hDto#z zPyl|$?dho~l)m3P7c?K7Yo9%@EoY}?qrJ9c>q&?x9CUSah`<|!y21E$`_XLFBM|*C z-1`yV`r1`E+Cv2oORJl3jAAJ|+Ni66J?_&a3zSHYS5KI!q3Mbkzu_fJ1-+G%n+xA=ONptZG8(HlE9PVfC2AP&(hjlP^b zB2r~DmH}jJl30c9-He@6j3`mGVB5BB-nMPqwr$(CZQHi3+qP}Hd+r;&yky=alhjYu zuByFHCG~U8*=u!+v?tON60NEptAb~L+87fc`|8gv5ytN_^%~X{dm5}r$}*IzEp2eE zVE2z1xP?&jV#x%^*js`Q%2eb%j?+Rn4*A{lb2)abF-UC?0u-q{`5KP& zC6D8MPaBKuQPW;hu~jrTsr!Og25ytRG1Rzm$%=^3@Q2H%AkGBRB|6bvvjLN1ZDvD= z090cd!6$TlW2B0tDM~h+ygz$sjeQI^hs&`H7UFVqIpCd zB5fM<@&y0v|4sg3XZUZ)Kg|DW3H}dhFgrWre@p&hWMg9g|0Ms^xS1&9Y$ehlLEO~B zLJs8YA#H8zV4neO|1$-r5e#hO0ui=%b%D6KrH#FAcQ~1Czn;u>zc2KBEb+EfU0hXa zR{i^5lrckQ2*~&$A=VNSQX)do^8p$e9315X)+qShk1$mH(PaO|j;{E#T`?h{rj+x{ zuOWf}MqzRRc>reeGx=l8qx6r0=^yX!3lsu{3tDeq381CM0${nI%4m9ehWcszbOO;k ze3Cz$oC7oh0Q2^I+k2b z1OdL@jg)s}YyisuI^r+ouLq7t(}!ySSN}Z(pbzn%;rB~9>3DqrcC>f@gazOLpuUP3h_$oZg8=mJFI|x}^&`DAWefYt`Nv8gpBsEoy27|G@=I4vw&Ppod8ha20?y`eM4m4<5#r$#&DH zd#dLG^dt2=+S*po()BBvq;g5sszj7iiUTtm1AcMPWsO8@;KJE7$#k__Cq;K*b zwmm)Zl9$dj|8omo_+DjZzMDwT8suNI4PeUeL}CgWeyAl5F!L*IQu@o2@QY6JqlftB zC-P4l?C`5o^vgK>+xPf&yM!dWvH~^AY6p84#{=xH#XPFtzgY`74)~+Vx-$CW--iY_ z{`Bj}(h8{2_3bwM>#K$Q7FYcn^tik47mNU=)lE4({*iJ1$Tv*@4jE51FAaHQZV5@x z*6Qok-FJPV&y?T80s-TV{C@lD<78;?&L%yBWNZogBNa3E>+i4MiG1LIX*W{)y-PV+Y+R8THTCYk& zH`WI9_aq6Yaa76jNf~oonQij(?-{F;{%1F-P}}xyteMPqDg^fg+5HFqWWsS+~! zcPn3$0O3WP&8VuCU8BqSCo=(c%N9(N|ga{I{DkQ)53Bs}G>p zz3JoNtH6tZ@5A-;ApCSjOd`!|UXQsGsYPY?cN=9nl8FM>fyUU8@8;J@MddwUf^?EQ z%8;o=xxx#fWQ+L`%u!YHT^3Ggi9x8IXiv$grqo-PYj2{;G7u(MugQew%Mgdu(gw6J z6X3jIqL@h}i$9GQG2uy$-?{LJvf8d6*@yOaFYa0-9wsr?Vm7?$*_^Jo3~;<=`lxdz za+Uo`y>d1n0l15t{!mjqtd9nH4=3Zg!X<*s*&28Go|IuF`A?UE!y%taF{Ek9ujNp( zRIaQq#c2X<&X~<+yBv=;z*asYvFQ^go3&?>&;kylM5pfh9OTmy-e7#&X=5PARoCBc znFhSlK_f#E;_<^8u(r@5}eIPQDR`>T-V<){bm7s*tG6Xb}a z>Y;y%Jh7jDMN=)aLkqG$pk$I)Jvq%4o>Bp4aZcAXcq^C5Q+E>+;R;+$xyDHXiKyFr*X2#k>W2OUk2uXd zv#1|o_*%Yqjdz8$uefO~Z2rkh^j9puJLi2FLdUZMse-yNCos1R_^68Y9jC>WK7 zD?+=|YBu@krfcj;_L8mR{J!4zhSBN9%a}soU%{y$+EdLpW0Gn@kl+r;QAVIl5VG7x zt!7raCJ9k95#&D(V{gV^(GHEc{Pexto*XX%-R=3H?~|9oFlp@GE|B*N31eeSvlUKT zGAfsO=YHzJ(E3~MZjVo2mP`Og&C)O6N*PB+W1l=!vI81W$iW)xH#<}Q$U}->Rifo- z?b}qqt6i2#%&w|_fso(l$wRPzt$RtY<64mO;Okh=yP%^d1yx$`)d#=0&p)_ky9NF*#)I8t>BoFyn~Mf#;d*9xJJ6-aP$`IuqtQsg}{>G<=u1GLnyRaUJ|>t_#*W?H+u`O zh=2qiKgn~wSpwk$Uf_{BTl&>braN3l6+(nB zxzKtJOTHeMRSEt?jVYOHwmBYrhi^j-oxWc!BwUm${6p~e19I*^U9~@Yu{h&j{sRjK zFm6dYV1NI!N7qGhJPHSDJ=zcxDj|G+!h$MoOoISOtawJ8g>_*-JyJ z&q+dnana=8L;O~spX2#BdlLvf6d2+OR#c{sWpvtnix8v>DI6gxGZ)3$E(zHatw&y2 z*&$--S7SGUp}JeuJ{<0En2$;FWGC_)duS=WEQ>IyGEd7H+`Fu~`!X}Ykj$>xm`Hd- z+i3-Ua)E8+D;}S9QPm+Ee#@1k&zr^SVh?H3eIM4z5q8H9{g+&uzAttsz8x=&L~Q(&>^~kLMMei%T``by45o{f(yv( zYwO`B-m|5FXIS1vTO}hrCNVfPKVk$sg5+!|i4|t^z~?r!GXtG1lf>9Kk@0ue=wg;z z3yT_oI&9-yrslvFQ@Q555qX%IaUee!UG2Xx3$}ZFI7yv7P%?m|JBa2SEo0?^N+Gk6 zHsPuux!s)M?zbPYbz;a<^X*g&96PhqqXA5k-AOg?elaq-wt?4mq@*2;XqJQs>qU_n`6<7fH9PHY1N!&9ahgXSM_?~rk-dZjNCsN~aX3KgA`ySVJ+SZ$UUy02{=IIB zkcDX~OYmTfEg=0A%T_tS>LGi`Q{nlm%iZ-NO_l@KN1f|$>Ei_t_+|FvRNwwjQxzYe zFWYtaLDROstfGIVXL#wxbac3wZnf6Y zQYK>YbMXTkFq4L0PQiCPodNcy%%{U=2+qH6!5Ph=?FijLLI#5p1f!ZfVGNv9R-qTo zvWU@a!N3E>i;EQ@*H#{Y+hV=`FnU)3OU`{Z)=~e#Q&I z^YI0VX?_@wU_~P+NdsA1Na%6J_m@(#Sl{2Fx8y`!@=qv7JHGlx{Tb;R3Wp8q0r8Fr z&hn?z-!_XM$of41$Vk$ULW3r`aEI3MKc4ejD?N}3nocpH=<0@eyuSzf4kQ(ZMQlkk zq*m1Pg7^O>PH)pPX4e8SxeD^VJq_@0W}+)MLdGb6@}}28;{fK8tS^=Y6IlWWl!mwX zq{0=1mE4DFvkLh@aLbooasO~XX)79#NgvRQ$eMpYF*CBf6Fx}Kh*J|JZbE^Az?du1O(Vl)M3@<5RXXYcz4KM$= zTr@2$t6&E7DRvNWXYd<bFETP`{X z=4a3O7}T-ac0spkR5qfTMv6}Hmr*CYEi$vA!`w1ZT$fB|EFv&a+*Q;bX?ldQa=5y; zh6^(%HOkn0pbRzaJWGq>er|G=lX+O_x<>saJZpZ``LnqV>NO^d!SJhl!I2--LGtkX zOO-H5A61*H#&`I%Q^AW5N1dG!`Bp+x3*t-_XNyQb3%7dQC-94JkbaIeWz~ zxP%Oamnx(1XWF+_h~*r0_VI?n!Fed5xyfPF1^cNoY+x3{rqi_ zwDsmD6=eVON2!j`F$D%!&^wMb)iyU-ZpEs_p8|iUoTntYCdY;d;2BXcV~3-eSWG%F zUsEC_HcKLnXLkk;vyWgDN(Gc~Fb7n>YpeqQ8Z)=6R1oB2c1_;|l+T`q7=MjdGg zeJgGER`aStsjHGqs0bVaPY6XpPE5z*3HM}}m^?Pn5SD^ye)QvGi(i-nEr4>dhYs|;A++(iUYit6>`fKnqIO#7 zA9Q}PKOHs)sQIj44`?P^kC&nCpcW^p=-q}aCN1`|c-eMvKRnCkl*6(7;RfFKDwv&v z+%?AuiyJgnY{VV`+UVcDs-Ou_RTns3c3QHFxGjC6i0IwI)E7K`^NQ`@8Y4wNv9TRj zs2iF&q##p-&w;amuURJy;Nk=1;G@;$`=!$!yW&ty^uckC<$FWho<`n#ybMoHR?6C* z0ra@3Wr`_+0stP|6W{R*2vB|WX2D<)k@Qg*h#(GwDukD-Ljug)BCpZ}SB>(gVscJ~ z5PBv*Trc5Rl6PgBE0uHiBf0Rv%en2u#QvuXj%jxEjIR40^GUZslLc5z@n=$BbCRwZ zEaErWcTW2hiK!v47QRvqC4&qh1~8Br$}I|t-7*^{Id>D&d&+pGW(-YoQbPZ9J^qEE ziwCBhr#RMf@5#b?-w)4)EbiaCC%(=~*{Gmg9Cn%do~#vCHRo4a^GUG`Y|NT?k*Wu! z?=D-%Y@ZfSyzM>ooQ#bPN6W;#potNb(m95dTeUlS-9s;YI%+ahKj}%B=_k=`<&*Td zu}cVo`@i<9Aen>j1w>|?)pzd~^*gMll66bm7>LyhNZ{cR5~7@+lGV$XEuMFiZII9toD(ESVL z(n{i}lJKg^%~4VKb({;9$0+bsxS;`t2-v;ls&VTZK`cCAj5vGIarA1U(QX|-rXh6Z zi_j6bD>#c!FMcJ`he>)**ZbJ)g?I z62@0Us6fc^r@gRF3l$-ej-ks(Y$LAxRmIB5^Ht;Jyo!`F#+HD<-&jt&{fhu2_vCg{ zM$^5yhO5E4ym4>1*%L?bqu#8+o^iFyvI)Wi!HA(ySo7zH(M;~iL0^^`mnEC~JxU~YZLZ_C1Dtm3Q}0A4v8Q6So=6pg_$d=T zdfyHF58#{kgrxUQSV)s$;5DO%;}ECugCs-_V8n_we<9v*;p`PVr6As1#B0SKl6=qM zWv+`Y$kN1ieLAZWq*QS%E*f*RiFB61jJo#lnp^HWZjT@a(*dON9WKPP3BsmF6k$Q` zvCS;q)&MD7NZOxkre3c#`*GTm@>vI7PJavkN?X);I_{qSJ>>m(`AcqgN^({ONb%s8fz@X>i8DlZhMzg=%ijy<( zBV!D^MlZN&eGoePa*MCBHfHX|B1BjrcyB!4+CKhN*>}W=<#4onkkjRZc?pTwaZj~? z|M?+&tFYW6tF6EQRCU3uDxg(hv$}h3>$(2yxMeKm>tvXg*r*GVN#gXHN7xie(X>(Rb5%k^<1;g7vqbw&oUGy90R827kG8jpyrhddmP-m{?;yoM1;;M=+OVkm(0z3rPDyU|5<66P` zByS!@**%XF{(>$7gOW`vo*mWX>*GK`6&ki|B?TgQrX?iDR!#Qd$M*P$^g95MpDY~E z%Uk1$l4T#i=li}|gNv9utDhN4b>C;T-dc?ZjSPKTt$zE#?#wI&XxShAFTx~eY$ZX# z@ElJ&-ssR19=mR2Bw1OgrpcIRL=Nzuv_uVsnLFO(^ zmXjvIKRT|q_r8;Ikc@ac6=@1YocFbV3-2H<(eYp?ibEV0OdhAvw>*RDG-BB&Ew&8z z%j6v}GbS^pUvi8Yxu)r9oF4kTREXF5lk_B6NU|#BSonce3IH^N2N=jL@ zja;YmDl&#uBrm>o2=ZMI!;@?KU^-fTL4dsIw^dfdopy(S|5 z4K4}l8f-dcc!Pnx&yP@FEd5upti*#~*i@+TUlCt_&1-U!hH>N!gvX9LUJ*d6-3x899b znqD#uC=%42--`C`eaun+Uj(XCk-6BEe1zV#T;4}W+8fc_^+JwFHf3(Fj(IeUB1y?- zs#?tKd-fdo%_$F0@`X>(H0d_J;3{qsFhTsIk!SyAdd^{t=3@l0j@@Y=BpmOJSNk-! z08jLNgk?vvYDC07ITTA3f>9=w`JOdvJk995_@=dK)-T~_1Zq1Ij3Q^Si`eYMACkfvXz$#J_JHs$<|Q#b%*fznbC)% z4bwJLFmei(Yh^Ck!5D>Mn@DJRDq=1JGyOh_Y_6hLo6`>pL3*-=L$|_=f9a1dvomJo}|}cmgulc zsf!P9S-3(L$j_NVhijbV6K>$BS!W3*8_#`6%HXdPhnVcbTOXW2l3_9}%LK6v(U0b1 zj^H++DmJgIGJCvF^ZMLQh`xGLu(pJ&iXYh0?UJPdaZcG%kOFgVD|Jh$H=u_i);G+7L$2O zw#suAOdccFM+1Zru}tBexqggw;(XY+;fDN0!vplsh$~tB8Ri3#Jos^2ubI4eRb2$T zz65DkSLHdaL946H*2m&Kaib#^lHVE@P)|6d-W}uKyE>|(X9Y<|v(H=oYaL#|o$jmOJTV#(>#hdjEI{GDIme)N3p&k2kYMP`Qk@KtGP zya;bDD+j6>#0XFp9sZ$*z{Mr z?ojzX(^^&;r1uFF-$&ayimw-*JfVJ0L%M(sm&Pl9Tok*R`oK)E@1`Oa;YUcORxjo! zfuIvtRU5Z}&cg_z9zyuBEew6*xBR+Atd357G5@p#R1R<_y{Rj^6{GL?W^mb0U4!?j8W zsq1>QK5muwuFugUa_0lm#}pIG%!2Tdq5j5993%J^Y&gl*tdO2&@F8t%@Iu@^a>TZ; z&7AJ{v&LaYKfe&1d=B@)(iNECcTr)gk5%AWfu`JpIQ5V^`9B@|Baa7;1)V_|#aTm+ z+SR)2^E6L#1JI!gIdS+7VA-_KN{&rv3|ImfZ5$-ACEU;V2PX4{Z4OBF7s>=t(v_Eo zqxx07T`G6ur!JQoQ;YG%rR+*IfGCd85xjQjXcoSMY4#`RA#X&%1KMOkMYsy==IAVd zZ%sSxitL<2L2tVIvo7(ez~k#l=$!%Pg7 zf%{hjphUL1)wyd`yGqas3WbHhlM?3#D|FvaaA_zH3hA|p(zR;F@+jEfc$d$h)8J^d zys6!=!vwR?Zd9&M^Od<$|6b^nkZTi+S)F(2tWA9NjND9MhR{UkkheQVsI7`a%577# z27{#x#|qYI{?X!)11uRpB80GY|K{@oKy#p_Wj&v!=k|JzGX{^u>SIz#uNY}!s_g)# z#@!#0#}a5YQXTj}m!m7{kGPYS&$O4SW!K zMU@-!jGNRFv=SOjoiC)rgNg+QW=fAm;Ibf8$j=KWGJRj<-Xo= zp3Xdk!!DWRvn9+vVWWN`3^W&-X+wG=t)>FotzWg7ubpvp8sl#WGAip9TN>r=nq*!~ z7Xr~$^3!P#0D48Nigi1tHeX;9$k}Z7O}&w>kBu%pwCBP{aT8TbLMAOYi{i|ed)4Sf zM2l`Qi#x;DccXDOw#){z#ve*oEMKJlbF~_6ecY3O*ZwUFoB=pflc9rX!fd!m1VUvj zPniG}Pi;U-1sT~8h6=5V`c=C@JsNH>#2%b#p$E`yjNL-mj_#}m+s`<%70E+tvRd7@ zLhi{R?i!#@KVFC%WMiyIhla(k|2Rv&a=uj#Fzm*}i|sE62I&%^Ii{YMPRc`*Vj6jR zQwsK%s0L)$4Ab%nW+3BLQn%2cuwHbGUo*D3e^^_Q`NKiiLr&+UPHV-RD;P3pXpC=m zz`aPX*js9|c+FzxBx^$Z!O@OphZ_au7DCfkMiCzipVc#bUI^e`lFqxj90W-}1w0s6 z!U7-zinuTFrT3kbmyAI5WCLJ~%SDF3Q`>H1{b|frq`c+-EC1%HZJx!r#{ncVdGcX` zN`(?AB2z;o&dY5Xj+bbi88gajzo}&1#xq)A+wquHd+q<{54HE0OV%hLa(TC6lrzL5`@4A(IqIhZ|Hh6v1qY_jYfZ+VuBA`J1E~lpQI<1$#5zIs3U=de6Dh?*^pCAuh)zo4L^@YiG^>;iz8y!P zWp#-~Xy-@SiUaF(CU99SrT%GD*X`N)a$rIycOcq#XeyAONDgEgCFePkbFH|iKA8mf z0Y=TOOU!r_zY~=rBgRjxrpTAd_V7Hl$|$wrURiVMV#Mw`H`5UhzFV#xz$HyZvlGV} z`$hBM=C-(3BqLSLp@{5VF+YJ)2yTHdZp*gglUZLU{U^_a{UApa3*g3Ws3Y&wn+ANI zJV_T)&sD{t*XF#*2)t1c7|^RPtH<9hVi<;+@kuStzgcfE>qMLV!h3#G^-gK zBLl;M$UlONS;d<@%{WA-ccCrksi>+@_LkGi%Q)=?QW1MyVfV&@J>LXvb8(;X)NaAz z<#;<(h_EroN2ZG8wue?45fP~0bU&a~O03jEihW+;!1h++MFt2K(q%Yfc8-5^{vO?o zH?6ZeGJ+3I(|L>U6P0$)J-a%jZ2FWr&_dS>wf^0clxUU?=lC?rBkiN}xgmyf7Ifvc zp7p0~Xre=Mh?;|9fnR2nw1oWYFE07MXudDy8!8j=+<23@bI}^% z!IH4Oi9Xd!he?P4L*orGeKCK5EoHd08I`YAA)2Wx7rN)5`auYj1@KW@m4lsZ(0HHj z0dLOITU~kNBL-MMM4LHHJz?^1VfnkYn!jstTnMx^<^9eN3_6VaYh32_QiebnX3{EP z>-pVwh3AmJxK^T?o)KXXhin>VL+Ps#`P&ir)MW{O3rZVCO(WTY0nicV%%~}}Z!U+H$ z*2!z)>ueu|lIs<-sNFqS)1e)X>a%K%yZ1LMn@rEy^$FSL$sfcfPEP|~=?Iu*R^j8r z3S*4IlC$pUcJ;CK;>>vUh!R!1pH-LH{pU}jra|2;-bL}TzzF~4`SpIWF=oq&B1>6? z2r#2DHAY}}I8DXA&=C%x^C%O6uQKqm2ac}POv@5=u&7N{=Ro@*r zDHDQ>4;z}K*e|Tb^v9xMkG`;e_lj0CL=ij6z148B0SSZ3Yllj4|1emjPZs_qektbh zL!LIn3CL8CRdHS;{n2W>fZAqLfYv_CphTWm(4_Jk4d0xV&``LX@~!>`9Uv$>T&G5u zU^S_~w<824n6P|rAKv(cd3z+x997+@*G=0x6%;?xW`$uH`#`Xqtx3u|lDuoZyEWic zWIPSZum~aOF_4~bFG@IGNfqJ- zZ-2H^h$J+LH_C}WmjOS3vu2QU+8a$-b^wV8SSo zROkN@u#JBT2{+EVIX^f%c=`-9r-SLC~XF%Hx z^bnTXZRN5>2t}?1eTY><_`H+T;k32uISO_^qiJ(AD9WK{C2cjJM5&pT+s0D!b$%k% z<43F*qR_Cmu1OS-�G4a1d<IACqr>G!rr)ME{+uUH0#n1?6Vd7eZXBG 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 JRabvEE&%SN8 + +----- + +Please note that this tutorial is a work in progress. We highly recommend +reading the haddock documentation and reading the Well-Typed blog, which +are offer the best quality sources of information at this time. + +In order to go through this tutorial you will need a Haskell development +environment and we recommend installing the latest version of the +[Haskell Platform](www.haskell.org/platform/) if you've not done +so already. + +Once you're up and running, you'll want to get hold of the distributed-process +library and a choice of network transport backend. This guide will use +the network-transport-tcp backend, but the simplelocalnet or inmemory +backends are also available on github, along with some other experimental +options. + +### Creating Nodes + +Cloud Haskell's *lightweight processes* reside on a 'node', which must +be initialised with a network transport implementation and a remote table. +The latter is required so that physically separate nodes can identify known +objects in the system (such as types and functions) when receiving messages +from other nodes. We'll look at inter-node communication later, so for now +it will suffice to pass the default remote table, which defines the built-in +stuff Cloud Haskell needs at a minimum. + +Our TCP network transport backend needs an IP address and port to get started +with, and we're good to go... + +{% highlight haskell %} +main :: IO () +main = do + Right (t, _) <- createTransport "127.0.0.1" "10501" defaultTCPParameters + node <- newLocalNode t initRemoteTable + .... +{% endhighlight %} + +And now we have a running node. + +### Messages + +We can start a new lightweight process with `forkProcess`, which takes a node, +a `Process` action - because our concurrent code will run in the `Process` +monad - and returns an address for the process in the form of a `ProcessId`. +The process id can be used to send messages to the running process - here we +will send one to ourselves! + +{% highlight haskell %} +-- in main + _ <- forkProcess node $ do + -- get our own process id + self <- getSelfPid + send self "hello" + hello <- expect :: Process String + liftIO $ putStrLn hello + return () +{% endhighlight %} + +Lightweight processes are implemented as `forkIO` threads. In general we will +try to forget about this implementation detail, but for now just note that we +haven't deadlocked ourself by sending to and receiving from our own mailbox +in this fashion. Sending a message is a completely asynchronous operation - even +if the recipient doesn't exist, no error will be raised and evaluating `send` +will not block the caller. + +Receiving messages works the other way around, blocking the caller until a message +matching the expected type arrives in the process (conceptual) mailbox. +If multiple messages of that type are in the queue, they will be returned in FIFO +order, otherwise the caller will be blocked until a message arrives that can be +decoded to the correct type. + +Let's spawn another process on the same node and make the two talk to each other. + +{% highlight haskell %} +main :: IO () +main = do + Right (t, _) <- createTransport "127.0.0.1" "10501" defaultTCPParameters + node <- newLocalNode t initRemoteTable + _ <- forkProcess node $ do + echoPid <- spawnLocal $ forever $ do + r <- receiveWait [ + match (\((sender :: ProcessId), (msg :: String)) -> send sender msg >> return ()) + , match (\(m :: String) -> say $ "printing " ++ m) + ] + -- send some messages! + self <- getSelfPid + send (self, "hello") + m <- expectTimeout 1000000 + case m of + Nothing -> die "nothing came back!" + (Just s) -> say $ "got back " ++ s +{% endhighlight %} + +Note that we've used a `receive` class of function this time around. The `match` +construct allows you to construct a list of potential message handling code and +have them evaluated against incoming messages. The first match indicates that, +given a tuple `t :: (ProcessId, String)` that we will send the `String` component +back to the sender's `ProcessId`. The second match prints out whatever string it +receives. + +Also note the use of a 'timeout' (given in microseconds), which is available for +both the `expect` and `receive` variants. This returns `Nothing` unless a message +can be dequeued from the mailbox within the specified time interval. + +### Serializable + +Processes can send data if the type implements the `Serializable` typeclass, which is +done indirectly by implementing `Binary` and deriving `Typeable`. Implementations are +already provided for primitives and some commonly used data structures. + +### Typed Channels + +Channels provides an alternative to message transmission with `send` and `expect`. +While `send` and `expect` allow transmission of messages of any `Serializable` +type, channels require a uniform type. Channels work like a distributed equivalent +of Haskell's `Control.Concurrent.Chan`, however they have distinct ends: a single +receiving port and a corollary send port. + +Channels provide a nice alternative to *bare send and receive*, which is a bit +*unHaskellish*, because the processes message queue has messages of multiple +types, and we have to do dynamic type checking. + +We create channels with a call to `newChan`, and send/receive on them using the +`{send,receive}Chan` primitives: + +{% highlight haskell %} +channelsDemo :: Process () +channelsDemo = do + (sp, rp) <- newChan :: Process (SendPort String, ReceivePort String) + + -- send on a channel + spawnLocal $ sendChan sp "hello!" + + -- receive on a channel + m <- receiveChan rp + say $ show m +{% endhighlight %} + +Channels are particularly useful when you are sending a message that needs a +response, because the code that receives the response knows exactly where it +came from. Channels can sometimes allows message types to be simplified, as +passing a `ProcessId` to reply to isn't required. Channels are not so useful +when you need to spawn a process and then send a bunch a messages to it and +wait for replies, because we can’t send the `ReceivePort`. + +ReceivePorts can be merged, so you can listen on several simultaneously. In the +latest version of [distributed-process][2], you can listen for *regular* messages +and on multiple channels at the same time, using `matchChan` in the list of +allowed matches passed `receive`. + +The [Control.Distributed.Process.Platform.Async][3] API provides an alternative +type safe mechanism for receiving data in request/reply scenarios. This relies +on spawning insulating processes and using either channels or STM internally, +so whilst it provides a neat API, there are some overheads involved. The +`ManagedProcess` API uses this mechanism to great effect for dealing with +client/server style interactions. See the [ManagedProcess.Client][4] APIs and +platform [documentation](/documentation.html) for further details. + +### Linking and monitoring + +Processes can be linked to other processes, nodes or channels. Links are unidirectional, +and guarantee that once the linked object *dies*, the linked process will also be +terminated. Monitors do not cause the *listening* process to exit, but rather they +put a `ProcessMonitorNotification` into the process' mailbox. + +### Spawning Remote Processes + +In order to spawn a process on a node we need something of type `Closure (Process ())`. +In distributed-process if `f : T1 -> T2` then + +{% highlight haskell %} + $(mkClosure 'f) :: T1 -> Closure T2 +{% endhighlight %} + +That is, the first argument the function we pass to mkClosure will act as the closure +environment for that process; if you want multiple values in the closure environment, +you must tuple them up. + +In order to spawn a process remotely we will need to configure the remote table +(see the documentation for more details) and the easiest way to do this, is to +let the library generate the relevant code for us. For example (taken from the +distributed-process-platform test suites): + +{% highlight haskell %} +sampleTask :: (TimeInterval, String) -> Process String +sampleTask (t, s) = sleep t >> return s + +$(remotable ['sampleTask]) +{% endhighlight %} + +We can now create a closure environment for `sampleTask` like so: + +{% highlight haskell %} +($(mkClosure 'sampleTask) (seconds 2, "foobar")) +{% endhighlight %} + +The call to `remotable` generates a remote table and generates a definition +`__remoteTable :: RemoteTable -> RemoteTable` in our module for us. We can +compose this with other remote tables in order to come up with a final, merged +remote table for use in our program: + +{% highlight haskell %} +myRemoteTable :: RemoteTable +myRemoteTable = Main.__remoteTable initRemoteTable + +main :: IO () +main = do + localNode <- newLocalNode transport myRemoteTable + -- etc +{% endhighlight %} + +### Stopping Processes + +Some processes, like the *outer* process in the previous example, will run until +they've completed and then return their value. This is just as we find with IO action, +and there is an instance of `MonadIO` for the `Process` monad, so you can `liftIO` if you +need to evaluate IO actions. + +Because processes are implemented with `forkIO` we might be tempted to stop +them by throwing an asynchronous exception to the process, but this is almost +certainly the wrong thing to do. Instead we might send a kind of poison pill, +which the process *ought* to handle by shutting down gracefully. Unfortunately +because of the asynchronous nature of sending, this is no good because `send` +will not fail under any circumstances. In fact, because `send` doesn't block, +we therefore have no way to no if the recipient existed at the time we sent the +poison pill. Even if the recipient did exist, we still have no guarantee that +the message we sent actually arrived - the network connection between the nodes +could have broken, for example. Making this *shutdown* protocol synchronous is +no good either - how long would we wait for a reply? Indefinitely? + +Exit signals come in two flavours - those that can +be caught and those that cannot. A call to +`exit :: (Serializable a) => ProcessId -> a -> Process ()` will dispatch an +exit signal to the specified process. These *signals* can be intercepted and +handled by the destination process however, so if you need to terminate the +process in a brutal way, you can use the `kill :: ProcessId -> String -> Process ()` +function, which sends an exit signal that cannot be handled. + +------ +#### __An important note about exit signals__ + +Exit signals in Cloud Haskell are unlike asynchronous exceptions in regular +haskell code. Whilst processes *can* use asynchronous exceptions - there's +nothing stoping this since the `Process` monad is an instance of `MonadIO` - +exceptions thrown are not bound by the same ordering guarantees as messages +delivered to a process. Link failures and exit signals *might* be implemented +using asynchronous exceptions - that is the case in the current +implementation - but these are implemented in such a fashion that if you +send a message and *then* an exit signal, the message is guaranteed to arrive +first. + +You should avoid throwing your own exceptions in code where possible. Instead, +you should terminate yourself, or another process, using the built-in primitives +`exit`, `kill` and `die`. + +{% highlight haskell %} +exit pid reason -- force `pid` to exit - reason can be any `Serializable` message +kill pid reason -- reason is a string - the *kill* signal cannot be caught +die reason -- as 'exit' but kills *us* +{% endhighlight %} + +The `exit` and `kill` primitives do essentially the same thing, but catching +the specific exception thrown by `kill` is impossible, making `kill` an +*untrappable exit signal*. Of course you could trap **all** exceptions, but +you already know that's a very bad idea right!? + +The `exit` primitive is a little different. This provides support for trapping +exit signals in a generic way, so long as your *exit handler* is able to +recognise the underlying type of the 'exit reason'. This (reason for exiting) +is stored as a raw `Message`, so if your handler takes the appropriate type +as an input (and therefore the `Message` can be decoded and passed to the +handler) then the handler will run. This is pretty much the same approach as +exception handling using `Typeable`, except that we decide whether or not the +exception can be handled based on the type of `reason` instead of the type of +the exception itself. + +Calling `die` will immediately raise an exit signal (i.e., `ProcessExitException`) +in the calling process. + +------ + +[1]: /static/doc/distributed-process/Control-Distributed-Process.html#v:Message +[2]: http://hackage.haskell.org/package/distributed-process +[3]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html +[4]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.htmlv:callAsync diff --git a/tutorials/2.nt_tutorial.md b/tutorials/2.nt_tutorial.md new file mode 100644 index 00000000..68d1346a --- /dev/null +++ b/tutorials/2.nt_tutorial.md @@ -0,0 +1,294 @@ +--- +layout: nt_tutorial +categories: tutorial +title: Programming with Network.Transport +--- + +### Introduction + +This is a tutorial introduction to `Network.Transport`. To follow along, +you should probably already be familiar with `Control.Concurrent`; in +particular, the use of `fork` and `MVar`s. The code for the tutorial can +be downloaded as [tutorial-server.hs](https://github.com/haskell-distributed/distributed-process/blob/master/doc/tutorial/tutorial-server.hs) +and +[tutorial-client.hs](https://github.com/haskell-distributed/distributed-process/blob/master/doc/tutorial/tutorial-client.hs). + +------- + +### The Network Transport API + +Network.Transport is a network abstraction layer which offers the following concepts: + +* Nodes in the network are represented by `EndPoint`s. These are heavyweight stateful objects. +* Each `EndPoint` has an `EndPointAddress`. +* Connections can be established from one `EndPoint` to another using the `EndPointAddress` of the remote end. +* The `EndPointAddress` can be serialised and sent over the network, where as `EndPoint`s and connections cannot. +* Connections between `EndPoint`s are unidirectional and lightweight. +* Outgoing messages are sent via a `Connection` object that represents the sending end of the connection. +* Incoming messages for **all** of the incoming connections on an `EndPoint` are collected via a shared receive queue. +* In addition to incoming messages, `EndPoint`s are notified of other `Event`s such as new connections or broken connections. + +In this tutorial we will create a simple "echo" server. Whenever a client +opens a new connection to the server, the server in turns opens a connection +back to the client. All messages that the client sends to the server will +echoed by the server back to the client. + +Here is what it will look like. We can start the server on one host: + +{% highlight bash %} +# ./tutorial-server 192.168.1.108 8080 +Echo server started at "192.168.1.108:8080:0" +{% endhighlight %} + +then start the client on another. The client opens a connection to the server, +sends "Hello world", and prints all the `Events` it receives: + +{% highlight bash %} +# ./tutorial-client 192.168.1.109 8080 192.168.1.108:8080:0 +ConnectionOpened 1024 ReliableOrdered "192.168.1.108:8080:0" +Received 1024 ["Hello world"] +ConnectionClosed 1024 +{% endhighlight %} + +The client receives three `Event`s: + +1. The server (with address "192.168.1.108:8080:0") opened a connection back to the client. The ID of this connection is 1024, and the connection is reliable and ordered (see below). +2. Received a message on connection 1024: that is, on the connection the server just opened. This is the server echoing the message we sent. +3. Connection 1024 was closed. + +Note that the server prints its address ("192.168.1.108:8080:0") to the +console when started and this must be passed explicitly as an argument to +the client. Peer discovery and related issues are outside the scope of +`Network.Transport`. + +### Writing the client + +We will start with the client +([tutorial-client.hs](https://github.com/haskell-distributed/distributed-process/blob/master/doc/tutorial/tutorial-client.hs)), +because it is simpler. We first need a bunch of imports: + +{% highlight haskell %} +import Network.Transport +import Network.Transport.TCP (createTransport) +import System.Environment +import Data.ByteString.Char8 +import Control.Monad +{% endhighlight %} + +The client will consist of a single main function. + +{% highlight haskell %} +main :: IO () +main = do +{% endhighlight %} + +When we start the client we expect three command line arguments. +Since the client will itself be a network endpoint, we need to know the IP +address and port number to use for the client. Moreover, we need to know the +endpoint address of the server (the server will print this address to the +console when it is started): + +{% highlight haskell %} +[host, port, serverAddr] <- getArgs +{% endhighlight %} + +Next we need to initialize the Network.Transport layer using `createTransport` +from `Network.Transport.TCP` (in this tutorial we will use the TCP instance of +`Network.Transport`). The type of `createTransport` is: + +{% highlight haskell %} +createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) +{% endhighlight %} + +(where `N` is an alias for `Network.Socket`). For the sake of this tutorial we +are going to ignore all error handling, so we are going to assume it will return +a `Right` transport: + +{% highlight haskell %} +Right transport <- createTransport host port +{% endhighlight %} + +Next we need to create an EndPoint for the client. Again, we are going +to ignore errors: + +{% highlight haskell %} +Right endpoint <- newEndPoint transport +{% endhighlight %} + +Now that we have an endpoint we can connect to the server, after we convert +the `String` we got from `getArgs` to an `EndPointAddress`: + +{% highlight haskell %} +let addr = EndPointAddress (pack serverAddr) +Right conn <- connect endpoint addr ReliableOrdered defaultConnectHints +{% endhighlight %} + +`ReliableOrdered` means that the connection will be reliable (no messages will be +lost) and ordered (messages will arrive in order). For the case of the TCP transport +this makes no difference (_all_ connections are reliable and ordered), but this may +not be true for other transports. + +Sending on our new connection is very easy: + +{% highlight haskell %} +send conn [pack "Hello world"] +{% endhighlight %} + +(`send` takes as argument an array of `ByteString`s). +Finally, we can close the connection: + +{% highlight haskell %} +close conn +{% endhighlight %} + +Function `receive` can be used to get the next event from an endpoint. To print the +first three events, we can do + +{% highlight haskell %} +replicateM_ 3 $ receive endpoint >>= print +{% endhighlight %} + +Since we're not expecting more than 3 events, we can now close the transport. + +{% highlight haskell %} +closeTransport transport +{% endhighlight %} + +That's it! Here is the entire client again: + +{% highlight haskell %} +main :: IO () +main = do + [host, port, serverAddr] <- getArgs + Right transport <- createTransport host port + Right endpoint <- newEndPoint transport + + let addr = EndPointAddress (fromString serverAddr) + Right conn <- connect endpoint addr ReliableOrdered defaultConnectHints + send conn [fromString "Hello world"] + close conn + + replicateM_ 3 $ receive endpoint >>= print + + closeTransport transport +{% endhighlight %} + +### Writing the server + +The server ([tutorial-server.hs](https://github.com/haskell-distributed/distributed-process/blob/master/doc/tutorial/tutorial-server.hs)) +is slightly more complicated, but only slightly. As with the client, we +start with a bunch of imports: + +{% highlight haskell %} +import Network.Transport +import Network.Transport.TCP (createTransport) +import Control.Concurrent +import Data.Map +import Control.Exception +import System.Environment +{% endhighlight %} + +We will write the main function first: + +{% highlight haskell %} +main :: IO () +main = do + [host, port] <- getArgs + serverDone <- newEmptyMVar + Right transport <- createTransport host port + Right endpoint <- newEndPoint transport + forkIO $ echoServer endpoint serverDone + putStrLn $ "Echo server started at " ++ show (address endpoint) + readMVar serverDone `onCtrlC` closeTransport transport +{% endhighlight %} + +This is very similar to the `main` function for the client. We get the +hostname and port number that the server should use and create a transport +and an endpoint. Then we fork a thread to do the real work. We will write +`echoServer` next; for now, suffices to note that `echoServer` will signal +on the MVar `serverDone` when it completes, so that the main thread knows +when to exit. Don't worry about `onCtrlC` for now; it does what the +name suggests. + +The goal of `echoServer` is simple: whenever somebody opens a connection to us, +open a connection to them; whenever somebody sends us a message, echo that message; +and whenever somebody closes their connection to us, we are going to close +our connection to them. + +`Event` is defined in `Network.Transport` as + +{% highlight haskell %} +data Event = + Received ConnectionId [ByteString] + | ConnectionClosed ConnectionId + | ConnectionOpened ConnectionId Reliability EndPointAddress + | EndPointClosed + ... +{% endhighlight %} + +(there are few other events, which we are going to ignore). `ConnectionId`s help us +distinguish messages sent on one connection from messages sent on another. In +`echoServer` we are going to maintain a mapping from those `ConnectionId`s to the +connections that we will use to reply: + +* Whenever somebody opens a connection, we open a connection in the other direction and add it to the map. +* Whenever we receive a message, we lookup the corresponding return connection and echo the message back. +* Whenever somebody closes the connection, we lookup and close the corresponding return connection. + +Finally, when we receive the `EndPointClosed` message we signal to the main +thread that we are doing and terminate. We will receive this message when the +main thread calls `closeTransport` (that is, when the user presses Control-C). + +{% highlight haskell %} +echoServer :: EndPoint -> MVar () -> IO () +echoServer endpoint serverDone = go empty + where + go :: Map ConnectionId (MVar Connection) -> IO () + go cs = do + event <- receive endpoint + case event of + ConnectionOpened cid rel addr -> do + connMVar <- newEmptyMVar + forkIO $ do + Right conn <- connect endpoint addr rel defaultConnectHints + putMVar connMVar conn + go (insert cid connMVar cs) + Received cid payload -> do + forkIO $ do + conn <- readMVar (cs ! cid) + send conn payload + return () + go cs + ConnectionClosed cid -> do + forkIO $ do + conn <- readMVar (cs ! cid) + close conn + go (delete cid cs) + EndPointClosed -> do + putStrLn "Echo server exiting" + putMVar serverDone () +{% endhighlight %} + +This implements almost exactly what we described above. The only complication is that we want to avoid blocking the receive queue; so for every message that comes in we spawn a new thread to deal with it. Since is therefore possible that we receive the `Received` event before an outgoing connection has been established, we map connection IDs to MVars containing connections. + +Finally, we need to define `onCtrlC`; `p onCtrlC q` will run `p`; if this is interrupted by Control-C we run `q` and then try again: + +{% highlight haskell %} +onCtrlC :: IO a -> IO () -> IO a +p `onCtrlC` q = catchJust isUserInterrupt p (const $ q >> p `onCtrlC` q) + where + isUserInterrupt :: AsyncException -> Maybe () + isUserInterrupt UserInterrupt = Just () + isUserInterrupt _ = Nothing +{% endhighlight %} + +### Conclusion + +In this tutorial, we have implemented a small echo client and server +to illustrate how the `Network.Transport` abstraction layer can be used. + + + +See the [`Network.Transport` wiki page](https://github.com/haskell-distributed/distributed-process/wiki/Network.Transport) for more details. + + diff --git a/wiki.md b/wiki.md new file mode 100644 index 00000000..675dedec --- /dev/null +++ b/wiki.md @@ -0,0 +1,33 @@ +--- +layout: wiki +title: Cloud Haskell Wiki +wiki: Welcome +--- + +### Welcome + +Welcome to the Cloud Haskell Wiki. Navigate to specific pages using the links +on the left. If you wish to edit or add to the pages in this wiki, read on. + +### Editing + +Editing the wiki is pretty simple. This entire website is stored in a git +repository and its dynamic content rendered by github pages using [Jekyll][1]. +You can clone the repository [here][2]. Instructions for using jekyll are +available [online][1], but in general it's just a matter of finding the right +markdown file. Wiki content is all located in the wiki subfolder. + +### Adding new content + +New wiki pages need to have some specific fields in their [Yaml Front Matter][3]. +There is a makefile in the root directory which will create a wiki page for +you (in the wiki directory) and populate the front matter for you. Calling the +makefile is pretty easy. + +{% highlight bash %} +make wikipage NAME= +{% endhighlight %} + +[1]: https://github.com/mojombo/jekyll +[2]: https://github.com/haskell-distributed/haskell-distributed.github.com +[3]: https://github.com/mojombo/jekyll/wiki/YAML-Front-Matter diff --git a/wiki/contributing.md b/wiki/contributing.md new file mode 100644 index 00000000..c238dfe0 --- /dev/null +++ b/wiki/contributing.md @@ -0,0 +1,106 @@ +--- +layout: wiki +title: Contributor Guide +wiki: contributing +--- + +# Contributing + +Community contributions are most welcome! These should be submitted via Github's +*pull request* mechanism. Following the guidelines described here will ensure +that pull requests require a minimum of effort to deal with, hopefully +allowing the maintainer who is merging your changes to focus on the substance +of the patch rather than stylistic concerns and/or handling merge conflicts. + +This document is quite long, but don't let that put you off! Most of the things +we're saying here are just common sense and none of them is hard to follow. + +With this in mind, please try to observe the following guidelines when +submitting patches. + +### __1. Check to see if your patch is likely to be accepted__ + +We have a rather full backlog, so your help will be most welcome assisting +us in clearing that. You can view the exiting open issues on the +[jira issue tracker](https://cloud-haskell.atlassian.net/issues/?filter=10001). + +If you wish to submit an issue there, you can do so without logging in, +although you obviously won't get any email notifications unless you create +an account and provide your email address. + +It is also important to work out which component or sub-system should be +changed. You may wish to email the maintainers to discuss this first. + +### __2. Make sure your patch merges cleanly__ + +Working through pull requests is time consuming and this project is entirely +staffed by volunteers. Please make sure your pull requests merge cleanly +whenever possible, so as to avoid wasting everyone's time. + +The best way to achieve this is to fork the main repository on github, and then +make your changes in a local branch. Before submitting your pull request, fetch +and rebase any changes to the upstream source branch and merge these into your +local branch. For example: + +{% highlight bash %} +## on your local repository, create a branch to work in + +$ git checkout -b bugfix-issue123 + +## make, add and commit your changes + +$ git checkout master +$ git remote add upstream git://github.com/haskell-distributed/distributed-process.git +$ git fetch upstream +$ git rebase upstream/master + +## and now rebase (or merge) against your work + +$ git checkout bugfix-issue123 +$ git merge master + +## make sure you resolve any merge conflicts +## and commit before sending a pull request! +{% endhighlight %} + +### __3. Follow the patch submission *rules of thumb*__ + +These are pretty simple and are mostly cribbed from the GHC wiki's git working +conventions page [here](http://hackage.haskell.org/trac/ghc/wiki/WorkingConventions/Git). + +1. try to make small patches - the bigger they are, the longer the pull request QA process will take +2. strictly separate all changes that affect functionality from those that just affect code layout, indentation, whitespace, filenames etc +3. always include the issue number (of the form `fixes #N`) in the final commit message for the patch - pull requests without an issue are unlikely to have been discussed (see above) +4. use Unix conventions for line endings. If you are on Windows, ensure that git handles line-endings sanely by running `git config --global core.autocrlf false` +5. make sure you have setup git to use the correct name and email for your commits - see the [github help guide](https://help.github.com/articles/setting-your-email-in-git) + +### __4. Make sure all the tests pass__ + +If there are any failing tests then your pull request will not be merged. Please +don't rely on the maintainers to deal with these, unless of course the tests are +failing already on the branch you've diverged from. In that case, please submit +an issue so that we can fix the failing tests asap! + +### __5. Try to eliminate compiler warnings__ + +Code should be compilable with `-Wall -Werror`. There should be no +warnings. We *may* make some exceptions to this rule, but pull requests that +contain multitudinous compiler warnings will take longer to QA. + +### __6. Always branch from the right place__ + +Please be aware of whether or not your changes are actually a bugfix or a new +feature, and branch from the right place accordingly. The general rule is: + +* new features must branch off `development` +* bug fixes must branch off `master` (which is the stable, production branch) + +If you branch from the wrong place then you will be asked to rework your changes +so try to get this right in the first place. If you're unsure whether a patch +should be considered a feature or a bug-fix then discuss this when opening a new +github issue. + +### General Style + +Please carefully review the [Style Guide](/wiki/style.html) and stick to the +conventions set out there as best you can. diff --git a/wiki/maintainers.md b/wiki/maintainers.md new file mode 100644 index 00000000..0fb4fe52 --- /dev/null +++ b/wiki/maintainers.md @@ -0,0 +1,40 @@ +--- +layout: wiki +title: Maintainers +wiki: Maintainers +--- + +### Maintainers + +This guide is specifically for maintainers, and outlines the +development process and in particular, the branching strategy. + +#### Master == Stable + +The master branch is the **stable** branch, and should always be +in a *releasable* state. This means that on the whole, only small +self contained commits or topic branch merges should be applied +to master, and tagged releases should always be made against it. + +#### Development + +Ongoing work can either be merged into master when complete or +merged into development. Development is effectively an integration +branch, to make sure ongoing changes and new features play nicely +with one another. + +#### Releases + +Remember to update the change log for each project when releasing it. +I forgot to add the changes to the changelog when tagging the recent +distributed-process-0.4.2 release, but in general they should be added +*before* tagging the release. + +#### Follow the Contributing guidelines + +What's good for the goose... + +#### After releasing, send out a mail + +To the Parallel Haskell Mailing List, and anywhere else that makes +sense. diff --git a/wiki/networktransport.md b/wiki/networktransport.md new file mode 100644 index 00000000..63220b27 --- /dev/null +++ b/wiki/networktransport.md @@ -0,0 +1,114 @@ +--- +layout: wiki +title: Network.Transport +wiki: Overview +--- + +### Overview + +`Network.Transport` is a Network Abstraction Layer which provides +the following high-level concepts: + +* Nodes in the network are represented by `EndPoint`s. These are heavyweight stateful objects. +* Each `EndPoint` has an `EndPointAddress`. +* Connections can be established from one `EndPoint` to another using the `EndPointAddress` of the remote end. +* The `EndPointAddress` can be serialised and sent over the network, where as `EndPoint`s and connections cannot. +* Connections between `EndPoint`s are unidirectional and lightweight. +* Outgoing messages are sent via a `Connection` object that represents the sending end of the connection. +* Incoming messages for **all** of the incoming connections on an `EndPoint` are collected via a shared receive queue. +* In addition to incoming messages, `EndPoint`s are notified of other `Event`s such as new connections or broken connections. + +This design was heavily influenced by the design of the [Common Communication Interface/CCI][1]. +Important design goals are: + +* Connections should be lightweight: it should be no problem to create thousands of connections between endpoints. +* Error handling is explicit: every function declares as part of its type which errors it can return (no exceptions are thrown) +* Error handling is "abstract": errors that originate from implementation specific problems (such as "no more sockets" in the TCP implementation) get mapped to generic errors ("insufficient resources") at the Transport level. + +It is intended that `Network.Transport` can be instantiated to use +many different protocols for message passing: TCP/IP, UDP, MPI, CCI, +ZeroMQ, ssh, MVars, Unix pipes and more. Currently, we offer a TCP/IP +transport and (mostly for demonstration purposes) an in-memory +`Chan`-based transport. + +### **Package status** + +The TCP/IP implementation of Network.Transport should be usable, if not +completely stable yet. The design of the transport layer may also still change. +Feedback and suggestions are most welcome. Email [Duncan](mailto:duncan@well-typed.com) or [Edsko](mailto:edsko@well-typed.com) at Well-Typed, find us at #HaskellTransportLayer on +freenode, or post on the [Parallel Haskell][2] mailing list. + +You may also submit issues on the [JIRA issue tracker][3]. + +### Hello World + +For a flavour of what programming with `Network.Transport` looks like, here is a tiny self-contained example. + +{% highlight haskell %} +import Network.Transport +import Network.Transport.TCP (createTransport, defaultTCPParameters) +import Control.Concurrent +import Control.Monad +import Data.String + +main :: IO () +main = do + serverAddr <- newEmptyMVar + clientDone <- newEmptyMVar + + Right transport <- createTransport "127.0.0.1" "10080" defaultTCPParameters + + -- "Server" + forkIO $ do + Right endpoint <- newEndPoint transport + putMVar serverAddr (address endpoint) + + forever $ do + event <- receive endpoint + case event of + Received _ msg -> print msg + _ -> return () -- ignore + + -- "Client" + forkIO $ do + Right endpoint <- newEndPoint transport + Right conn <- do addr <- readMVar serverAddr + connect endpoint addr ReliableOrdered defaultConnectHints + send conn [fromString "Hello world"] + putMVar clientDone () + + -- Wait for the client to finish + takeMVar clientDone +{% endhighlight %} + +We create a "server" and a "client" (each represented by an `EndPoint`). +The server waits for `Event`s and whenever it receives a message it just prints +it to the console; it ignores all other messages. The client sets up a connection +to the server, sends a single message, and then signals to the main process +that it is done. + +### More Information + +* [Programming with Network.Transport][4] introduces `Network.Transport` from an application developer's point of view. +* [Creating New Transports][5] describes how to design new instantiations of `Network.Transport` for other messaging protocols, and describes the TCP transport in some detail as a guiding example. +* [New backend and transport design][6] has some notes about the design of the transport layer. Note however that this page is currently out of date. + +### How can I help? + +If you want to help with the development of `Network.Transport`, you can help in one of two ways: + +1. Play with the TCP implementation. Do the tutorial [Programming with Network.Transport][4]. Write some simple applications. Make it break and report the bugs. +2. If you have domain specific knowledge of other protocols (UDP, MPI, CCI, ZeroMQ, ssh, sctp, RUDP, enet, UDT, etc.) and you think it would be useful to have a Transport implementation for that protocol, then implement it! [Creating New Transports][5] might be a good place to start. Not only would it be great to have lots of supported protocols, but the implementation of other protocols is also a good test to see if the abstract interface that we provide in `Network.Transport` is missing anything. + +Note however that the goal of `Network.Transport` is _not_ to provide a general purpose network abstraction layer, but rather it is designed to support certain kinds of applications. [New backend and transport design][6] contains some notes about this, although it is sadly out of date and describes an older version of the API. + +If you are interested in helping out, please add a brief paragraph to +[Applications and Other Protocols][7] so that we can coordinate the efforts. + +[1]: http://www.olcf.ornl.gov/center-projects/common-communication-interface/ +[2]: https://groups.google.com/forum/?fromgroups#!forum/parallel-haskell +[3]: http://cloud-haskell.atlassian.net +[4]: /tutorials/2.nt_tutorial.html +[5]: /wiki/newtransports.html +[6]: /wiki/newdesign.html +[7]: /wiki/protocols.html diff --git a/wiki/newdesign.md b/wiki/newdesign.md new file mode 100644 index 00000000..13e1a371 --- /dev/null +++ b/wiki/newdesign.md @@ -0,0 +1,577 @@ +--- +layout: wiki +title: New Backend and Transport Design +wiki: Outline +--- + +Outline +======= + +This is a outline of the problem and a design for a new `distributed-process` (Cloud Haskell) implementation. + +Compared to the initial implementation, the aim is not to change the main API but to give more flexibility in the network layer, both in the transport technology: + + * shared memory, + * IP, + * HPC interconnects + +and in configuration: + + * neighbour discovery/startup + * network parameter tuning + +The approach we take is to abstract over a network transport layer. That is, we define an interface between the upper layers of the distributed-process library (the `Process` monad and all that) and the network transport layer. We keep the interface to the upper layers essentially the same but are able to switch the transport layer. + +### Use cases + + * networking for the `distributed-process` package + * networked versions of DPH + * other distributed/cluster programming use cases + +Non-use cases: + + * general client/server networking + * general internet services + +In addition to `distributed-process`, it is hoped that an independent transport layer will be useful for other middleware and applications. It is not the intention however to be compatible and interoperable with existing protocols like HTTP. This transport system is for use cases where you control all the nodes involved. It is not intended for use cases where you need to interoperate with other independent components. + +An good comparison is Erlang and use cases where people choose Erlang's native VM-to-VM network protocol rather than picking a standard protocol like HTTP. + +---- + +### Network transports + +There are "transports" that focus purely on transporting data. Examples are: + + * TCP/IP, UDP + * InfiniBand/Verbs (or one of their other 10 protocols or so) + * pipes (Unix/Win32) + * NUMA transports (shared memory) + * PCI transports + * Portals + * CCI + +The last two (Portals and CCI) are libraries that attempt to provide consistent semantics over multiple lower protocols. CCI appears particularly promising. + +Then there are transports embedded in generally much larger libraries, and +layered on one or more of the above. Examples are: + + * MPI + * ZeroMQ + * ssh + * SSL sockets + * HTTP connections + * RPC connections (many flavours) + +Generally this second group of transports have the following attributes: + +* The semantics are richer. +* There is a bigger overhead (especially noticeable for low-latency HPC + transports, less so for IP transports) +* There are large libraries of functionality available. Unless re-writing + those libraries from scratch is desirable, it's nice to have access to + all of it. +* The failure semantics are very different from that of the underlying low + level transport. + +Experience indicates that it is difficult to use a "fat" protocol in place of a thin one merely for the purpose of moving packets (due to the different semantics, and particularly failure semantics). If you use a fat one, you typically want to be using the richer semantics and/or library of additional routines. + +### Terminology: Addresses and Endpoints + +An address is a resource name. This is a value and has value semantics. In particular it can be serialised and deserialised. + +An endpoint is a networking level object. That is, it is a stateful object with identity and it has reference semantics. + +### Approach + +We want to provide a common interface to multiple different transports. We only intend to provide relatively limited functionality for sending messages between nodes in a network. We are primarily targeting lower level transports like IP not the higher level transports with richer semantics like HTTP. + +We want enough to allow code to be written against the interface to actually be reusable with multiple transports. We want to be able to take full advantage of the configuration/tuning parameters for each underlying transport so that we can maximise performance. These intentions tend to pull in opposite directions. + +To give us some leeway, we do not require that our common interface covers all the functionality of each of the underlying transports. We are happy to divide functionality between a common interface and interfaces specific to each transport backend. We do however want to maximise the functionality covered by the common interface so that we can maximise the amount of code that can be reusable between multiple transports. + +Looking at different transports, the area where they differ the most is in initialisation and initial configuration, addresses and per-connection performance parameters. Our approach to partitioning into common and per-backend interfaces is to put configuration, initialisation, initial neighbour creation/discovery and initial connection establishment into per-backend interfaces and to put everything else into the common interface. This enables us to write reusable code that works on the assumption that we already have connections to our neighbour nodes. From there we can create extra connections and send messages. + +A particular challenge is the per-connection performance parameters. It is vital for performance to be able to set these, but they differ significantly between transports. Our proposed solution to this is described below in the detailed design. + +---- + +### System outline + +The following diagram shows dependencies between the various modules for the initial Cloud Haskell implementation. Arrows represent explicit module dependencies. + + +------------------------------+ + | Application | + +------------------------------+ + | + V + +------------------------------+ + | Cloud Haskell | + +------------------------------+ + | + V + +------------------------------+ + | Haskell network (IP) library | + +------------------------------+ + +As the diagram indicates, the initial implementation is monolithic and uses a single specific transport (TCP/IP). + +The next diagram shows the various modules that are envisaged in the new design. We partition the system into the Cloud Haskell layer and a separate network transport layer. Each of the two layers has backend packages for different transports. + +{% highlight %} + +------------------------------------------------------------+ + | Application | + +------------------------------------------------------------+ + | | + V V + +-------------------------+ +------------------------------+ + | Cloud Haskell |<--| Cloud Haskell Backend | + | (distributed-process) | | (distributed-process-...) | + +-------------------------+ +------------------------------+ + | ______/ | + V V V + +-------------------------+ +------------------------------+ + | Transport Interface |<--| Transport Implementation | + | (network-transport) | | (network-transport-...) | + +-------------------------+ +------------------------------+ + | + V + +------------------------------+ + | Haskell/C Transport Library | + +------------------------------+ +{% endhighlight %} + +We still expect applications to use the the Cloud Haskell layer directly. Additionally the application also depends on a specific Cloud Haskell backend, which provides functions to allow the initialisation of the transport layer using whatever topology might be appropriate to the application. + +Complete applications will necessarily depend on a specific Cloud Haskell backend and would require (hopefully minor) code changes to switch backend. However libraries of reusable distributed algorithms could be written that depend only on the Cloud Haskell package. + +Both the Cloud Haskell interface and implementation make use of the transport interface. This also serves as an interface for the transport implementations, which may for example, be based on some external library written in Haskell or C. + +Typically a Cloud Haskell backend will depend on a single transport implementation. There may be several different Cloud Haskell backends that all make use of the same transport implementation but that differ in how they discover or create peers. For example one backend might be designed for discovering peers on a LAN by broadcasting, while another might create peers by firing up new VMs using some cloud system such as EC2. Both such backends could still use the same TCP transport implementation. + +This example also illustrates somewhat the distinction between a transport implementation and a Cloud Haskell backend. The transport implementation is that one deals with the low level details of the network transport while the other makes use of a transport implementation to initialise a Cloud Haskell node. Part of the reason for the separation is that the network layer is intended to be reusable on its own without having to use the Cloud Haskell layer. + + +### Model overview + +We will now focus on the transport layer. + +Before describing the interfaces in detail we will give an overview of the networking model that our interfaces use. In particular we will focus on the common part of the interface rather than the per-backend parts. + +Compared to traditional networking interfaces, like the venerable socket API, our model is a little different looking. The socket API has functionality for creating listening sockets and trying to establish connections to foreign addresses (which may or may not exist). + +By contrast, our model is much more like a set of Unix processes connected via anonymous unix domain sockets (which are much like ordinary unix pipes). In particular, unix domain sockets can be created anonymously with two ends (see socketpair(2)) and either end may be passed between processes over existing sockets. Note that this arrangement only allows communication via existing connections: new sockets can be made and can be passed around, but only to processes that are already part of the network graph. It does not provide any method for making connections to processes not already connected into the network graph. + +This anonymous unix domain socket model has the advantage of simplicity. There is no need for addresses: socket endpoints are simply created anonymously and passed between processes. Obviously this simplicity comes at the cost of not actually being able to establish new networks from scratch -- only to make new connections within existing networks. By putting the establishment of initial connections outside the common interface, we allow that aspect to be different for each network transport and network topology. We can write distributed algorithms that are reusable with multiple transports, on the assumption that the network peers are already known. + +We hope this is a reasonable compromise, otherwise it is hard to include connection creation in the common interface since each transport has its own address spaces and parameters for new connections. + +Our model is almost as simple as the anonymous unix domain socket model. We have to make it work with real networks, without the assistance of a shared OS. Unlike with unix domain sockets where both ends can be moved (and indeed shared) between processes, we differentiate the source and target endpoints and only allow the source endpoint to be moved around. Additionally, because we cannot actually move a stateful object like a source endpoint from one node to another, we re-introduce the notion of an address. However it is a severely limited notion of address: we cannot make new addresses within the common interface, only take the address of existing target endpoints. Those addresses can then be sent by value over existing links and used to create source endpoints. Thus every address created in this way uniquely identifies a target endpoint. + +This model gives us something like many-to-one connections. The target endpoint cannot be "moved". The source endpoint can be "copied" and all copies can be used to send messages to the target endpoint. + + +### Connection kinds and behaviours + +The above overview covers our primary kind of connection. Overall we provide four kinds of connection. + + 1. many-to-one, reliable ordered delivery of arbitrary sized messages + 2. many-to-one, reliable unordered delivery of arbitrary sized messages + 3. many-to-one, unreliable delivery of messages with bounded size + 4. multicast one-to-many, unreliable delivery of messages with bounded size + +The first one is the primary kind of connection, used in most circumstances while the other three are useful in more specialised cases. The first three are all ordinary point-to-point connections with varying degrees of reliability and ordering guarantee. We provide only datagram/message style connections, not stream style. + +For our primary kind of connection, we stipulate that it provides reliable ordered delivery of arbitrary sized messages. More specifically: + + * Message/datagram (rather than stream oriented) + * Arbitrary message size + * Non-corruption, that is messages are delivered without modification and are + delivered whole or not at all. + * Messages are delivered at most once + * Messages are delivered in-order. Subsequent messages are not delivered + until earlier ones are delivered. This only applies between messages sent + between the same pair of source and target endpoints -- there is no + ordering between messages sent from different source endpoints to the same + target endpoint. + * Somewhat-asynchronous send is permitted: + * send is not synchronous, send completing does not imply successful + delivery + * send side buffering is permitted but not required + * receive side buffering is permitted but not required + * send may block (e.g. if too much data is in flight or destination buffers + are full) + * Mismatched send/receive is permitted. + It is not an error to send without a thread at the other end already + waiting in receive (but it may block). + +These properties are based on what we can get with (or build on top of) tcp/ip, udp/ip, unix IPC, MPI and the CCI HPC transport. (In particular CCI emphasises the property that a node should be able to operate with receive buffer size that is independent of the number of connections/nodes it communicates with unlike tcp/ip which has a buffer per connection. Also, CCI allows unexpected receipt of small messages but requires pre-arrangement for large transfers so the receive side can prepare buffers). + +For the reliable unordered connections the ordering requirement is dropped while all others are preserved. + +For the unreliable connections (both point to point and multicast) the ordering, at-most-once and arbitrary message size requirements are dropped. All others are preserved. For these unreliable connections there may be an upper limit on message length and there is a way to discover that limit on a per-connection basis for established connections. + +While transport implementations must guarantee the provision of the reliable ordered connection kind (and the unordered and unreliable variants can obviously be emulated at no extra cost in terms of the reliable ordered kind), transport implementations do not need to guarantee the provision of multicast connections. In many transports, including IP, the availability of multicast connections cannot be relied upon. Transport clients have to be prepared for the creation of multicast connections to fail. Since this is the case even for transports that can support multicast, we use the same mechanism for transports that have no multicast support at all. + + +### Blocking vs. non-blocking send and receive + +For sending or receiving messages, one important design decision is how it interacts with Haskell lightweight threads. Almost all implementations are going to consist of a Haskell-thread blocking layer built on top of a non-blocking layer. We could choose to put the transport interface at the blocking or non-blocking layer. We have decided to go for a design that is blocking at the Haskell thread level. This makes the backend responsible for mapping blocking calls into something non-blocking at the OS thread level. That is, the backend must ensure that a blocking send/receive only blocks the Haskell thread, not all threads on that core. In the general situation we anticipate having many Haskell threads blocked on network IO while other Haskell threads continue doing computation. (In an IP backend we get this property for free because the existing network library implements the blocking behaviour using the IO manager.) + + +### Transport common interface + +We start with a Transport. Creating a Transport is totally backend dependent. More on that later. + +A Transport lets us create new connections. Our current implementation provides ordinary reliable many-to-one connections, plus the multicast one-to-many connections. It does not yet provide unordered or unreliable many-to-one connections, but these will closely follow the interface for the ordinary reliable many-to-one connections. + +{% highlight haskell %} +data Transport = Transport + { newConnectionWith :: Hints -> IO TargetEnd + , newMulticastWith :: Hints -> IO MulticastSourceEnd + , deserialize :: ByteString -> Maybe Address + } +{% endhighlight %} + +We will start with ordinary connections and look at multicast later. + +We will return later to the meaning of the hints. We have a helper function for the common case of default hints. + +{% highlight haskell %} +newConnection :: Transport -> IO TargetEnd +newConnection transport = newConnectionWith transport defaultHints +{% endhighlight %} + +The `newConnection` action creates a new connection and gives us its `TargetEnd`. The `TargetEnd` is a stateful object representing one endpoint of the connection. For the corresponding source side, instead of creating a stateful `SourceEnd` endpoint, we can take the address of any `TargetEnd`: + +{% highlight haskell %} +address :: TargetEnd -> Address +{% endhighlight %} + +The reason for getting the address of the target rather than `newConnection` just giving us a `SourceEnd` is that usually we only want to create a `SourceEnd` on remote nodes not on the local node. + +An `Address` represents an address of an existing endpoint. It can be serialised and copied to other nodes. On the remote node the Transport's `deserialize` function is is used to reconstruct the `Address` value. Once on the remote node, a `SourceEnd` can created that points to the `TargetEnd` identified by the `Address`. + +{% highlight haskell %} +data Address = Address + { connectWith :: SourceHints -> IO SourceEnd + , serialize :: ByteString + } +{% endhighlight %} + +Again, ignore the hints for now. + +{% highlight haskell %} +connect :: Address -> IO SourceEnd +connect address = connectWith address defaultSourceHints +{% endhighlight %} + +The `connect` action makes a stateful endpoint from the address. It is what really establishes a connection. After that the `SourceEnd` can be used to send messages which will be received at the `TargetEnd`. + +The `SourceEnd` and `TargetEnd` are then relatively straightforward. They are both stateful endpoint objects representing corresponding ends of an established connection. + +{% highlight haskell %} +newtype SourceEnd = SourceEnd + { send :: [ByteString] -> IO () + } + +newtype TargetEnd = TargetEnd + { receive :: IO [ByteString] + , address :: Address + } +{% endhighlight %} + +The `SourceEnd` sports a vectored send. That is, it allows sending a message stored in a discontiguous buffer (represented as a list of ByteString chunks). The `TargetEnd` has a vectored receive, though it is not vectored in the traditional way because it is the transport not the caller that handles the buffers and decides if it will receive the incoming message into a single contiguous buffer or a discontiguous buffer. Callers must always be prepared to handle discontiguous incoming messages or pay the cost of copying into a contiguous buffer. + +The use of discontiguous buffers has performance advantages and with modern binary serialisation/deserialisation libraries their use is not problematic. + +TODO: we have not yet covered closing connections, shutting down the transport and failure modes / exceptions. + + +### Unordered and unreliable connections + +Though not currently implemented, these connection types follow the same pattern as the normal reliable ordered connection. The difference is just one of behaviour. The only difference is that the source end for unreliable connections lets one find out the maximum message length (i.e. the MTU). + +The reason each of these are separate types is because they have different semantics and must not be accidentally confused. The reason we don't use a parameterised type is because typically the implementations will be different so this simplifies the implementation. + +### Multicast connections + +For the multicast connections, the address, source and target ends are analogous. The difference is that the address that is passed around is used to create a target endpoint so that multiple nodes can receive the messages sent from the source endpoint. It's sort of the dual of the ordinary many-to-one connections. + +The `newMulticast` is the other way round compared to `newConnection`: it gives us a stateful `MulticastSourceEnd` from which we can obtain the address `MulticastAddress`. + +{% highlight haskell %} +newMulticast :: Transport -> IO MulticastSourceEnd +newMulticast transport = newMulticastWith transport defaultHints + +newtype MulticastSourceEnd = MulticastSourceEnd + { multicastSend :: [ByteString] -> IO () + , multicastAddress :: MulticastAddress + , multicastMaxMsgSize :: Int + } + +newtype MulticastAddress = MulticastAddress + { multicastConnect :: IO MulticastTargetEnd + } + +newtype MulticastTargetEnd = MulticastTargetEnd + { multicastReceive :: IO [ByteString] + } +{% endhighlight %} + +The multicast send has an implementation-defined upper bound on the message size which can be discovered on a per-connection basis. + + +### Creating a Transport via a transport backend + +Creating a `Transport` object is completely backend-dependent. There is the opportunity to pass in a great deal of configuration data at this stage and to use types specific to the backend to do so (e.g. TCP parameters). + +In the simplest case (e.g. a dummy in-memory transport) there might be nothing to configure: + +{% highlight haskell %} +mkTransport :: IO Transport +{% endhighlight %} + +For a TCP backend we might have: + +{% highlight haskell %} +mkTransport :: TCPConfig -> IO Transport + +data TCPConfig = ... +{% endhighlight %} + +This `TCPConfig` can contain arbitrary amounts of configuration data. Exactly what it contains is closely connected with how we should set per-connection parameters. + + +### Connection parameters and hints + +As mentioned previously, a major design challenge with creating a common transport interface is how to set various parameters when creating new connections. Different transports have widely different parameters, for example the parameters for a TCP/IP socket have very little in common with a connection using shared memory or infiniband. Yet being able to set these parameters is vital for performance in some circumstances. + +The traditional sockets API handles this issue by allowing each different kind of socket to have its own set of configuration parameters. This is fine but it does not really enable reusable code. Generic, transport-independent code would need to get such parameters from somewhere, and if connections used for different purposes needed different parameters then the problem would be worse. + +With our design approach it is easy to pass backend-specific types and configuration parameters when a transport is initialised, but impossible to pass backend-specific types when creating connections later on. + +This makes it easy to use a constant set of configuration parameters for every connection. For example for our example TCP backend above we could have: + +{% highlight haskell %} +data TCPConfig = TCPConfig { + socketConfiguration :: SocketOptions + } +{% endhighlight %} + +This has the advantage that it gives us full access to all the options using the native types of the underlying network library (`SocketOptions` type comes from the `network` library). + +The drawback of this simple approach is that we cannot set different options for different connections. To optimise performance in some applications or networks we might want to use different options for different network addresses (e.g. local links vs longer distance links). Similarly we might want to use different options for connections that are used differently, e.g. using more memory or network resources for certain high bandwidth connections, or taking different tradeoffs on latency vs bandwidth due to different use characteristics. + +Allowing different connection options depending on the source and destination addresses is reasonably straightforward: + +{% highlight haskell %} +data TCPConfig = TCPConfig { + socketConfiguration :: Ip.Address -> Ip.Address + -> SocketOptions + } +{% endhighlight %} + +We simply make the configuration be a function that returns the connection options but is allowed to vary depending on the IP addresses involved. Separately this could make use of configuration data such as a table of known nodes, perhaps passed in by a cluster job scheduler. + +Having options vary depending on how the connection is to be used is more tricky. If we are to continue with this approach then it relies on the transport being able to identify how a client is using (or intends to use) each connection. Our proposed solution is that when each new connection is made, the client supplies a set of "hints". These are not backend specific, they are general indications of what the client wants, or how the client intends to use the connection. The backend can then interpret these hints and transform them into the real network-specific connection options: + +{% highlight haskell %} +data TCPConfig = TCPConfig { + socketConfiguration :: Hints -> Ip.Address -> Ip.Address + -> SocketOptions + } +{% endhighlight %} + +What exactly goes into the hints will have to be considered in consultation with networking experts and people implementing backends. In particular it might indicate if bandwidth or latency is more important (e.g. to help decide if NO_DELAY should be used), if the connection is to be heavily or lightly used (to help decide buffer size) etc. + +Using this approach may require a slightly different way of thinking about network programming and tuning than normal. Instead of specifying exactly what you want when creating each connection, you instead have to say how you intend to use the connection and then trust the backend to do the right thing. As noted however, the backend can take a custom configuration function that can pick very specific connection options, so long as it has enough information to distinguish the different classes of connections it is interested in. + +Middleware like Cloud Haskell faces this problem anyway. Because it is transport independent it cannot use network-specific connection options anyway, they would have to be passed in as configuration. The best that middleware can do is to provide what detail it can on how each connection is likely to be used (the information available to it is fairly limited). + +The only place where we are in a position to fully exploit all the network-specific connection options is when we have a complete (or near complete) application, using a specific transport, and we know what kind of network environment/hardware it will be used on. At this point, the ideal thing would indeed be to pass in a (perhaps complex) configuration when the application is launched, rather than modify the app and the middleware to pass down network-specific connection options to each place where connections are established. + +So hopefully if this model works, it might be quite nice in allowing the configuration to be more-or-less separated from the main program, allowing reusable code and still allowing turning to the network environment. + +A useful analogy perhaps is CSS. It allows medium-specific (screen, print, voice etc) configuration to be specified in detail but separately from the content/structure. It allows general/constant options to be specified easily but allows a fair degree of discrimination between different HTML elements and allows distinction based on class or id names. + +Following this analogy, it might prove useful in the most extreme cases to be able to give names/class/ids to connections in the hints, so that very detailed configurations could pick specific extra tweaks for specific connections or classes of connections. + + +### Cloud Haskell Usage + +Given the Transport interface described above, the main Cloud Haskell implementation then needs to be rebuilt on top, providing the same API as now, except for the initialisation and peer discovery. + +We were initially inclined to stipulate that the communication endpoints provided by our network transport be sufficiently lightweight that we could use one per cloud Haskell process (for each processes' input message queue) and one per cloud Haskell typed channel. The transport backend would then be responsible for doing any multiplexing necessary to give us lightweight endpoint resources. Unfortunately, when considering the design of the TCP/IP transport we could not find a sensible way to provide lightweight connection endpoints while at the same time avoiding "head of line" blocking and sensible buffering. + +We therefore assume for the moment that transport connections are heavyweight resources and the Cloud Haskell implementation will have to take care of multiplexing messages between lightweight processes over these connections. + +### Requirements for NodeID and ProcessId + +A `ProcessId` serves two purposes, one is to communicate with a process directly and the other is to talk about that process to various service processes. + +The main APIs involving a ProcessId are: + +{% highlight haskell %} +getSelfPid :: ProcessM ProcessId +send :: Serializable a => ProcessId -> a -> ProcessM () +spawn :: NodeId -> Closure (ProcessM ()) -> ProcessM ProcessId +{% endhighlight %} + +and linking and service requests: + +{% highlight haskell %} +linkProcess :: ProcessId -> ProcessM () +monitorProcess :: ProcessId -> ProcessId -> MonitorAction -> ProcessM () +nameQuery :: NodeId -> String -> ProcessM (Maybe ProcessId) +{% endhighlight %} + +A NodeId is used to enable us to talk to the service processes on a node. + +The main APIs involving a `NodeId` are: + +{% highlight haskell %} +getSelfNode :: ProcessM NodeId +spawn :: NodeId -> Closure (ProcessM ()) -> ProcessM ProcessId +nameQuery :: NodeId -> String -> ProcessM (Maybe ProcessId) +{% endhighlight %} + + +### NodeID and ProcessId representation + +So for a ProcessId we need: + + * a communication endpoint to the process message queue so we can talk to the + process itself + * the node id and an identifier for the process on that node so that we can + talk to node services about that process + +We define it as + +{% highlight haskell %} +data ProcessId = ProcessId SourceEnd NodeId LocalProcessId +{% endhighlight %} + +For a `NodeId` we need to be able to talk to the service processes on that node. + +{% highlight haskell %} +data NodeId = NodeId SourceEnd +{% endhighlight %} + +The single 'SourceEnd's is for talking to the basic service processes (ie the processes involved in implementing spawn and link/monitor). The service process Ids on each node are well known and need not be stored. + +### Cloud Haskell channel representation + +A cloud Haskell channel `SendPort` is similar to a `ProcessId` except that we do not need the `NodeId` because we do not need to talk about the process on the other end of the port. + +{% highlight haskell %} +data SendPort a = SendPort SourceEnd +{% endhighlight %} + +### Cloud Haskell backend initialisation and neighbour setup + +In the first implementation, the initialisation was done using: + +{% highlight haskell %} +remoteInit :: Maybe FilePath -> (String -> ProcessM ()) -> IO () +{% endhighlight %} + +This takes a configuration file (or uses an environment variable to find the same), an initial process, and it launches everything by reading the config, creating the local node and running the initial process. The initial process gets passed some role string obtained from the configuration file. + +One of the slightly tricky issues with writing a program for a cluster is how to initialise everything in the first place: how to get each node talking to the appropriate neighbours. + +The first cloud Haskell implementation provides: + +{% highlight haskell %} +type PeerInfo = Map String [NodeId] +getPeers :: ProcessM PeerInfo +findPeerByRole :: PeerInfo -> String -> [NodeId] +{% endhighlight %} + +The implementation obtains this information using magic and configuration files. + +Our new design moves this functionality out of the common interface entirely and into each Cloud Haskell backend. Consider for example the difference between neighbour setup in the following hypothetical Cloud Haskell backends: + + * A single-host multi-process model where we fork a number of OS processes (typically the number of CPU cores) and connect them by pipes. Here the neighbours are created at initialisation time and remain fixed thereafter. + * A cluster backend that uses a cluster job scheduler to start a binary on each node and inform each one of a certain number of their peers (e.g. forming some hypercube topology). + * A cloud backend that allows firing up VMs and starting new instances of the program on the new VMs. Here peers are not discovered but created, either at the start of the job or later to adjust for demand. + * A backend providing something like the current approach where nodes are discovered by broadcasting on the LAN. + +(As a side note: the latter three backends probably all use the same IP transport implementation, but they handle configuration and peer setup quite differently. The point being, there's more to a Cloud Haskell backend than a choice of transport implementation.) + +We think that making an interface that covers all these cases would be end up rather clumsy. We believe it is simpler to have each of these backends provide their own way to initialise and discover/create peer nodes. + +So in the new design, each application selects a Cloud Haskell backend by importing the backend and using an initialisation function from it. + +Exactly how this is exposed has not been finalised. Internally we have an abstraction `LocalNode` which is a context object that knows about all the locally running processes on the node. We have: + +{% highlight haskell %} +newLocalNode :: Transport -> IO LocalNode +runProcess :: LocalNode -> Process () -> IO () +{% endhighlight %} + +and each backend will (at least internally) have a function something like: + +{% highlight haskell %} +mkTransport :: {...config...} -> IO Transport +{% endhighlight %} + +So the initialisation process is more or less + +{% highlight haskell %} +init :: {...} -> Process () -> IO () +init config initialProcess = do + transport <- mkTransport config + localnode <- newLocalNode transport + runProcess localnode initialProcess +{% endhighlight %} + +We could export all these things and have applications plug them together. + +Alternatively we might have each backend provide an initialisation that does it all in one go. For example the backend that forks multiple OS process might have an init like this: + +{% highlight haskell %} +init :: Int -> ([NodeId] -> Process ()) -> IO () +{% endhighlight %} + +It takes a number of (OS) processes to fork and the initial (CH) process gets passes a corresponding number of remote `NodeId`s. + +For the backend that deals with VMs in the cloud, it might have two initialisation functions, one for the master controller node and one for slave nodes. + +{% highlight haskell %} +initMaster :: MasterConfig -> Process () -> IO () +initSlave :: SlaveConfig -> IO () +{% endhighlight %} + +Additionally it might have actions for firing up new VMs and running the program binary in slave mode on that VM: + +{% highlight haskell %} +spawnVM :: VmAccount -> IO VM +initOnVM :: VM -> IO NodeId +shutdownVM :: VM -> IO () +{% endhighlight %} + +For example, supposing in our application's 'main' we call the IP backend and initialise a Transport object, representing the transport backend for cloud Haskell: + +### Using multiple transports in the Cloud Haskell layer + +There are contexts where it makes sense to use more than one `Transport` in a single distributed application. The most common example is likely to be a local transport, e.g. shared memory or pipes, for communication between nodes that share the same host, and a second transport using a network (IP or some HPC network). This is a very common setup with OpenMPI for example which allows the use of multiple "byte transfer layers" (BTLs) which correspond roughly to our notion of transport. + +There are various challenges related to addressing. Assuming these can be solved, it should be considered how initialisation might be done when there are multiple transports / backends in use. We might want to have: + +{% highlight haskell %} +newLocalNode :: [Transport] -> IO LocalNode +{% endhighlight %} + +and expose it to the clients. + +Alternatively, instead of allowing arbitrary stacks of transports, we might make each backend pick a fixed combination of transports. This is likely to work OK because the range of sensible combinations is not very large. + + +### Open issues + + * Is the configuration model sufficient and if so, the exact details of what goes into the connection hints. See the section above about connection parameters and hints. + + * If connection endpoints should be lightweight or heavyweight. We think leaving it as heavyweight is the way to go, but perhaps others can see a plausible design. See the section on the Cloud Haskell implementation built on Transport interface. + + * What style of initialisation API should be exposed to Cloud Haskell clients, and how this should be divided between the common interface and the backend interfaces. See the section on Cloud Haskell backend initialisation and neighbour setup. diff --git a/wiki/newtransports.md b/wiki/newtransports.md new file mode 100644 index 00000000..69d614f5 --- /dev/null +++ b/wiki/newtransports.md @@ -0,0 +1,140 @@ +--- +layout: wiki +title: Building new Transports +wiki: Guide +--- + +## Guide + +On this page we describe the TCP Transport as an example for developers who wish to write their own instantiations of the Transport layer. The purpose of any such instantiation is to provide a function + +{% highlight haskell %} +createTransport :: -> IO (Either Transport) +{% endhighlight %} + +For instance, the TCP transport offers + +{% highlight haskell %} +createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport) +{% endhighlight %} + +This could be the only function that `Network.Transport.TCP` exports (the only reason it exports more is to provide an API for unit tests for the TCP transport, some of which work at a lower level). Your implementation will now be guided by the `Network.Transport` API. In particular, you will need to implement `newEndPoint`, which in turn will require you to implement `receive`, `connect`, etc. + +#### Mapping Network.Transport concepts to TCP + +The following picture is a schematic overview of how the Network.Transport concepts map down to their TCP counterparts. + +---- +![The TCP transport](/img/NetworkTCP.png) + +---- + +* The blue boxes represent `Transport` instances. At the top we have two Transport instances on the same TCP node 198.51.100.1. These could be as part of the same Haskell process or, perhaps more typically, in separate processes. Different Transport instances on the same host must get a different port number. +* The orange boxes represent `EndPoint`s. A Transport can have multiple EndPoints (or none). EndPointAddresses in the TCP transport are triplets `TCP host:TCP port:endpoint ID`. +* The heavyset lines represent (heavyweight, bidirectional) TCP connections; the squiggly lines represent Transport level (lightweight, unidirectional) connections. The TCP transport guarantees that at most a single TCP connection will be set up between any pair of endpoints; all lightweight connections get multiplexed over this single connection. +* All squiggly lines end up in a single fat dot in the middle of the endpoint. This represents a `Chan` on which all endpoint `Event`s are posted. + +## Implementation + +We briefly discuss the implementation of the most important functions in the Transport API. + +#### Setup (`createTransport`) + +When a TCP transport is created at host 192.51.100.1, port 1080, `createTransport` sets up a socket, binds it to 192.51.100.1:1080, and then spawns a thread to listen for incoming requests. This thread will handle the connection requests for all endpoints on this TCP node: individual endpoints do _not_ set up their own listening sockets. + +This is the only set up that `Network.Transport.TCP.createTransport` needs to do. + +#### Creating new endpoints (`newEndPoint`) + +In the TCP transport the set up of new endpoints is straightforward. We only need to create a new `Chan` on which we will output all the events and allocate a new ID for the endpoint. Now `receive` is just `readChan` and `address` is the triplet `host:port:ID` + +#### New connections (`connect`) + +Consider the situation shown in the diagram above, and suppose that endpoint 198.51.100.1:1081:0 (lets call it _A_) wants to connect to 198.51.100.2:1080:1 (_B_). Since there is no TCP connection between these two endpoints yet we must set one up. + +* _A_ creates a socket and connects to 198.51.100.2:1080. The transport thread at 198.51.100.2 accepts the connection. +* _A_ now sends two messages across the socket: first, the ID of the remote endpoint it wants to connect to (1) and then it own full address (192.51.100.1:1081:0). The first message is necessary because the remote transport is responsible for handling the connection requests for all its endpoints. The second message is necessary to ensure that when _B_ subsequently attempts to `connect` to _A_ we know that a TCP connection to _A_ is already available. +* _B_ will respond with `ConnectionRequestAccepted` and spawn a thread to listen for incoming messages on the newly created TCP connection. + +At this point there is a TCP connection between _A_ and _B_ but not yet a Network.Transport connection; at this point, however, the procedure is the same for all connection requests from _A_ to _B_ (as well as as from _B_ to _A_): + +* _A_ sends a `RequestConnectionId` message to _B_ across the existing TCP connection. +* _B_ creates a new connection ID and sends it back to _A_. At this point _B_ will output a `ConnectionOpened` on _B_'s endpoint. + +> A complication arises when _A_ and _B_ simultaneously attempt to connect each other and no TCP connection has yet been set up. In this case _two_ TCP connections will temporarily be created; _B_ will accept the connection request from _A_, keeping the first TCP connection, but _A_ will reply with `ConnectionRequestCrossed`, denying the connection request from _B_, and then close the socket. + +Note that connection IDs are _locally_ unique. When _A_ and _B_ both connect to _C_, then _C_ will receive two `ConnectionOpened` events with IDs (say) 1024 and 1025. However, when _A_ connects to _B_ and _C_, then it is entirely possible that the connection ID that _A_ receives from both _B_ and _C_ is identical. Connection IDs for outgoing connections are however not visible to application code. + +#### Sending messages (`send`) + +To send a message from _A_ to _B_ the payload is given a Transport header consisting of the message length and the connection ID. When _B_ receives the message it posts a `Received` event. + +#### Closing a connection + +To close a connection, _A_ just sends a `CloseConnection` request to _B_, and _B_ will post a `ConnectionClosed` event. + +When there are no more Transport connections between two endpoints the TCP connection between them is torn down. + +> Actually, it's not quite that simple, because _A_ and _B_ need to agree that the TCP connection is no longer required. _A_ might think that the connection can be torn down, but there might be a `RequestConnectionId` message from _B_ to _A_ in transit in the network. _A_ and _B_ both do reference counting on the TCP connection. When _A_'s reference count for its connection to _B_ reaches zero it will send a `CloseSocket` request to _B_. When _B_ receives it, and its refcount for its connection to _A_ is also zero, it will close its socket and reply with a reciprocal `CloseSocket` to _A_. If however _B_ had already sent a `RequestConnectionId` to _A_ it will simply ignore the `CloseSocket` request, and when _A_ receives the `RequestConnectionId` it simply forgets it ever sent the `CloseSocket` request. + +#### Transport instances for connectionless protocols + +In the TCP transport `createTransport` needs to do some setup, `newEndPoint` barely needs to do any at all, and `connect` needs to set up a TCP connection when none yet exists to the destination endpoint. In Transport instances for connectionless protocols this balance of work will be different. For instance, for a UDP transport `createTransport` may barely need to do any setup, `newEndPoint` may set up a UDP socket for the endpoint, and `connect` may only have to setup some internal datastructures and send a UDP message. + +#### Error handling + +Network.Transport API functions should not throw any exceptions, but declare explicitly in their types what errors can be returned. This means that we are very explicit about which errors can occur, and moreover map Transport-specific errors ("socket unavailable") to generic Transport errors ("insufficient resources"). A typical example is `connect` with type: + +{% highlight haskell %} +connect :: EndPoint -- ^ Local endpoint + -> EndPointAddress -- ^ Remote endpoint + -> Reliability -- ^ Desired reliability of the connection + -> IO (Either (TransportError ConnectErrorCode) Connection) +{% endhighlight %} + +`TransportError` is defined as + +{% highlight haskell %} +data TransportError error = TransportError error String + deriving Typeable +{% endhighlight %} + +and has `Show` and `Exception` instances so that application code has the option of `throw`ing returned errors. Here is a typical example of error handling in the TCP transport; it is an internal function that does the initial part of the TCP connection setup: create a new socket, and the remote endpoint ID we're interested in and our own address, and then wait for and return the response: + +{% highlight haskell %} +socketToEndPoint :: EndPointAddress -- ^ Our address + -> EndPointAddress -- ^ Their address + -> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse)) +socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do + (host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of + Nothing -> throw (failed . userError $ "Could not parse") + Just dec -> return dec + addr:_ <- mapExceptionIO invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port) + bracketOnError (createSocket addr) N.sClose $ \sock -> do + mapExceptionIO failed $ N.setSocketOption sock N.ReuseAddr 1 + mapExceptionIO invalidAddress $ N.connect sock (N.addrAddress addr) + response <- mapExceptionIO failed $ do + sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress]) + recvInt32 sock + case tryToEnum response of + Nothing -> throw (failed . userError $ "Unexpected response") + Just r -> return (sock, r) + where + createSocket :: N.AddrInfo -> IO N.Socket + createSocket addr = mapExceptionIO insufficientResources $ + N.socket (N.addrFamily addr) N.Stream N.defaultProtocol + + invalidAddress, insufficientResources, failed :: IOException -> TransportError ConnectErrorCode + invalidAddress = TransportError ConnectNotFound . show + insufficientResources = TransportError ConnectInsufficientResources . show + failed = TransportError ConnectFailed . show +{% endhighlight %} + +Note how exceptions get mapped to `TransportErrors` using `mapExceptionID`, which is defined in `Network.Transport.Internal` as + +{% highlight haskell %} + mapExceptionIO :: (Exception e1, Exception e2) => (e1 -> e2) -> IO a -> IO a + mapExceptionIO f p = catch p (throw . f) +{% endhighlight %} + +Moreover, the original exception is used as the `String` part of the `TransportError`. This means that application developers get transport-specific feedback, which is useful for debugging, not cannot take use this transport-specific information in their _code_, which would couple applications to tightly with one specific transport implementation. diff --git a/wiki/style.md b/wiki/style.md new file mode 100644 index 00000000..70098a14 --- /dev/null +++ b/wiki/style.md @@ -0,0 +1,120 @@ +--- +layout: wiki +title: Style Guide +wiki: Style +--- + +### Style + +A lot of this **is** taken from the GHC Coding Style entry [here](http://hackage.haskell.org/trac/ghc/wiki/Commentary/CodingStyle). +In particular, please follow **all** the advice on that wiki page when it comes +to including comments in your code. + +I am also grateful to @tibbe for his +[haskell-style-guide](https://github.com/tibbe/haskell-style-guide), from +which some of these rules have been taken. + +As a general rule, stick to the same coding style as is already used in the file +you're editing. It **is** much better to write code that is transparent than to +write code that is short. Please don't assume everyone is a minimalist - self +explanatory code is **much** better in the long term than pithy one-liners. +Having said that, we *do* like reusing abstractions where doing so adds to the +clarity of the code as well as minimising repetitious boilerplate. + +### Formatting + +#### Line Length + +Maximum line length is *80 characters*. This might seem antiquated +to you, but some of us do things like github pull-request code +reviews on our mobile devices on the way to work, and long lines +make this horrendously difficult. Besides which, some of us are +also emacs users and have this rule set up for all of our source +code editing modes. + +#### Indentation + +Tabs are illegal. Use **only** spaces for indenting. +Indentation is usually 2 spaces, with 4 spaces used in some places. +We're pretty chilled about this, but try to remain consistent. + +#### Blank Lines + +One blank line between top-level definitions. No blank lines between +type signatures and function definitions. Add one blank line between +functions in a type class instance declaration if the functions bodies +are large. As always, use your judgement. + +#### Whitespace + +Do not introduce trailing whitespace. If you find trailing whitespace, +feel free to strip it out - in a separate commit of course! + +Surround binary operators with a single space on either side. Use +your better judgement for the insertion of spaces around arithmetic +operators but always be consistent about whitespace on either side of +a binary operator. + +#### Alignment + +When it comes to alignment, there's probably a mix of things in the codebase +right now. Personally, I tend not to align import statements as these change +quite frequently and it is pain keeping the indentation consistent. + +The one exception to this is probably imports/exports, which we *are* a +bit finicky about: + +{% highlight haskell %} +import qualified Foo.Bar.Baz as Bz +import Data.Binary + ( Binary (..), + , getWord8 + , putWord8 + ) +import Data.Blah +import Data.Boom (Typeable) +{% endhighlight %} + +Personally I don't care *that much* about alignment for other things, +but as always, try to follow the convention in the file you're editing +and don't change things just for the sake of it. + +### Comments + +#### Punctuation + +Write proper sentences; start with a capital letter and use proper +punctuation. + +#### Top-Level Definitions + +Comment every top level function (particularly exported functions), +and provide a type signature; use Haddock syntax in the comments. +Comment every exported data type. Function example: + +{% highlight haskell %} +-- | Send a message on a socket. The socket must be in a connected +-- state. Returns the number of bytes sent. Applications are +-- responsible for ensuring that all data has been sent. +send :: Socket -- ^ Connected socket + -> ByteString -- ^ Data to send + -> IO Int -- ^ Bytes sent +{% endhighlight %} + +For functions the documentation should give enough information to +apply the function without looking at the function's definition. + +### Naming + +Use `mixedCase` when naming functions and `CamelCase` when naming data +types. + +For readability reasons, don't capitalize all letters when using an +abbreviation. For example, write `HttpServer` instead of +`HTTPServer`. Exception: Two letter abbreviations, e.g. `IO`. + +#### Modules + +Use singular when naming modules e.g. use `Data.Map` and +`Data.ByteString.Internal` instead of `Data.Maps` and +`Data.ByteString.Internals`. From 2d5c4ee4c2b55105ceafdfd67d60193702908a13 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 30 Jan 2013 22:07:31 +0000 Subject: [PATCH 0899/2357] backport cabal fixes to master --- distributed-process-platform.cabal | 43 ------------------------------ 1 file changed, 43 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 38e7dfb9..438678d1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -42,7 +42,6 @@ library Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, Control.Distributed.Process.Platform.Timer, @@ -82,11 +81,6 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs - other-modules: - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -111,13 +105,6 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -143,13 +130,6 @@ test-suite AsyncTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -175,29 +155,6 @@ test-suite GenServerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async, - Control.Distributed.Process.Platform.Async.AsyncChan, - Control.Distributed.Process.Platform.Async.AsyncSTM, - Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, - Control.Distributed.Process.Platform.Test, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types, - Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types, - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.ManagedProcess, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs From fd05a4d8637055e3fd9d84481bb781bc06ecb017 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 30 Jan 2013 22:07:31 +0000 Subject: [PATCH 0900/2357] backport cabal fixes to master --- distributed-process-platform.cabal | 43 ------------------------------ 1 file changed, 43 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 38e7dfb9..438678d1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -42,7 +42,6 @@ library Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, Control.Distributed.Process.Platform.Timer, @@ -82,11 +81,6 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs - other-modules: - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -111,13 +105,6 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -143,13 +130,6 @@ test-suite AsyncTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -175,29 +155,6 @@ test-suite GenServerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async, - Control.Distributed.Process.Platform.Async.AsyncChan, - Control.Distributed.Process.Platform.Async.AsyncSTM, - Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, - Control.Distributed.Process.Platform.Test, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types, - Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types, - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.ManagedProcess, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs From 3614d9f93ba85a4eba8136bd6be38b620183cf46 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 30 Jan 2013 22:07:31 +0000 Subject: [PATCH 0901/2357] backport cabal fixes to master --- distributed-process-platform.cabal | 43 ------------------------------ 1 file changed, 43 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 38e7dfb9..438678d1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -42,7 +42,6 @@ library Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, Control.Distributed.Process.Platform.Timer, @@ -82,11 +81,6 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs - other-modules: - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -111,13 +105,6 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -143,13 +130,6 @@ test-suite AsyncTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -175,29 +155,6 @@ test-suite GenServerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async, - Control.Distributed.Process.Platform.Async.AsyncChan, - Control.Distributed.Process.Platform.Async.AsyncSTM, - Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, - Control.Distributed.Process.Platform.Test, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types, - Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types, - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.ManagedProcess, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs From ea270cd0e2a32ed5dd55b2eb802777c22fa30887 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 30 Jan 2013 22:07:31 +0000 Subject: [PATCH 0902/2357] backport cabal fixes to master --- distributed-process-platform.cabal | 43 ------------------------------ 1 file changed, 43 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 38e7dfb9..438678d1 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -42,7 +42,6 @@ library Control.Distributed.Process.Platform.Async.AsyncChan, Control.Distributed.Process.Platform.Async.AsyncSTM, Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, Control.Distributed.Process.Platform.Test, Control.Distributed.Process.Platform.Time, Control.Distributed.Process.Platform.Timer, @@ -82,11 +81,6 @@ test-suite TimerTests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind extensions: CPP main-is: TestTimer.hs - other-modules: - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess test-suite PrimitivesTests type: exitcode-stdio-1.0 @@ -111,13 +105,6 @@ test-suite PrimitivesTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestPrimitives.hs @@ -143,13 +130,6 @@ test-suite AsyncTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - TestUtils, - TestGenServer, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestAsync.hs @@ -175,29 +155,6 @@ test-suite GenServerTests src, tests ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind - other-modules: - Control.Distributed.Process.Platform, - Control.Distributed.Process.Platform.Async, - Control.Distributed.Process.Platform.Async.AsyncChan, - Control.Distributed.Process.Platform.Async.AsyncSTM, - Control.Distributed.Process.Platform.Call, - Control.Distributed.Process.Platform.GenServer, - Control.Distributed.Process.Platform.Test, - Control.Distributed.Process.Platform.Time, - Control.Distributed.Process.Platform.Timer, - Control.Distributed.Process.Platform.Internal.Primitives, - Control.Distributed.Process.Platform.Internal.Types, - Control.Distributed.Process.Platform.Internal.Common, - Control.Distributed.Process.Platform.Async.Types, - TestUtils, - MathsDemo, - Counter, - SimplePool, - Control.Distributed.Process.Platform.ManagedProcess, - Control.Distributed.Process.Platform.ManagedProcess.Client, - Control.Distributed.Process.Platform.ManagedProcess.Server, - Control.Distributed.Process.Platform.ManagedProcess.Internal.Types, - Control.Distributed.Process.Platform.ManagedProcess.Internal.GenProcess extensions: CPP main-is: TestGenServer.hs From 09d37f8552a04fb9be95ece5b0615f6aa104e18e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 30 Jan 2013 22:46:08 +0000 Subject: [PATCH 0903/2357] tidy up --- _layouts/documentation.html | 3 +- documentation.md | 80 ++++++++++++++++++++++++++----------- wiki/maintainers.md | 78 +++++++++++++++++++++++++++++++----- wiki/networktransport.md | 3 +- wiki/newdesign.md | 10 ++++- 5 files changed, 134 insertions(+), 40 deletions(-) diff --git a/_layouts/documentation.html b/_layouts/documentation.html index 936ad94b..de7ecbfb 100644 --- a/_layouts/documentation.html +++ b/_layouts/documentation.html @@ -22,8 +22,7 @@
    diff --git a/documentation.md b/documentation.md index 57cb48e1..216dc5dc 100644 --- a/documentation.md +++ b/documentation.md @@ -22,12 +22,6 @@ growing number of features for * working with several network transport implementations (and more in the pipeline) * supporting *static* values (required for remote communication) -API documentation for the latest releases is available on hackage. The latest -(HEAD) API documentation for the platform can be viewed -[here](/static/doc/distributed-process-platform/index.html). - -### Architecture - Cloud Haskell comprises the following components, some of which are complete, others experimental. @@ -42,9 +36,8 @@ others experimental. * [distributed-process-simplelocalnet][10]: Simple backend for local networks * [distributed-process-azure][11]: Azure backend for Cloud Haskell (proof of concept) - -One goal of Cloud Haskell is to separate the transport layer from the -process layer, so that the transport backend is entirely independent: +One of Cloud Haskell's goals is to separate the transport layer from the +*process layer*, so that the transport backend is entirely independent: it is envisaged that this interface might later be used by models other than the Cloud Haskell paradigm, and that applications built using Cloud Haskell might be easily configured to work with different @@ -63,6 +56,30 @@ The following diagram shows dependencies between the various subsystems, in an application using Cloud Haskell, where arrows represent explicit directional dependencies. +----- + + +------------------------------------------------------------+ + | Application | + +------------------------------------------------------------+ + | | + V V + +-------------------------+ +------------------------------+ + | Cloud Haskell |<--| Cloud Haskell Backend | + | (distributed-process) | | (distributed-process-...) | + +-------------------------+ +------------------------------+ + | ______/ | + V V V + +-------------------------+ +------------------------------+ + | Transport Interface |<--| Transport Implementation | + | (network-transport) | | (network-transport-...) | + +-------------------------+ +------------------------------+ + | + V + +------------------------------+ + | Haskell/C Transport Library | + +------------------------------+ + +----- In this diagram, the various nodes roughly correspond to specific modules: @@ -184,25 +201,40 @@ pass data between processes using *ordinary* concurrency primitives such as types like `TMVar a` just as normal Haskell threads are. Numerous features in [distributed-process-platform][3] use this facility, for example the way that `Control.Distributed.Processes.Platform.Async.AsyncSTM` handles passing -the result of its computation back to the caller: +the result of its computation back to the caller, as the following snippet +demonstrates: + +---- {% highlight haskell %} - workerPid <- spawnLocal $ do - -- ... some setup - r <- proc - void $ liftIO $ atomically $ putTMVar result (AsyncDone r) + root <- getSelfPid + result <- liftIO $ newEmptyTMVarIO + sigStart <- liftIO $ newEmptyTMVarIO + (sp, rp) <- newChan + + -- listener/response proxy + insulator <- spawnLocal $ do + worker <- spawnLocal $ do + liftIO $ atomically $ takeTMVar sigStart + r <- proc + void $ liftIO $ atomically $ putTMVar result (AsyncDone r) + + sendChan sp worker -- let the parent process know the worker pid + + wref <- monitor worker + rref <- case shouldLink of + True -> monitor root >>= return . Just + False -> return Nothing + finally (pollUntilExit worker result) + (unmonitor wref >> + return (maybe (return ()) unmonitor rref)) + + workerPid <- receiveChan rp + liftIO $ atomically $ putTMVar sigStart () + -- etc .... {% endhighlight %} -For example, we might implement a local process group using *only* message -passing, and when members enter or leave the group, a *master* process does -the book keeping to ensure the other members of the group can retain a -consist view. If we want to introduce a *group level barrier* to facilitate -mutual exclusion, we have two choices for handling this. If the process -group allows members to enter and leave on an ad-hoc basis, then a shared -memory based solution is a poor choice, because there is no *sane* way to -pass the `MVar` (or whatever) to new joiners. Locking is best achieved using -a messaging based protocol in this instance, which complicates the implementation - +---- Processes reside on nodes, which in our implementation map directly to the `Control.Distributed.Processes.Node` module. Given a configured diff --git a/wiki/maintainers.md b/wiki/maintainers.md index 0fb4fe52..41152e33 100644 --- a/wiki/maintainers.md +++ b/wiki/maintainers.md @@ -9,32 +9,88 @@ wiki: Maintainers This guide is specifically for maintainers, and outlines the development process and in particular, the branching strategy. -#### Master == Stable +#### Branching/Merging Policy The master branch is the **stable** branch, and should always be in a *releasable* state. This means that on the whole, only small self contained commits or topic branch merges should be applied to master, and tagged releases should always be made against it. -#### Development +#### Development Branches Ongoing work can either be merged into master when complete or merged into development. Development is effectively an integration branch, to make sure ongoing changes and new features play nicely -with one another. +with one another. On the other hand, master is a 'stable' branch +and therefore you should only merge into it if the result will be +releasable. -#### Releases +In general, we try to merge changes that belong to a major version +upgrade into development, whereas changes that will go into the +next minor version upgrade can be merged into master. -Remember to update the change log for each project when releasing it. -I forgot to add the changes to the changelog when tagging the recent -distributed-process-0.4.2 release, but in general they should be added -*before* tagging the release. +#### Keeping History + +Try to make only clean commits, so that bisect will continue to work. +At the same time, it's best to avoid making destructive updates. If +you're planning on doing lots of squashing, then work in a branch +and don't commit directly to development - and **definitely** not to +master. #### Follow the Contributing guidelines What's good for the goose... -#### After releasing, send out a mail +#### Making API documentation available on the website + +Currently this is a manual process. If you don't sed/awk out the +reference/link paths, it'll be a mess. We will add a script to +handle this some time soon. + +There is also an open ticket to set up nightly builds, which will +update the HEAD haddocks (on the website) and produce an 'sdist' +bundle and add that to the website too. + +See https://cloud-haskell.atlassian.net/browse/INFRA-1 for details. + +### Release Process + +First of all, a few prior warnings. **Do not** tag any projects +until *after* you've finished the release. If you build and tag +three projects, only to find that a subsequent dependent package +needs a bit of last minute surgery, you'll be sorry you didn't +wait. With that in mind.... + +Before releasing any source code, make sure that all the jira tickets +added to the release are either resolved or remove them from the +release if you've decided to exclude them. + +First, make sure all the version numbers and dependencies are aligned. + +* bump the version numbers for each project that is being released +* bump the dependency versions across each project if needed +* make sure you've run a complete integration build and everything still installs ok +* bump the dependency version numbers in the cloud-haskell meta package + +Now you'll want to go and update the change log and release notes for each +project. Change logs are stored in the individual project's git repository, +whilst release notes are stored on the wiki. This is easy to forget, as I +discovered! Change logs should be more concise than full blown release +notes. + +Generate the packages with `cabal sdist` and test them all locally, then +upload them to hackage. Don't forget to upload the cloud-haskell meta-package +too! + +#### After the release + +**Now** you should tag all the projects with the relevant version number. +Since moving to individual git repositories, the tagging scheme is now +`x.y.z` and *not* `-x.y.z`. + +Once the release is out, you should go to [JIRA](https://cloud-haskell.atlassian.net) +and close all the tickets for the release. Jira has a nice 'bulk change' +feature that makes this very easy. -To the Parallel Haskell Mailing List, and anywhere else that makes -sense. +After that, it's time to tweet about the release, post to the parallel-haskell +mailing list, blog etc. Spread the word. diff --git a/wiki/networktransport.md b/wiki/networktransport.md index 63220b27..640abe75 100644 --- a/wiki/networktransport.md +++ b/wiki/networktransport.md @@ -38,7 +38,7 @@ completely stable yet. The design of the transport layer may also still change. Feedback and suggestions are most welcome. Email [Duncan](mailto:duncan@well-typed.com) or [Edsko](mailto:edsko@well-typed.com) at Well-Typed, find us at #HaskellTransportLayer on freenode, or post on the [Parallel Haskell][2] mailing list. -You may also submit issues on the [JIRA issue tracker][3]. +You may also submit issues on the [JIRA issue tracker][8]. ### Hello World @@ -112,3 +112,4 @@ If you are interested in helping out, please add a brief paragraph to [5]: /wiki/newtransports.html [6]: /wiki/newdesign.html [7]: /wiki/protocols.html +[8]: https://cloud-haskell.atlassian.net/issues/?filter=10002 diff --git a/wiki/newdesign.md b/wiki/newdesign.md index 13e1a371..5cd14089 100644 --- a/wiki/newdesign.md +++ b/wiki/newdesign.md @@ -100,6 +100,8 @@ A particular challenge is the per-connection performance parameters. It is vital The following diagram shows dependencies between the various modules for the initial Cloud Haskell implementation. Arrows represent explicit module dependencies. +---- + +------------------------------+ | Application | +------------------------------+ @@ -114,11 +116,14 @@ The following diagram shows dependencies between the various modules for the ini | Haskell network (IP) library | +------------------------------+ +---- + As the diagram indicates, the initial implementation is monolithic and uses a single specific transport (TCP/IP). The next diagram shows the various modules that are envisaged in the new design. We partition the system into the Cloud Haskell layer and a separate network transport layer. Each of the two layers has backend packages for different transports. -{% highlight %} +---- + +------------------------------------------------------------+ | Application | +------------------------------------------------------------+ @@ -139,7 +144,8 @@ The next diagram shows the various modules that are envisaged in the new design. +------------------------------+ | Haskell/C Transport Library | +------------------------------+ -{% endhighlight %} + +---- We still expect applications to use the the Cloud Haskell layer directly. Additionally the application also depends on a specific Cloud Haskell backend, which provides functions to allow the initialisation of the transport layer using whatever topology might be appropriate to the application. From b58b3c89bceb0b5c0a5ca35f89666f938a15abf6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Wed, 30 Jan 2013 22:48:03 +0000 Subject: [PATCH 0904/2357] point to the right issue tracker --- contact.html | 2 +- .../Paths_distributed_process_platform.hs | 32 ---------- dist/build/autogen/cabal_macros.h | 58 ------------------- dist/setup-config | 2 - 4 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 dist/build/autogen/Paths_distributed_process_platform.hs delete mode 100644 dist/build/autogen/cabal_macros.h delete mode 100644 dist/setup-config diff --git a/contact.html b/contact.html index ffed7d7a..9a8385d4 100644 --- a/contact.html +++ b/contact.html @@ -24,7 +24,7 @@

    IRC

    Issues Tracker

    Notice a problem that you don't know how to fix? Or want to request a new feature? (in order to create new issues you will need to create a login)

    -

    Browse Issues

    +

    Browse Issues

    diff --git a/dist/build/autogen/Paths_distributed_process_platform.hs b/dist/build/autogen/Paths_distributed_process_platform.hs deleted file mode 100644 index 170d157a..00000000 --- a/dist/build/autogen/Paths_distributed_process_platform.hs +++ /dev/null @@ -1,32 +0,0 @@ -module Paths_distributed_process_platform ( - version, - getBinDir, getLibDir, getDataDir, getLibexecDir, - getDataFileName - ) where - -import qualified Control.Exception as Exception -import Data.Version (Version(..)) -import System.Environment (getEnv) -catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a -catchIO = Exception.catch - - -version :: Version -version = Version {versionBranch = [0,1,0], versionTags = []} -bindir, libdir, datadir, libexecdir :: FilePath - -bindir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/bin" -libdir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/lib" -datadir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/share" -libexecdir = "/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-platform-0.1.0/libexec" - -getBinDir, getLibDir, getDataDir, getLibexecDir :: IO FilePath -getBinDir = catchIO (getEnv "distributed_process_platform_bindir") (\_ -> return bindir) -getLibDir = catchIO (getEnv "distributed_process_platform_libdir") (\_ -> return libdir) -getDataDir = catchIO (getEnv "distributed_process_platform_datadir") (\_ -> return datadir) -getLibexecDir = catchIO (getEnv "distributed_process_platform_libexecdir") (\_ -> return libexecdir) - -getDataFileName :: FilePath -> IO FilePath -getDataFileName name = do - dir <- getDataDir - return (dir ++ "/" ++ name) diff --git a/dist/build/autogen/cabal_macros.h b/dist/build/autogen/cabal_macros.h deleted file mode 100644 index f47225c8..00000000 --- a/dist/build/autogen/cabal_macros.h +++ /dev/null @@ -1,58 +0,0 @@ -/* DO NOT EDIT: This file is automatically generated by Cabal */ - -/* package base-4.5.1.0 */ -#define VERSION_base "4.5.1.0" -#define MIN_VERSION_base(major1,major2,minor) (\ - (major1) < 4 || \ - (major1) == 4 && (major2) < 5 || \ - (major1) == 4 && (major2) == 5 && (minor) <= 1) - -/* package binary-0.5.1.0 */ -#define VERSION_binary "0.5.1.0" -#define MIN_VERSION_binary(major1,major2,minor) (\ - (major1) < 0 || \ - (major1) == 0 && (major2) < 5 || \ - (major1) == 0 && (major2) == 5 && (minor) <= 1) - -/* package containers-0.4.2.1 */ -#define VERSION_containers "0.4.2.1" -#define MIN_VERSION_containers(major1,major2,minor) (\ - (major1) < 0 || \ - (major1) == 0 && (major2) < 4 || \ - (major1) == 0 && (major2) == 4 && (minor) <= 2) - -/* package derive-2.5.11 */ -#define VERSION_derive "2.5.11" -#define MIN_VERSION_derive(major1,major2,minor) (\ - (major1) < 2 || \ - (major1) == 2 && (major2) < 5 || \ - (major1) == 2 && (major2) == 5 && (minor) <= 11) - -/* package distributed-process-0.4.2 */ -#define VERSION_distributed_process "0.4.2" -#define MIN_VERSION_distributed_process(major1,major2,minor) (\ - (major1) < 0 || \ - (major1) == 0 && (major2) < 4 || \ - (major1) == 0 && (major2) == 4 && (minor) <= 2) - -/* package mtl-2.1.2 */ -#define VERSION_mtl "2.1.2" -#define MIN_VERSION_mtl(major1,major2,minor) (\ - (major1) < 2 || \ - (major1) == 2 && (major2) < 1 || \ - (major1) == 2 && (major2) == 1 && (minor) <= 2) - -/* package stm-2.4 */ -#define VERSION_stm "2.4" -#define MIN_VERSION_stm(major1,major2,minor) (\ - (major1) < 2 || \ - (major1) == 2 && (major2) < 4 || \ - (major1) == 2 && (major2) == 4 && (minor) <= 0) - -/* package transformers-0.3.0.0 */ -#define VERSION_transformers "0.3.0.0" -#define MIN_VERSION_transformers(major1,major2,minor) (\ - (major1) < 0 || \ - (major1) == 0 && (major2) < 3 || \ - (major1) == 0 && (major2) == 3 && (minor) <= 0) - diff --git a/dist/setup-config b/dist/setup-config deleted file mode 100644 index 57ae3b40..00000000 --- a/dist/setup-config +++ /dev/null @@ -1,2 +0,0 @@ -Saved package config for distributed-process-platform-0.1.0 written by Cabal-1.14.0 using ghc-7.4 -LocalBuildInfo {configFlags = ConfigFlags {configPrograms = [], configProgramPaths = [], configProgramArgs = [], configHcFlavor = Flag GHC, configHcPath = NoFlag, configHcPkg = NoFlag, configVanillaLib = Flag True, configProfLib = Flag True, configSharedLib = Flag False, configDynExe = Flag False, configProfExe = Flag False, configConfigureArgs = [], configOptimization = Flag NormalOptimisation, configProgPrefix = Flag "", configProgSuffix = Flag "", configInstallDirs = InstallDirs {prefix = Flag "/Users/t4/Library/Haskell/$compiler/lib/$pkgid", bindir = NoFlag, libdir = NoFlag, libsubdir = Flag "", dynlibdir = NoFlag, libexecdir = NoFlag, progdir = NoFlag, includedir = NoFlag, datadir = NoFlag, datasubdir = Flag "", docdir = Flag "$prefix/doc", mandir = NoFlag, htmldir = NoFlag, haddockdir = NoFlag}, configScratchDir = NoFlag, configExtraLibDirs = [], configExtraIncludeDirs = [], configDistPref = Flag "dist", configVerbosity = Flag Normal, configUserInstall = Flag True, configPackageDB = NoFlag, configGHCiLib = Flag True, configSplitObjs = Flag False, configStripExes = Flag True, configConstraints = [Dependency (PackageName "transformers") (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})),Dependency (PackageName "stm") (ThisVersion (Version {versionBranch = [2,4], versionTags = []})),Dependency (PackageName "mtl") (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []})),Dependency (PackageName "distributed-process") (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []})),Dependency (PackageName "derive") (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []})),Dependency (PackageName "containers") (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []})),Dependency (PackageName "binary") (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []})),Dependency (PackageName "base") (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))], configConfigurationsFlags = [], configTests = Flag False, configBenchmarks = Flag False, configLibCoverage = Flag False}, extraConfigArgs = [], installDirTemplates = InstallDirs {prefix = "/Users/t4/Library/Haskell/$compiler/lib/$pkgid", bindir = "$prefix/bin", libdir = "$prefix/lib", libsubdir = "", dynlibdir = "$libdir", libexecdir = "$prefix/libexec", progdir = "$libdir/hugs/programs", includedir = "$libdir/$libsubdir/include", datadir = "$prefix/share", datasubdir = "", docdir = "$prefix/doc", mandir = "$datadir/man", htmldir = "$docdir/html", haddockdir = "$htmldir"}, compiler = Compiler {compilerId = CompilerId GHC (Version {versionBranch = [7,4,2], versionTags = []}), compilerLanguages = [(Haskell98,"-XHaskell98"),(Haskell2010,"-XHaskell2010")], compilerExtensions = [(UnknownExtension "Haskell98","-XHaskell98"),(UnknownExtension "Haskell2010","-XHaskell2010"),(UnknownExtension "Unsafe","-XUnsafe"),(EnableExtension Trustworthy,"-XTrustworthy"),(EnableExtension Safe,"-XSafe"),(EnableExtension CPP,"-XCPP"),(DisableExtension CPP,"-XNoCPP"),(EnableExtension PostfixOperators,"-XPostfixOperators"),(DisableExtension PostfixOperators,"-XNoPostfixOperators"),(EnableExtension TupleSections,"-XTupleSections"),(DisableExtension TupleSections,"-XNoTupleSections"),(EnableExtension PatternGuards,"-XPatternGuards"),(DisableExtension PatternGuards,"-XNoPatternGuards"),(EnableExtension UnicodeSyntax,"-XUnicodeSyntax"),(DisableExtension UnicodeSyntax,"-XNoUnicodeSyntax"),(EnableExtension MagicHash,"-XMagicHash"),(DisableExtension MagicHash,"-XNoMagicHash"),(EnableExtension PolymorphicComponents,"-XPolymorphicComponents"),(DisableExtension PolymorphicComponents,"-XNoPolymorphicComponents"),(EnableExtension ExistentialQuantification,"-XExistentialQuantification"),(DisableExtension ExistentialQuantification,"-XNoExistentialQuantification"),(EnableExtension KindSignatures,"-XKindSignatures"),(DisableExtension KindSignatures,"-XNoKindSignatures"),(EnableExtension EmptyDataDecls,"-XEmptyDataDecls"),(DisableExtension EmptyDataDecls,"-XNoEmptyDataDecls"),(EnableExtension ParallelListComp,"-XParallelListComp"),(DisableExtension ParallelListComp,"-XNoParallelListComp"),(EnableExtension TransformListComp,"-XTransformListComp"),(DisableExtension TransformListComp,"-XNoTransformListComp"),(UnknownExtension "MonadComprehensions","-XMonadComprehensions"),(UnknownExtension "NoMonadComprehensions","-XNoMonadComprehensions"),(EnableExtension ForeignFunctionInterface,"-XForeignFunctionInterface"),(DisableExtension ForeignFunctionInterface,"-XNoForeignFunctionInterface"),(EnableExtension UnliftedFFITypes,"-XUnliftedFFITypes"),(DisableExtension UnliftedFFITypes,"-XNoUnliftedFFITypes"),(UnknownExtension "InterruptibleFFI","-XInterruptibleFFI"),(UnknownExtension "NoInterruptibleFFI","-XNoInterruptibleFFI"),(UnknownExtension "CApiFFI","-XCApiFFI"),(UnknownExtension "NoCApiFFI","-XNoCApiFFI"),(EnableExtension GHCForeignImportPrim,"-XGHCForeignImportPrim"),(DisableExtension GHCForeignImportPrim,"-XNoGHCForeignImportPrim"),(EnableExtension LiberalTypeSynonyms,"-XLiberalTypeSynonyms"),(DisableExtension LiberalTypeSynonyms,"-XNoLiberalTypeSynonyms"),(EnableExtension Rank2Types,"-XRank2Types"),(DisableExtension Rank2Types,"-XNoRank2Types"),(EnableExtension RankNTypes,"-XRankNTypes"),(DisableExtension RankNTypes,"-XNoRankNTypes"),(EnableExtension ImpredicativeTypes,"-XImpredicativeTypes"),(DisableExtension ImpredicativeTypes,"-XNoImpredicativeTypes"),(EnableExtension TypeOperators,"-XTypeOperators"),(DisableExtension TypeOperators,"-XNoTypeOperators"),(EnableExtension RecursiveDo,"-XRecursiveDo"),(DisableExtension RecursiveDo,"-XNoRecursiveDo"),(EnableExtension DoRec,"-XDoRec"),(DisableExtension DoRec,"-XNoDoRec"),(EnableExtension Arrows,"-XArrows"),(DisableExtension Arrows,"-XNoArrows"),(UnknownExtension "ParallelArrays","-XParallelArrays"),(UnknownExtension "NoParallelArrays","-XNoParallelArrays"),(EnableExtension TemplateHaskell,"-XTemplateHaskell"),(DisableExtension TemplateHaskell,"-XNoTemplateHaskell"),(EnableExtension QuasiQuotes,"-XQuasiQuotes"),(DisableExtension QuasiQuotes,"-XNoQuasiQuotes"),(EnableExtension ImplicitPrelude,"-XImplicitPrelude"),(DisableExtension ImplicitPrelude,"-XNoImplicitPrelude"),(EnableExtension RecordWildCards,"-XRecordWildCards"),(DisableExtension RecordWildCards,"-XNoRecordWildCards"),(EnableExtension NamedFieldPuns,"-XNamedFieldPuns"),(DisableExtension NamedFieldPuns,"-XNoNamedFieldPuns"),(EnableExtension RecordPuns,"-XRecordPuns"),(DisableExtension RecordPuns,"-XNoRecordPuns"),(EnableExtension DisambiguateRecordFields,"-XDisambiguateRecordFields"),(DisableExtension DisambiguateRecordFields,"-XNoDisambiguateRecordFields"),(EnableExtension OverloadedStrings,"-XOverloadedStrings"),(DisableExtension OverloadedStrings,"-XNoOverloadedStrings"),(EnableExtension GADTs,"-XGADTs"),(DisableExtension GADTs,"-XNoGADTs"),(EnableExtension GADTSyntax,"-XGADTSyntax"),(DisableExtension GADTSyntax,"-XNoGADTSyntax"),(EnableExtension ViewPatterns,"-XViewPatterns"),(DisableExtension ViewPatterns,"-XNoViewPatterns"),(EnableExtension TypeFamilies,"-XTypeFamilies"),(DisableExtension TypeFamilies,"-XNoTypeFamilies"),(EnableExtension BangPatterns,"-XBangPatterns"),(DisableExtension BangPatterns,"-XNoBangPatterns"),(EnableExtension MonomorphismRestriction,"-XMonomorphismRestriction"),(DisableExtension MonomorphismRestriction,"-XNoMonomorphismRestriction"),(EnableExtension NPlusKPatterns,"-XNPlusKPatterns"),(DisableExtension NPlusKPatterns,"-XNoNPlusKPatterns"),(EnableExtension DoAndIfThenElse,"-XDoAndIfThenElse"),(DisableExtension DoAndIfThenElse,"-XNoDoAndIfThenElse"),(EnableExtension RebindableSyntax,"-XRebindableSyntax"),(DisableExtension RebindableSyntax,"-XNoRebindableSyntax"),(EnableExtension ConstraintKinds,"-XConstraintKinds"),(DisableExtension ConstraintKinds,"-XNoConstraintKinds"),(UnknownExtension "PolyKinds","-XPolyKinds"),(UnknownExtension "NoPolyKinds","-XNoPolyKinds"),(UnknownExtension "DataKinds","-XDataKinds"),(UnknownExtension "NoDataKinds","-XNoDataKinds"),(EnableExtension MonoPatBinds,"-XMonoPatBinds"),(DisableExtension MonoPatBinds,"-XNoMonoPatBinds"),(EnableExtension ExplicitForAll,"-XExplicitForAll"),(DisableExtension ExplicitForAll,"-XNoExplicitForAll"),(UnknownExtension "AlternativeLayoutRule","-XAlternativeLayoutRule"),(UnknownExtension "NoAlternativeLayoutRule","-XNoAlternativeLayoutRule"),(UnknownExtension "AlternativeLayoutRuleTransitional","-XAlternativeLayoutRuleTransitional"),(UnknownExtension "NoAlternativeLayoutRuleTransitional","-XNoAlternativeLayoutRuleTransitional"),(EnableExtension DatatypeContexts,"-XDatatypeContexts"),(DisableExtension DatatypeContexts,"-XNoDatatypeContexts"),(EnableExtension NondecreasingIndentation,"-XNondecreasingIndentation"),(DisableExtension NondecreasingIndentation,"-XNoNondecreasingIndentation"),(UnknownExtension "RelaxedLayout","-XRelaxedLayout"),(UnknownExtension "NoRelaxedLayout","-XNoRelaxedLayout"),(UnknownExtension "TraditionalRecordSyntax","-XTraditionalRecordSyntax"),(UnknownExtension "NoTraditionalRecordSyntax","-XNoTraditionalRecordSyntax"),(EnableExtension MonoLocalBinds,"-XMonoLocalBinds"),(DisableExtension MonoLocalBinds,"-XNoMonoLocalBinds"),(EnableExtension RelaxedPolyRec,"-XRelaxedPolyRec"),(DisableExtension RelaxedPolyRec,"-XNoRelaxedPolyRec"),(EnableExtension ExtendedDefaultRules,"-XExtendedDefaultRules"),(DisableExtension ExtendedDefaultRules,"-XNoExtendedDefaultRules"),(EnableExtension ImplicitParams,"-XImplicitParams"),(DisableExtension ImplicitParams,"-XNoImplicitParams"),(EnableExtension ScopedTypeVariables,"-XScopedTypeVariables"),(DisableExtension ScopedTypeVariables,"-XNoScopedTypeVariables"),(EnableExtension PatternSignatures,"-XPatternSignatures"),(DisableExtension PatternSignatures,"-XNoPatternSignatures"),(EnableExtension UnboxedTuples,"-XUnboxedTuples"),(DisableExtension UnboxedTuples,"-XNoUnboxedTuples"),(EnableExtension StandaloneDeriving,"-XStandaloneDeriving"),(DisableExtension StandaloneDeriving,"-XNoStandaloneDeriving"),(EnableExtension DeriveDataTypeable,"-XDeriveDataTypeable"),(DisableExtension DeriveDataTypeable,"-XNoDeriveDataTypeable"),(EnableExtension DeriveFunctor,"-XDeriveFunctor"),(DisableExtension DeriveFunctor,"-XNoDeriveFunctor"),(EnableExtension DeriveTraversable,"-XDeriveTraversable"),(DisableExtension DeriveTraversable,"-XNoDeriveTraversable"),(EnableExtension DeriveFoldable,"-XDeriveFoldable"),(DisableExtension DeriveFoldable,"-XNoDeriveFoldable"),(UnknownExtension "DeriveGeneric","-XDeriveGeneric"),(UnknownExtension "NoDeriveGeneric","-XNoDeriveGeneric"),(UnknownExtension "DefaultSignatures","-XDefaultSignatures"),(UnknownExtension "NoDefaultSignatures","-XNoDefaultSignatures"),(EnableExtension TypeSynonymInstances,"-XTypeSynonymInstances"),(DisableExtension TypeSynonymInstances,"-XNoTypeSynonymInstances"),(EnableExtension FlexibleContexts,"-XFlexibleContexts"),(DisableExtension FlexibleContexts,"-XNoFlexibleContexts"),(EnableExtension FlexibleInstances,"-XFlexibleInstances"),(DisableExtension FlexibleInstances,"-XNoFlexibleInstances"),(EnableExtension ConstrainedClassMethods,"-XConstrainedClassMethods"),(DisableExtension ConstrainedClassMethods,"-XNoConstrainedClassMethods"),(EnableExtension MultiParamTypeClasses,"-XMultiParamTypeClasses"),(DisableExtension MultiParamTypeClasses,"-XNoMultiParamTypeClasses"),(EnableExtension FunctionalDependencies,"-XFunctionalDependencies"),(DisableExtension FunctionalDependencies,"-XNoFunctionalDependencies"),(EnableExtension GeneralizedNewtypeDeriving,"-XGeneralizedNewtypeDeriving"),(DisableExtension GeneralizedNewtypeDeriving,"-XNoGeneralizedNewtypeDeriving"),(EnableExtension OverlappingInstances,"-XOverlappingInstances"),(DisableExtension OverlappingInstances,"-XNoOverlappingInstances"),(EnableExtension UndecidableInstances,"-XUndecidableInstances"),(DisableExtension UndecidableInstances,"-XNoUndecidableInstances"),(EnableExtension IncoherentInstances,"-XIncoherentInstances"),(DisableExtension IncoherentInstances,"-XNoIncoherentInstances"),(EnableExtension PackageImports,"-XPackageImports"),(DisableExtension PackageImports,"-XNoPackageImports")]}, buildDir = "dist/build", scratchDir = "dist/scratch", libraryConfig = Just (ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}), executableConfigs = [], compBuildOrder = [CLibName], testSuiteConfigs = [("GenServerTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}),("AsyncTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}),("PrimitivesTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]}),("TimerTests",ComponentLocalBuildInfo {componentPackageDeps = [(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}})]})], benchmarkConfigs = [], installedPkgs = PackageIndex (fromList [(InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageInfo {installedPackageId = InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297", sourcePackageId = PackageIdentifier {pkgName = PackageName "array", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Mutable and immutable arrays", description = "This package defines the classes @IArray@ of immutable arrays and\n@MArray@ of arrays mutable within appropriate monads, as well as\nsome instances of these classes.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Array","Base"],ModuleName ["Data","Array","IArray"],ModuleName ["Data","Array","IO"],ModuleName ["Data","Array","IO","Safe"],ModuleName ["Data","Array","IO","Internals"],ModuleName ["Data","Array","MArray"],ModuleName ["Data","Array","MArray","Safe"],ModuleName ["Data","Array","ST"],ModuleName ["Data","Array","ST","Safe"],ModuleName ["Data","Array","Storable"],ModuleName ["Data","Array","Storable","Safe"],ModuleName ["Data","Array","Storable","Internals"],ModuleName ["Data","Array","Unboxed"],ModuleName ["Data","Array","Unsafe"],ModuleName ["Data","Array"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], hsLibraries = ["HSarray-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0/array.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0"]}),(InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageInfo {installedPackageId = InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd", sourcePackageId = PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Basic libraries", description = "This package contains the Prelude and its support libraries,\nand a large collection of useful libraries ranging from data\nstructures to parsing combinators and debugging utilities.", category = "", exposed = True, exposedModules = [ModuleName ["Foreign","Concurrent"],ModuleName ["GHC","Arr"],ModuleName ["GHC","Base"],ModuleName ["GHC","Conc"],ModuleName ["GHC","Conc","IO"],ModuleName ["GHC","Conc","Signal"],ModuleName ["GHC","Conc","Sync"],ModuleName ["GHC","ConsoleHandler"],ModuleName ["GHC","Constants"],ModuleName ["GHC","Desugar"],ModuleName ["GHC","Enum"],ModuleName ["GHC","Environment"],ModuleName ["GHC","Err"],ModuleName ["GHC","Exception"],ModuleName ["GHC","Exts"],ModuleName ["GHC","Fingerprint"],ModuleName ["GHC","Fingerprint","Type"],ModuleName ["GHC","Float"],ModuleName ["GHC","Float","ConversionUtils"],ModuleName ["GHC","Float","RealFracMethods"],ModuleName ["GHC","Foreign"],ModuleName ["GHC","ForeignPtr"],ModuleName ["GHC","Handle"],ModuleName ["GHC","IO"],ModuleName ["GHC","IO","Buffer"],ModuleName ["GHC","IO","BufferedIO"],ModuleName ["GHC","IO","Device"],ModuleName ["GHC","IO","Encoding"],ModuleName ["GHC","IO","Encoding","CodePage"],ModuleName ["GHC","IO","Encoding","Failure"],ModuleName ["GHC","IO","Encoding","Iconv"],ModuleName ["GHC","IO","Encoding","Latin1"],ModuleName ["GHC","IO","Encoding","Types"],ModuleName ["GHC","IO","Encoding","UTF16"],ModuleName ["GHC","IO","Encoding","UTF32"],ModuleName ["GHC","IO","Encoding","UTF8"],ModuleName ["GHC","IO","Exception"],ModuleName ["GHC","IO","FD"],ModuleName ["GHC","IO","Handle"],ModuleName ["GHC","IO","Handle","FD"],ModuleName ["GHC","IO","Handle","Internals"],ModuleName ["GHC","IO","Handle","Text"],ModuleName ["GHC","IO","Handle","Types"],ModuleName ["GHC","IO","IOMode"],ModuleName ["GHC","IOArray"],ModuleName ["GHC","IOBase"],ModuleName ["GHC","IORef"],ModuleName ["GHC","Int"],ModuleName ["GHC","List"],ModuleName ["GHC","MVar"],ModuleName ["GHC","Num"],ModuleName ["GHC","PArr"],ModuleName ["GHC","Pack"],ModuleName ["GHC","Ptr"],ModuleName ["GHC","Read"],ModuleName ["GHC","Real"],ModuleName ["GHC","ST"],ModuleName ["GHC","Stack"],ModuleName ["GHC","Stats"],ModuleName ["GHC","Show"],ModuleName ["GHC","Stable"],ModuleName ["GHC","Storable"],ModuleName ["GHC","STRef"],ModuleName ["GHC","TopHandler"],ModuleName ["GHC","Unicode"],ModuleName ["GHC","Weak"],ModuleName ["GHC","Word"],ModuleName ["System","Timeout"],ModuleName ["GHC","Event"],ModuleName ["Control","Applicative"],ModuleName ["Control","Arrow"],ModuleName ["Control","Category"],ModuleName ["Control","Concurrent"],ModuleName ["Control","Concurrent","Chan"],ModuleName ["Control","Concurrent","MVar"],ModuleName ["Control","Concurrent","QSem"],ModuleName ["Control","Concurrent","QSemN"],ModuleName ["Control","Concurrent","SampleVar"],ModuleName ["Control","Exception"],ModuleName ["Control","Exception","Base"],ModuleName ["Control","OldException"],ModuleName ["Control","Monad"],ModuleName ["Control","Monad","Fix"],ModuleName ["Control","Monad","Instances"],ModuleName ["Control","Monad","ST"],ModuleName ["Control","Monad","ST","Safe"],ModuleName ["Control","Monad","ST","Unsafe"],ModuleName ["Control","Monad","ST","Lazy"],ModuleName ["Control","Monad","ST","Lazy","Safe"],ModuleName ["Control","Monad","ST","Lazy","Unsafe"],ModuleName ["Control","Monad","ST","Strict"],ModuleName ["Control","Monad","Zip"],ModuleName ["Data","Bits"],ModuleName ["Data","Bool"],ModuleName ["Data","Char"],ModuleName ["Data","Complex"],ModuleName ["Data","Dynamic"],ModuleName ["Data","Either"],ModuleName ["Data","Eq"],ModuleName ["Data","Data"],ModuleName ["Data","Fixed"],ModuleName ["Data","Foldable"],ModuleName ["Data","Function"],ModuleName ["Data","Functor"],ModuleName ["Data","HashTable"],ModuleName ["Data","IORef"],ModuleName ["Data","Int"],ModuleName ["Data","Ix"],ModuleName ["Data","List"],ModuleName ["Data","Maybe"],ModuleName ["Data","Monoid"],ModuleName ["Data","Ord"],ModuleName ["Data","Ratio"],ModuleName ["Data","STRef"],ModuleName ["Data","STRef","Lazy"],ModuleName ["Data","STRef","Strict"],ModuleName ["Data","String"],ModuleName ["Data","Traversable"],ModuleName ["Data","Tuple"],ModuleName ["Data","Typeable"],ModuleName ["Data","Typeable","Internal"],ModuleName ["Data","Unique"],ModuleName ["Data","Version"],ModuleName ["Data","Word"],ModuleName ["Debug","Trace"],ModuleName ["Foreign"],ModuleName ["Foreign","C"],ModuleName ["Foreign","C","Error"],ModuleName ["Foreign","C","String"],ModuleName ["Foreign","C","Types"],ModuleName ["Foreign","ForeignPtr"],ModuleName ["Foreign","ForeignPtr","Safe"],ModuleName ["Foreign","ForeignPtr","Unsafe"],ModuleName ["Foreign","Marshal"],ModuleName ["Foreign","Marshal","Alloc"],ModuleName ["Foreign","Marshal","Array"],ModuleName ["Foreign","Marshal","Error"],ModuleName ["Foreign","Marshal","Pool"],ModuleName ["Foreign","Marshal","Safe"],ModuleName ["Foreign","Marshal","Utils"],ModuleName ["Foreign","Marshal","Unsafe"],ModuleName ["Foreign","Ptr"],ModuleName ["Foreign","Safe"],ModuleName ["Foreign","StablePtr"],ModuleName ["Foreign","Storable"],ModuleName ["Numeric"],ModuleName ["Prelude"],ModuleName ["System","Console","GetOpt"],ModuleName ["System","CPUTime"],ModuleName ["System","Environment"],ModuleName ["System","Exit"],ModuleName ["System","IO"],ModuleName ["System","IO","Error"],ModuleName ["System","IO","Unsafe"],ModuleName ["System","Info"],ModuleName ["System","Mem"],ModuleName ["System","Mem","StableName"],ModuleName ["System","Mem","Weak"],ModuleName ["System","Posix","Internals"],ModuleName ["System","Posix","Types"],ModuleName ["Text","ParserCombinators","ReadP"],ModuleName ["Text","ParserCombinators","ReadPrec"],ModuleName ["Text","Printf"],ModuleName ["Text","Read"],ModuleName ["Text","Read","Lex"],ModuleName ["Text","Show"],ModuleName ["Text","Show","Functions"],ModuleName ["Unsafe","Coerce"]], hiddenModules = [ModuleName ["GHC","Event","Array"],ModuleName ["GHC","Event","Clock"],ModuleName ["GHC","Event","Control"],ModuleName ["GHC","Event","EPoll"],ModuleName ["GHC","Event","IntMap"],ModuleName ["GHC","Event","Internal"],ModuleName ["GHC","Event","KQueue"],ModuleName ["GHC","Event","Manager"],ModuleName ["GHC","Event","PSQ"],ModuleName ["GHC","Event","Poll"],ModuleName ["GHC","Event","Thread"],ModuleName ["GHC","Event","Unique"],ModuleName ["Control","Monad","ST","Imp"],ModuleName ["Control","Monad","ST","Lazy","Imp"],ModuleName ["Foreign","ForeignPtr","Imp"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], hsLibraries = ["HSbase-4.5.1.0"], extraLibraries = ["iconv"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0/include"], includes = ["HsBase.h"], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0/base.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0"]}),(InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageInfo {installedPackageId = InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b", sourcePackageId = PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Lennart Kolmodin, Don Stewart ", author = "Lennart Kolmodin ", stability = "provisional", homepage = "http://code.haskell.org/binary/", pkgUrl = "", synopsis = "Binary serialisation for Haskell values using lazy ByteStrings", description = "Efficient, pure binary serialisation using lazy ByteStrings.\nHaskell values may be encoded to and from binary formats,\nwritten to disk as binary, or sent over the network.\nSerialisation speeds of over 1 G\\/sec have been observed,\nso this library should be suitable for high performance\nscenarios.", category = "Data, Parsing", exposed = True, exposedModules = [ModuleName ["Data","Binary"],ModuleName ["Data","Binary","Put"],ModuleName ["Data","Binary","Get"],ModuleName ["Data","Binary","Builder"],ModuleName ["Data","Binary","Builder","Internal"]], hiddenModules = [ModuleName ["Data","Binary","Builder","Base"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], hsLibraries = ["HSbinary-0.5.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0/binary.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0"]}),(InstalledPackageId "builtin_rts",InstalledPackageInfo {installedPackageId = InstalledPackageId "builtin_rts", sourcePackageId = PackageIdentifier {pkgName = PackageName "rts", pkgVersion = Version {versionBranch = [1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "glasgow-haskell-users@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "", category = "", exposed = True, exposedModules = [], hiddenModules = [], trusted = False, importDirs = [], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2"], hsLibraries = ["HSrts"], extraLibraries = ["m","dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/include"], includes = ["Stg.h"], depends = [], hugsOptions = [], ccOptions = [], ldOptions = ["-u","_ghczmprim_GHCziTypes_Izh_static_info","-u","_ghczmprim_GHCziTypes_Czh_static_info","-u","_ghczmprim_GHCziTypes_Fzh_static_info","-u","_ghczmprim_GHCziTypes_Dzh_static_info","-u","_base_GHCziPtr_Ptr_static_info","-u","_base_GHCziWord_Wzh_static_info","-u","_base_GHCziInt_I8zh_static_info","-u","_base_GHCziInt_I16zh_static_info","-u","_base_GHCziInt_I32zh_static_info","-u","_base_GHCziInt_I64zh_static_info","-u","_base_GHCziWord_W8zh_static_info","-u","_base_GHCziWord_W16zh_static_info","-u","_base_GHCziWord_W32zh_static_info","-u","_base_GHCziWord_W64zh_static_info","-u","_base_GHCziStable_StablePtr_static_info","-u","_ghczmprim_GHCziTypes_Izh_con_info","-u","_ghczmprim_GHCziTypes_Czh_con_info","-u","_ghczmprim_GHCziTypes_Fzh_con_info","-u","_ghczmprim_GHCziTypes_Dzh_con_info","-u","_base_GHCziPtr_Ptr_con_info","-u","_base_GHCziPtr_FunPtr_con_info","-u","_base_GHCziStable_StablePtr_con_info","-u","_ghczmprim_GHCziTypes_False_closure","-u","_ghczmprim_GHCziTypes_True_closure","-u","_base_GHCziPack_unpackCString_closure","-u","_base_GHCziIOziException_stackOverflow_closure","-u","_base_GHCziIOziException_heapOverflow_closure","-u","_base_ControlziExceptionziBase_nonTermination_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnMVar_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnSTM_closure","-u","_base_ControlziExceptionziBase_nestedAtomically_closure","-u","_base_GHCziWeak_runFinalizzerBatch_closure","-u","_base_GHCziTopHandler_flushStdHandles_closure","-u","_base_GHCziTopHandler_runIO_closure","-u","_base_GHCziTopHandler_runNonIO_closure","-u","_base_GHCziConcziIO_ensureIOManagerIsRunning_closure","-u","_base_GHCziConcziSync_runSparks_closure","-u","_base_GHCziConcziSignal_runHandlers_closure","-Wl,-search_paths_first"], frameworkDirs = [], frameworks = [], haddockInterfaces = [], haddockHTMLs = []}),(InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageInfo {installedPackageId = InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065", sourcePackageId = PackageIdentifier {pkgName = PackageName "bytestring", pkgVersion = Version {versionBranch = [0,9,2,1], versionTags = []}}, license = BSD3, copyright = "Copyright (c) Don Stewart 2005-2009,\n(c) Duncan Coutts 2006-2009,\n(c) David Roundy 2003-2005.", maintainer = "dons00@gmail.com, duncan@community.haskell.org", author = "Don Stewart, Duncan Coutts", stability = "", homepage = "http://www.cse.unsw.edu.au/~dons/fps.html", pkgUrl = "", synopsis = "Fast, packed, strict and lazy byte arrays with a list interface", description = "A time and space-efficient implementation of byte vectors using\npacked Word8 arrays, suitable for high performance use, both in terms\nof large data quantities, or high speed requirements. Byte vectors\nare encoded as strict 'Word8' arrays of bytes, and lazy lists of\nstrict chunks, held in a 'ForeignPtr', and can be passed between C\nand Haskell with little effort.\n\nTest coverage data for this library is available at:\n", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","ByteString"],ModuleName ["Data","ByteString","Char8"],ModuleName ["Data","ByteString","Unsafe"],ModuleName ["Data","ByteString","Internal"],ModuleName ["Data","ByteString","Lazy"],ModuleName ["Data","ByteString","Lazy","Char8"],ModuleName ["Data","ByteString","Lazy","Internal"],ModuleName ["Data","ByteString","Fusion"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], hsLibraries = ["HSbytestring-0.9.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1/include"], includes = ["fpstring.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1/bytestring.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1"]}),(InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageInfo {installedPackageId = InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce", sourcePackageId = PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "fox@ucw.cz", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Assorted concrete container types", description = "This package contains efficient general-purpose implementations\nof various basic immutable container types. The declared cost of\neach operation is either worst-case or amortized, but remains\nvalid even if structures are shared.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Graph"],ModuleName ["Data","Sequence"],ModuleName ["Data","Tree"],ModuleName ["Data","IntMap"],ModuleName ["Data","IntSet"],ModuleName ["Data","Map"],ModuleName ["Data","Set"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], hsLibraries = ["HScontainers-0.4.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1/containers.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1"]}),(InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4",InstalledPackageInfo {installedPackageId = InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "cpphs", pkgVersion = Version {versionBranch = [1,14], versionTags = []}}, license = LGPL Nothing, copyright = "2004-2012, Malcolm Wallace", maintainer = "Malcolm Wallace ", author = "Malcolm Wallace ", stability = "", homepage = "http://haskell.org/cpphs/", pkgUrl = "", synopsis = "A liberalised re-implementation of cpp, the C pre-processor.", description = "Cpphs is a re-implementation of the C pre-processor that is both\nmore compatible with Haskell, and itself written in Haskell so\nthat it can be distributed with compilers.\n\nThis version of the C pre-processor is pretty-much\nfeature-complete and compatible with traditional (K&R)\npre-processors. Additional features include: a plain-text mode;\nan option to unlit literate code files; and an option to turn\noff macro-expansion.", category = "Development", exposed = True, exposedModules = [ModuleName ["Language","Preprocessor","Cpphs"],ModuleName ["Language","Preprocessor","Unlit"]], hiddenModules = [ModuleName ["Language","Preprocessor","Cpphs","CppIfdef"],ModuleName ["Language","Preprocessor","Cpphs","HashDefine"],ModuleName ["Language","Preprocessor","Cpphs","MacroPass"],ModuleName ["Language","Preprocessor","Cpphs","Options"],ModuleName ["Language","Preprocessor","Cpphs","Position"],ModuleName ["Language","Preprocessor","Cpphs","ReadFirst"],ModuleName ["Language","Preprocessor","Cpphs","RunCpphs"],ModuleName ["Language","Preprocessor","Cpphs","SymTab"],ModuleName ["Language","Preprocessor","Cpphs","Tokenise"],ModuleName ["Text","ParserCombinators","HuttonMeijer"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], hsLibraries = ["HScpphs-1.14"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html/cpphs.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html"]}),(InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb",InstalledPackageInfo {installedPackageId = InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb", sourcePackageId = PackageIdentifier {pkgName = PackageName "data-accessor", pkgVersion = Version {versionBranch = [0,2,2,3], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Henning Thielemann ", author = "Henning Thielemann , Luke Palmer ", stability = "", homepage = "http://www.haskell.org/haskellwiki/Record_access", pkgUrl = "", synopsis = "Utilities for accessing and manipulating fields of records", description = "In Haskell 98 the name of a record field\nis automatically also the name of a function which gets the value\nof the according field.\nE.g. if we have\n\ndata Pair a b = Pair\nfirst :: a, second :: b\n\nthen\n\n> first :: Pair a b -> a\n> second :: Pair a b -> b\n\nHowever for setting or modifying a field value\nwe need to use some syntactic sugar, which is often clumsy.\n\nmodifyFirst :: (a -> a) -> (Pair a b -> Pair a b)\nmodifyFirst f r\\@(Pair\nfirst=a\n) = r\nfirst = f a\n\nWith this package you can define record field accessors\nwhich allow setting, getting and modifying values easily.\nThe package clearly demonstrates the power of the functional approach:\nYou can combine accessors of a record and sub-records,\nto make the access look like the fields of the sub-record belong to the main record.\n\nExample:\n\n> *Data.Accessor.Example> (first^:second^=10) (('b',7),\"hallo\")\n> (('b',10),\"hallo\")\n\nYou can easily manipulate record fields in a 'Control.Monad.State.State' monad,\nyou can easily code 'Show' instances that use the Accessor syntax\nand you can parse binary streams into records.\nSee @Data.Accessor.Example@ for demonstration of all features.\n\nIt would be great if in revised Haskell versions the names of record fields\nare automatically 'Data.Accessor.Accessor's\nrather than plain @get@ functions.\nFor now, the package @data-accessor-template@ provides Template Haskell functions\nfor automated generation of 'Data.Acesssor.Accessor's.\nSee also the other @data-accessor@ packages\nthat provide an Accessor interface to other data types.\nThe package @enumset@ provides accessors to bit-packed records.\n\nFor similar packages see @lenses@ and @fclabel@.\nA related concept are editors\n.\nEditors only consist of a modify method\n(and @modify@ applied to a 'const' function is a @set@ function).\nThis way, they can modify all function values of a function at once,\nwhereas an accessor can only change a single function value,\nsay, it can change @f 0 = 1@ to @f 0 = 2@.\nThis way, editors can even change the type of a record or a function.\nAn Arrow instance can be defined for editors,\nbut for accessors only a Category instance is possible ('(.)' method).\nThe reason is the @arr@ method of the @Arrow@ class,\nthat conflicts with the two-way nature (set and get) of accessors.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Accessor"],ModuleName ["Data","Accessor","Basic"],ModuleName ["Data","Accessor","Container"],ModuleName ["Data","Accessor","Show"],ModuleName ["Data","Accessor","Tuple"],ModuleName ["Data","Accessor","BinaryRead"],ModuleName ["Data","Accessor","MonadState"]], hiddenModules = [ModuleName ["Data","Accessor","Example"],ModuleName ["Data","Accessor","Private"],ModuleName ["Data","Accessor","MonadStatePrivate"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], hsLibraries = ["HSdata-accessor-0.2.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html/data-accessor.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html"]}),(InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageInfo {installedPackageId = InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6", sourcePackageId = PackageIdentifier {pkgName = PackageName "deepseq", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Deep evaluation of data structures", description = "This package provides methods for fully evaluating data structures\n(\\\"deep evaluation\\\"). Deep evaluation is often used for adding\nstrictness to a program, e.g. in order to force pending exceptions,\nremove space leaks, or force lazy I/O to happen. It is also useful\nin parallel programs, to ensure pending work does not migrate to the\nwrong thread.\n\nThe primary use of this package is via the 'deepseq' function, a\n\\\"deep\\\" version of 'seq'. It is implemented on top of an 'NFData'\ntypeclass (\\\"Normal Form Data\\\", data structures with no unevaluated\ncomponents) which defines strategies for fully evaluating different\ndata types.\n", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","DeepSeq"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], hsLibraries = ["HSdeepseq-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0/deepseq.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0"]}),(InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4",InstalledPackageInfo {installedPackageId = InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/derive/", pkgUrl = "", synopsis = "A program and library to derive instances for data types", description = "Data.Derive is a library and a tool for deriving instances for Haskell programs.\nIt is designed to work with custom derivations, SYB and Template Haskell mechanisms.\nThe tool requires GHC, but the generated code is portable to all compilers.\nWe see this tool as a competitor to DrIFT.", category = "Development", exposed = True, exposedModules = [ModuleName ["Data","DeriveMain"],ModuleName ["Data","DeriveTH"],ModuleName ["Data","DeriveDSL"],ModuleName ["Data","Derive","All"],ModuleName ["Data","Derive","DSL","Apply"],ModuleName ["Data","Derive","DSL","Derive"],ModuleName ["Data","Derive","DSL","DSL"],ModuleName ["Data","Derive","DSL","HSE"],ModuleName ["Data","Derive","DSL","SYB"],ModuleName ["Data","Derive","Instance","Arities"],ModuleName ["Data","Derive","Class","Arities"],ModuleName ["Data","Derive","Class","Default"],ModuleName ["Language","Haskell"],ModuleName ["Language","Haskell","Convert"],ModuleName ["Language","Haskell","TH","All"],ModuleName ["Language","Haskell","TH","Compat"],ModuleName ["Language","Haskell","TH","Data"],ModuleName ["Language","Haskell","TH","ExpandSynonym"],ModuleName ["Language","Haskell","TH","FixedPpr"],ModuleName ["Language","Haskell","TH","Helper"],ModuleName ["Language","Haskell","TH","Peephole"],ModuleName ["Data","Derive","Arbitrary"],ModuleName ["Data","Derive","ArbitraryOld"],ModuleName ["Data","Derive","Arities"],ModuleName ["Data","Derive","Binary"],ModuleName ["Data","Derive","BinaryDefer"],ModuleName ["Data","Derive","Bounded"],ModuleName ["Data","Derive","Data"],ModuleName ["Data","Derive","DataAbstract"],ModuleName ["Data","Derive","Default"],ModuleName ["Data","Derive","Enum"],ModuleName ["Data","Derive","EnumCyclic"],ModuleName ["Data","Derive","Eq"],ModuleName ["Data","Derive","Fold"],ModuleName ["Data","Derive","Foldable"],ModuleName ["Data","Derive","From"],ModuleName ["Data","Derive","Functor"],ModuleName ["Data","Derive","Has"],ModuleName ["Data","Derive","Is"],ModuleName ["Data","Derive","JSON"],ModuleName ["Data","Derive","LazySet"],ModuleName ["Data","Derive","Lens"],ModuleName ["Data","Derive","Monoid"],ModuleName ["Data","Derive","NFData"],ModuleName ["Data","Derive","Ord"],ModuleName ["Data","Derive","Read"],ModuleName ["Data","Derive","Ref"],ModuleName ["Data","Derive","Serial"],ModuleName ["Data","Derive","Serialize"],ModuleName ["Data","Derive","Set"],ModuleName ["Data","Derive","Show"],ModuleName ["Data","Derive","Traversable"],ModuleName ["Data","Derive","Typeable"],ModuleName ["Data","Derive","UniplateDirect"],ModuleName ["Data","Derive","UniplateTypeable"],ModuleName ["Data","Derive","Update"],ModuleName ["Data","Derive","Internal","Derivation"]], hiddenModules = [ModuleName ["Data","Derive","Internal","Instance"],ModuleName ["Data","Derive","Internal","Traversal"],ModuleName ["Derive","Main"],ModuleName ["Derive","Derivation"],ModuleName ["Derive","Flags"],ModuleName ["Derive","Generate"],ModuleName ["Derive","Test"],ModuleName ["Derive","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], hsLibraries = ["HSderive-2.5.11"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b",InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html/derive.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html"]}),(InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageInfo {installedPackageId = InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691", sourcePackageId = PackageIdentifier {pkgName = PackageName "directory", pkgVersion = Version {versionBranch = [1,1,0,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "library for directory handling", description = "This package provides a library for handling directories.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Directory"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], hsLibraries = ["HSdirectory-1.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2/include"], includes = ["HsDirectory.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2/directory.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2"]}),(InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd",InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "watson.timothy@gmail.com, edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Cloud Haskell: Erlang-style concurrency in Haskell", description = "This is an implementation of Cloud Haskell, as described in\n/Towards Haskell in the Cloud/ by Jeff Epstein, Andrew Black,\nand Simon Peyton Jones\n(),\nalthough some of the details are different. The precise message\npassing semantics are based on /A unified semantics for future Erlang/\nby Hans Svensson, Lars-\197ke Fredlund and Clara Benac Earle.\nYou will probably also want to install a Cloud Haskell backend such\nas distributed-process-simplelocalnet.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Process","Internal","Closure","TH"],ModuleName ["Control","Distributed","Process"],ModuleName ["Control","Distributed","Process","Serializable"],ModuleName ["Control","Distributed","Process","Closure"],ModuleName ["Control","Distributed","Process","Node"],ModuleName ["Control","Distributed","Process","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Internal","CQueue"],ModuleName ["Control","Distributed","Process","Internal","Types"],ModuleName ["Control","Distributed","Process","Internal","Trace"],ModuleName ["Control","Distributed","Process","Internal","Closure","BuiltIn"],ModuleName ["Control","Distributed","Process","Internal","Messaging"],ModuleName ["Control","Distributed","Process","Internal","StrictList"],ModuleName ["Control","Distributed","Process","Internal","StrictMVar"],ModuleName ["Control","Distributed","Process","Internal","WeakTQueue"],ModuleName ["Control","Distributed","Process","Internal","StrictContainerAccessors"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], hsLibraries = ["HSdistributed-process-0.4.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb",InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0",InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html/distributed-process.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html"]}),(InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15",InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-static", pkgVersion = Version {versionBranch = [0,2,1,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://www.github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Compositional, type-safe, polymorphic static values and closures", description = "/Towards Haskell in the Cloud/ (Epstein et al, Haskell\nSymposium 2011) introduces the concept of /static/ values:\nvalues that are known at compile time. In a distributed\nsetting where all nodes are running the same executable,\nstatic values can be serialized simply by transmitting a\ncode pointer to the value. This however requires special\ncompiler support, which is not yet available in ghc. We\ncan mimick the behaviour by keeping an explicit mapping\n('RemoteTable') from labels to values (and making sure\nthat all distributed nodes are using the same\n'RemoteTable'). In this module we implement this mimickry\nand various extensions: type safety (including for\npolymorphic static values) and compositionality.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Static"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], hsLibraries = ["HSdistributed-static-0.2.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html/distributed-static.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html"]}),(InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageInfo {installedPackageId = InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57", sourcePackageId = PackageIdentifier {pkgName = PackageName "filepath", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Neil Mitchell", stability = "", homepage = "http://www-users.cs.york.ac.uk/~ndm/filepath/", pkgUrl = "", synopsis = "Library for manipulating FilePaths in a cross platform way.", description = "", category = "System", exposed = True, exposedModules = [ModuleName ["System","FilePath"],ModuleName ["System","FilePath","Posix"],ModuleName ["System","FilePath","Windows"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], hsLibraries = ["HSfilepath-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0/filepath.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0"]}),(InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageInfo {installedPackageId = InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7", sourcePackageId = PackageIdentifier {pkgName = PackageName "ghc-prim", pkgVersion = Version {versionBranch = [0,2,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "GHC primitives", description = "GHC primitives.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Prim"],ModuleName ["GHC","Classes"],ModuleName ["GHC","CString"],ModuleName ["GHC","Debug"],ModuleName ["GHC","Generics"],ModuleName ["GHC","Magic"],ModuleName ["GHC","PrimopWrappers"],ModuleName ["GHC","IntWord64"],ModuleName ["GHC","Tuple"],ModuleName ["GHC","Types"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], hsLibraries = ["HSghc-prim-0.2.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0/ghc-prim.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0"]}),(InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c",InstalledPackageInfo {installedPackageId = InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c", sourcePackageId = PackageIdentifier {pkgName = PackageName "hashable", pkgVersion = Version {versionBranch = [1,1,2,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "johan.tibell@gmail.com", author = "Milan Straka \nJohan Tibell ", stability = "Provisional", homepage = "http://github.com/tibbe/hashable", pkgUrl = "", synopsis = "A class for types that can be converted to a hash value", description = "This package defines a class, 'Hashable', for types that\ncan be converted to a hash value. This class\nexists for the benefit of hashing-based data\nstructures. The package provides instances for\nbasic types and a way to combine hash values.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Hashable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], hsLibraries = ["HShashable-1.1.2.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html/hashable.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html"]}),(InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697",InstalledPackageInfo {installedPackageId = InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697", sourcePackageId = PackageIdentifier {pkgName = PackageName "haskell-src-exts", pkgVersion = Version {versionBranch = [1,13,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Niklas Broberg ", author = "Niklas Broberg", stability = "Stable", homepage = "http://code.haskell.org/haskell-src-exts", pkgUrl = "", synopsis = "Manipulating Haskell source: abstract syntax, lexer, parser, and pretty-printer", description = "Haskell-Source with Extensions (HSE, haskell-src-exts)\nis an extension of the standard haskell-src package,\nand handles most registered syntactic extensions to Haskell, including:\n\n* Multi-parameter type classes with functional dependencies\n\n* Indexed type families (including associated types)\n\n* Empty data declarations\n\n* GADTs\n\n* Implicit parameters\n\n* Template Haskell\n\nand a few more. All extensions implemented in GHC are supported.\nApart from these standard extensions,\nit also handles regular patterns as per the HaRP extension\nas well as HSX-style embedded XML syntax.", category = "Language", exposed = True, exposedModules = [ModuleName ["Language","Haskell","Exts"],ModuleName ["Language","Haskell","Exts","Lexer"],ModuleName ["Language","Haskell","Exts","Parser"],ModuleName ["Language","Haskell","Exts","Pretty"],ModuleName ["Language","Haskell","Exts","Syntax"],ModuleName ["Language","Haskell","Exts","Extension"],ModuleName ["Language","Haskell","Exts","Build"],ModuleName ["Language","Haskell","Exts","Fixity"],ModuleName ["Language","Haskell","Exts","Comments"],ModuleName ["Language","Haskell","Exts","SrcLoc"],ModuleName ["Language","Haskell","Exts","Annotated"],ModuleName ["Language","Haskell","Exts","Annotated","Syntax"],ModuleName ["Language","Haskell","Exts","Annotated","Fixity"],ModuleName ["Language","Haskell","Exts","Annotated","Build"],ModuleName ["Language","Haskell","Exts","Annotated","ExactPrint"],ModuleName ["Language","Haskell","Exts","Annotated","Simplify"]], hiddenModules = [ModuleName ["Language","Haskell","Exts","ExtScheme"],ModuleName ["Language","Haskell","Exts","ParseMonad"],ModuleName ["Language","Haskell","Exts","ParseSyntax"],ModuleName ["Language","Haskell","Exts","InternalLexer"],ModuleName ["Language","Haskell","Exts","ParseUtils"],ModuleName ["Language","Haskell","Exts","InternalParser"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], hsLibraries = ["HShaskell-src-exts-1.13.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html/haskell-src-exts.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html"]}),(InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageInfo {installedPackageId = InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43", sourcePackageId = PackageIdentifier {pkgName = PackageName "integer-gmp", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Integer library based on GMP", description = "This package contains an Integer library based on GMP.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Integer"],ModuleName ["GHC","Integer","GMP","Internals"],ModuleName ["GHC","Integer","GMP","Prim"],ModuleName ["GHC","Integer","Logarithms"],ModuleName ["GHC","Integer","Logarithms","Internals"]], hiddenModules = [ModuleName ["GHC","Integer","Type"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], hsLibraries = ["HSinteger-gmp-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0/integer-gmp.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0"]}),(InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",InstalledPackageInfo {installedPackageId = InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29", sourcePackageId = PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Edward Kmett ", author = "Andy Gill", stability = "", homepage = "http://github.com/ekmett/mtl", pkgUrl = "", synopsis = "Monad classes, using functional dependencies", description = "Monad classes using functional dependencies, with instances\nfor various monad transformers, inspired by the paper\n/Functional Programming with Overloading and Higher-Order Polymorphism/,\nby Mark P Jones, in /Advanced School of Functional Programming/, 1995\n().", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Monad","Cont"],ModuleName ["Control","Monad","Cont","Class"],ModuleName ["Control","Monad","Error"],ModuleName ["Control","Monad","Error","Class"],ModuleName ["Control","Monad","Identity"],ModuleName ["Control","Monad","List"],ModuleName ["Control","Monad","RWS"],ModuleName ["Control","Monad","RWS","Class"],ModuleName ["Control","Monad","RWS","Lazy"],ModuleName ["Control","Monad","RWS","Strict"],ModuleName ["Control","Monad","Reader"],ModuleName ["Control","Monad","Reader","Class"],ModuleName ["Control","Monad","State"],ModuleName ["Control","Monad","State","Class"],ModuleName ["Control","Monad","State","Lazy"],ModuleName ["Control","Monad","State","Strict"],ModuleName ["Control","Monad","Trans"],ModuleName ["Control","Monad","Writer"],ModuleName ["Control","Monad","Writer","Class"],ModuleName ["Control","Monad","Writer","Lazy"],ModuleName ["Control","Monad","Writer","Strict"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], hsLibraries = ["HSmtl-2.1.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html/mtl.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html"]}),(InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812",InstalledPackageInfo {installedPackageId = InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812", sourcePackageId = PackageIdentifier {pkgName = PackageName "network-transport", pkgVersion = Version {versionBranch = [0,3,0,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Network abstraction layer", description = "\"Network.Transport\" is a Network Abstraction Layer which provides\nthe following high-level concepts:\n\n* Nodes in the network are represented by 'EndPoint's. These are\nheavyweight stateful objects.\n\n* Each 'EndPoint' has an 'EndPointAddress'.\n\n* Connections can be established from one 'EndPoint' to another\nusing the 'EndPointAddress' of the remote end.\n\n* The 'EndPointAddress' can be serialised and sent over the\nnetwork, where as 'EndPoint's and connections cannot.\n\n* Connections between 'EndPoint's are unidirectional and lightweight.\n\n* Outgoing messages are sent via a 'Connection' object that\nrepresents the sending end of the connection.\n\n* Incoming messages for /all/ of the incoming connections on\nan 'EndPoint' are collected via a shared receive queue.\n\n* In addition to incoming messages, 'EndPoint's are notified of\nother 'Event's such as new connections or broken connections.\n\nThis design was heavily influenced by the design of the Common\nCommunication Interface\n().\nImportant design goals are:\n\n* Connections should be lightweight: it should be no problem to\ncreate thousands of connections between endpoints.\n\n* Error handling is explicit: every function declares as part of\nits type which errors it can return (no exceptions are thrown)\n\n* Error handling is \"abstract\": errors that originate from\nimplementation specific problems (such as \"no more sockets\" in\nthe TCP implementation) get mapped to generic errors\n(\"insufficient resources\") at the Transport level.\n\nThis package provides the generic interface only; you will\nprobably also want to install at least one transport\nimplementation (network-transport-*).", category = "Network", exposed = True, exposedModules = [ModuleName ["Network","Transport"],ModuleName ["Network","Transport","Util"],ModuleName ["Network","Transport","Internal"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], hsLibraries = ["HSnetwork-transport-0.3.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html/network-transport.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html"]}),(InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageInfo {installedPackageId = InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-locale", pkgVersion = Version {versionBranch = [1,0,0,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "locale library", description = "This package provides the old locale library.\nFor new code, the new locale library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Locale"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], hsLibraries = ["HSold-locale-1.0.0.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4/old-locale.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4"]}),(InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d",InstalledPackageInfo {installedPackageId = InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-time", pkgVersion = Version {versionBranch = [1,1,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Time library", description = "This package provides the old time library.\nFor new code, the new time library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Time"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], hsLibraries = ["HSold-time-1.1.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0/include"], includes = ["HsTime.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0/old-time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0"]}),(InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b",InstalledPackageInfo {installedPackageId = InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b", sourcePackageId = PackageIdentifier {pkgName = PackageName "pretty", pkgVersion = Version {versionBranch = [1,1,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "David Terei ", author = "", stability = "Stable", homepage = "http://github.com/haskell/pretty", pkgUrl = "", synopsis = "Pretty-printing library", description = "This package contains a pretty-printing library, a set of API's\nthat provides a way to easily print out text in a consistent\nformat of your choosing. This is useful for compilers and related\ntools.\n\nThis library was originally designed by John Hughes's and has since\nbeen heavily modified by Simon Peyton Jones.", category = "Text", exposed = True, exposedModules = [ModuleName ["Text","PrettyPrint"],ModuleName ["Text","PrettyPrint","HughesPJ"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], hsLibraries = ["HSpretty-1.1.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0/pretty.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0"]}),(InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4",InstalledPackageInfo {installedPackageId = InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4", sourcePackageId = PackageIdentifier {pkgName = PackageName "process", pkgVersion = Version {versionBranch = [1,1,0,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Process libraries", description = "This package contains libraries for dealing with system processes.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Process","Internals"],ModuleName ["System","Process"],ModuleName ["System","Cmd"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], hsLibraries = ["HSprocess-1.1.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1/include"], includes = ["runProcess.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1/process.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1"]}),(InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1",InstalledPackageInfo {installedPackageId = InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1", sourcePackageId = PackageIdentifier {pkgName = PackageName "random", pkgVersion = Version {versionBranch = [1,0,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "rrnewton@gmail.com", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "random number library", description = "This package provides a basic random number generation\nlibrary, including the ability to split random number\ngenerators.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Random"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], hsLibraries = ["HSrandom-1.0.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html/random.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html"]}),(InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0",InstalledPackageInfo {installedPackageId = InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0", sourcePackageId = PackageIdentifier {pkgName = PackageName "rank1dynamic", pkgVersion = Version {versionBranch = [0,1,0,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types", description = "\"Data.Typeable\" and \"Data.Dynamic\" only support monomorphic types.\nIn this package we provide similar functionality but with\nsupport for rank-1 polymorphic types.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Rank1Dynamic"],ModuleName ["Data","Rank1Typeable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], hsLibraries = ["HSrank1dynamic-0.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html/rank1dynamic.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html"]}),(InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",InstalledPackageInfo {installedPackageId = InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d", sourcePackageId = PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Software Transactional Memory", description = "A modular composable concurrency abstraction.\n\nChanges in version 2.4\n\n* Added \"Control.Concurrent.STM.TQueue\" (a faster @TChan@)\n\n* Added \"Control.Concurrent.STM.TBQueue\" (a bounded channel based on @TQueue@)\n\n* @TChan@ has an @Eq@ instances\n\n* Added @newBroadcastTChan@ and @newBroadcastTChanIO@\n\n* Some performance improvements for @TChan@\n\n* Added @cloneTChan@", category = "Concurrency", exposed = True, exposedModules = [ModuleName ["Control","Concurrent","STM"],ModuleName ["Control","Concurrent","STM","TArray"],ModuleName ["Control","Concurrent","STM","TVar"],ModuleName ["Control","Concurrent","STM","TChan"],ModuleName ["Control","Concurrent","STM","TMVar"],ModuleName ["Control","Concurrent","STM","TQueue"],ModuleName ["Control","Concurrent","STM","TBQueue"],ModuleName ["Control","Monad","STM"]], hiddenModules = [ModuleName ["Control","Sequential","STM"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], hsLibraries = ["HSstm-2.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html/stm.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html"]}),(InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageInfo {installedPackageId = InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24", sourcePackageId = PackageIdentifier {pkgName = PackageName "syb", pkgVersion = Version {versionBranch = [0,3,7], versionTags = []}}, license = BSD3, copyright = "", maintainer = "generics@haskell.org", author = "Ralf Lammel, Simon Peyton Jones, Jose Pedro Magalhaes", stability = "provisional", homepage = "http://www.cs.uu.nl/wiki/GenericProgramming/SYB", pkgUrl = "", synopsis = "Scrap Your Boilerplate", description = "This package contains the generics system described in the\n/Scrap Your Boilerplate/ papers (see\n).\nIt defines the @Data@ class of types permitting folding and unfolding\nof constructor applications, instances of this class for primitive\ntypes, and a variety of traversals.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics"],ModuleName ["Data","Generics","Basics"],ModuleName ["Data","Generics","Instances"],ModuleName ["Data","Generics","Aliases"],ModuleName ["Data","Generics","Schemes"],ModuleName ["Data","Generics","Text"],ModuleName ["Data","Generics","Twins"],ModuleName ["Data","Generics","Builders"],ModuleName ["Generics","SYB"],ModuleName ["Generics","SYB","Basics"],ModuleName ["Generics","SYB","Instances"],ModuleName ["Generics","SYB","Aliases"],ModuleName ["Generics","SYB","Schemes"],ModuleName ["Generics","SYB","Text"],ModuleName ["Generics","SYB","Twins"],ModuleName ["Generics","SYB","Builders"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], hsLibraries = ["HSsyb-0.3.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html/syb.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html"]}),(InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageInfo {installedPackageId = InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949", sourcePackageId = PackageIdentifier {pkgName = PackageName "template-haskell", pkgVersion = Version {versionBranch = [2,7,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "Facilities for manipulating Haskell source code using Template Haskell.", category = "", exposed = True, exposedModules = [ModuleName ["Language","Haskell","TH","Syntax","Internals"],ModuleName ["Language","Haskell","TH","Syntax"],ModuleName ["Language","Haskell","TH","PprLib"],ModuleName ["Language","Haskell","TH","Ppr"],ModuleName ["Language","Haskell","TH","Lib"],ModuleName ["Language","Haskell","TH","Quote"],ModuleName ["Language","Haskell","TH"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], hsLibraries = ["HStemplate-haskell-2.7.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0/template-haskell.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0"]}),(InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c",InstalledPackageInfo {installedPackageId = InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c", sourcePackageId = PackageIdentifier {pkgName = PackageName "text", pkgVersion = Version {versionBranch = [0,11,2,3], versionTags = []}}, license = BSD3, copyright = "2009-2011 Bryan O'Sullivan, 2008-2009 Tom Harper", maintainer = "Bryan O'Sullivan ", author = "Bryan O'Sullivan ", stability = "", homepage = "https://github.com/bos/text", pkgUrl = "", synopsis = "An efficient packed Unicode text type.", description = "\nAn efficient packed, immutable Unicode text type (both strict and\nlazy), with a powerful loop fusion optimization framework.\n\nThe 'Text' type represents Unicode character strings, in a time and\nspace-efficient manner. This package provides text processing\ncapabilities that are optimized for performance critical use, both\nin terms of large data quantities and high speed.\n\nThe 'Text' type provides character-encoding, type-safe case\nconversion via whole-string case conversion functions. It also\nprovides a range of functions for converting 'Text' values to and from\n'ByteStrings', using several standard encodings.\n\nEfficient locale-sensitive support for text IO is also supported.\n\nThese modules are intended to be imported qualified, to avoid name\nclashes with Prelude functions, e.g.\n\n> import qualified Data.Text as T\n\nTo use an extended and very rich family of functions for working\nwith Unicode text (including normalization, regular expressions,\nnon-standard encodings, text breaking, and locales), see\nthe @text-icu@ package:\n\n\n—— RELEASE NOTES ——\n\nChanges in 0.11.2.0:\n\n* String literals are now converted directly from the format in\nwhich GHC stores them into 'Text', without an intermediate\ntransformation through 'String', and without inlining of\nconversion code at each site where a string literal is declared.\n", category = "Data, Text", exposed = True, exposedModules = [ModuleName ["Data","Text"],ModuleName ["Data","Text","Array"],ModuleName ["Data","Text","Encoding"],ModuleName ["Data","Text","Encoding","Error"],ModuleName ["Data","Text","Foreign"],ModuleName ["Data","Text","IO"],ModuleName ["Data","Text","Internal"],ModuleName ["Data","Text","Lazy"],ModuleName ["Data","Text","Lazy","Builder"],ModuleName ["Data","Text","Lazy","Builder","Int"],ModuleName ["Data","Text","Lazy","Builder","RealFloat"],ModuleName ["Data","Text","Lazy","Encoding"],ModuleName ["Data","Text","Lazy","IO"],ModuleName ["Data","Text","Lazy","Internal"],ModuleName ["Data","Text","Lazy","Read"],ModuleName ["Data","Text","Read"]], hiddenModules = [ModuleName ["Data","Text","Encoding","Fusion"],ModuleName ["Data","Text","Encoding","Fusion","Common"],ModuleName ["Data","Text","Encoding","Utf16"],ModuleName ["Data","Text","Encoding","Utf32"],ModuleName ["Data","Text","Encoding","Utf8"],ModuleName ["Data","Text","Fusion"],ModuleName ["Data","Text","Fusion","CaseMapping"],ModuleName ["Data","Text","Fusion","Common"],ModuleName ["Data","Text","Fusion","Internal"],ModuleName ["Data","Text","Fusion","Size"],ModuleName ["Data","Text","IO","Internal"],ModuleName ["Data","Text","Lazy","Builder","Functions"],ModuleName ["Data","Text","Lazy","Builder","RealFloat","Functions"],ModuleName ["Data","Text","Lazy","Encoding","Fusion"],ModuleName ["Data","Text","Lazy","Fusion"],ModuleName ["Data","Text","Lazy","Search"],ModuleName ["Data","Text","Private"],ModuleName ["Data","Text","Search"],ModuleName ["Data","Text","Unsafe"],ModuleName ["Data","Text","Unsafe","Base"],ModuleName ["Data","Text","UnsafeChar"],ModuleName ["Data","Text","UnsafeShift"],ModuleName ["Data","Text","Util"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], hsLibraries = ["HStext-0.11.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html/text.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html"]}),(InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f",InstalledPackageInfo {installedPackageId = InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f", sourcePackageId = PackageIdentifier {pkgName = PackageName "time", pkgVersion = Version {versionBranch = [1,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Ashley Yakeley", stability = "stable", homepage = "http://semantic.org/TimeLib/", pkgUrl = "", synopsis = "A time library", description = "A time library", category = "System", exposed = True, exposedModules = [ModuleName ["Data","Time","Calendar"],ModuleName ["Data","Time","Calendar","MonthDay"],ModuleName ["Data","Time","Calendar","OrdinalDate"],ModuleName ["Data","Time","Calendar","WeekDate"],ModuleName ["Data","Time","Calendar","Julian"],ModuleName ["Data","Time","Calendar","Easter"],ModuleName ["Data","Time","Clock"],ModuleName ["Data","Time","Clock","POSIX"],ModuleName ["Data","Time","Clock","TAI"],ModuleName ["Data","Time","LocalTime"],ModuleName ["Data","Time","Format"],ModuleName ["Data","Time"]], hiddenModules = [ModuleName ["Data","Time","Calendar","Private"],ModuleName ["Data","Time","Calendar","Days"],ModuleName ["Data","Time","Calendar","Gregorian"],ModuleName ["Data","Time","Calendar","JulianYearDay"],ModuleName ["Data","Time","Clock","Scale"],ModuleName ["Data","Time","Clock","UTC"],ModuleName ["Data","Time","Clock","CTimeval"],ModuleName ["Data","Time","Clock","UTCDiff"],ModuleName ["Data","Time","LocalTime","TimeZone"],ModuleName ["Data","Time","LocalTime","TimeOfDay"],ModuleName ["Data","Time","LocalTime","LocalTime"],ModuleName ["Data","Time","Format","Parse"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], hsLibraries = ["HStime-1.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4/include"], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4/time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4"]}),(InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",InstalledPackageInfo {installedPackageId = InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4", sourcePackageId = PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Ross Paterson ", author = "Andy Gill, Ross Paterson", stability = "", homepage = "", pkgUrl = "", synopsis = "Concrete functor and monad transformers", description = "A portable library of functor and monad transformers, inspired by\nthe paper \\\"Functional Programming with Overloading and Higher-Order\nPolymorphism\\\", by Mark P Jones,\nin /Advanced School of Functional Programming/, 1995\n().\n\nThis package contains:\n\n* the monad transformer class (in \"Control.Monad.Trans.Class\")\n\n* concrete functor and monad transformers, each with associated\noperations and functions to lift operations associated with other\ntransformers.\n\nIt can be used on its own in portable Haskell code, or with the monad\nclasses in the @mtl@ or @monads-tf@ packages, which automatically\nlift operations introduced by monad transformers through other\ntransformers.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Applicative","Backwards"],ModuleName ["Control","Applicative","Lift"],ModuleName ["Control","Monad","IO","Class"],ModuleName ["Control","Monad","Trans","Class"],ModuleName ["Control","Monad","Trans","Cont"],ModuleName ["Control","Monad","Trans","Error"],ModuleName ["Control","Monad","Trans","Identity"],ModuleName ["Control","Monad","Trans","List"],ModuleName ["Control","Monad","Trans","Maybe"],ModuleName ["Control","Monad","Trans","Reader"],ModuleName ["Control","Monad","Trans","RWS"],ModuleName ["Control","Monad","Trans","RWS","Lazy"],ModuleName ["Control","Monad","Trans","RWS","Strict"],ModuleName ["Control","Monad","Trans","State"],ModuleName ["Control","Monad","Trans","State","Lazy"],ModuleName ["Control","Monad","Trans","State","Strict"],ModuleName ["Control","Monad","Trans","Writer"],ModuleName ["Control","Monad","Trans","Writer","Lazy"],ModuleName ["Control","Monad","Trans","Writer","Strict"],ModuleName ["Data","Functor","Compose"],ModuleName ["Data","Functor","Constant"],ModuleName ["Data","Functor","Identity"],ModuleName ["Data","Functor","Product"],ModuleName ["Data","Functor","Reverse"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], hsLibraries = ["HStransformers-0.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html/transformers.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html"]}),(InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd",InstalledPackageInfo {installedPackageId = InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd", sourcePackageId = PackageIdentifier {pkgName = PackageName "uniplate", pkgVersion = Version {versionBranch = [1,6,7], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/uniplate/", pkgUrl = "", synopsis = "Help writing simple, concise and fast generic operations.", description = "Uniplate is library for writing simple and concise generic operations.\nUniplate has similar goals to the original Scrap Your Boilerplate work,\nbut is substantially simpler and faster. The Uniplate manual is available at\n.\n\nTo get started with Uniplate you should import one of the three following\nmodules:\n\n* \"Data.Generics.Uniplate.Data\" - to quickly start writing generic functions.\nMost users should start by importing this module.\n\n* \"Data.Generics.Uniplate.Direct\" - a replacement for \"Data.Generics.Uniplate.Data\"\nwith substantially higher performance (around 5 times), but requires writing\ninstance declarations.\n\n* \"Data.Generics.Uniplate.Operations\" - definitions of all the operations defined\nby Uniplate. Both the above two modules re-export this module.\n\nIn addition, some users may want to make use of the following modules:\n\n* \"Data.Generics.Uniplate.Zipper\" - a zipper built on top of Uniplate instances.\n\n* \"Data.Generics.SYB\" - users transitioning from the Scrap Your Boilerplate library.\n\n* \"Data.Generics.Compos\" - users transitioning from the Compos library.\n\n* \"Data.Generics.Uniplate.DataOnly\" - users making use of both @Data@ and @Direct@\nto avoid getting instance conflicts.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics","Str"],ModuleName ["Data","Generics","Compos"],ModuleName ["Data","Generics","SYB"],ModuleName ["Data","Generics","Uniplate","Data"],ModuleName ["Data","Generics","Uniplate","Data","Instances"],ModuleName ["Data","Generics","Uniplate","DataOnly"],ModuleName ["Data","Generics","Uniplate","Direct"],ModuleName ["Data","Generics","Uniplate","Operations"],ModuleName ["Data","Generics","Uniplate","Typeable"],ModuleName ["Data","Generics","Uniplate","Zipper"],ModuleName ["Data","Generics","Uniplate"],ModuleName ["Data","Generics","UniplateOn"],ModuleName ["Data","Generics","UniplateStr"],ModuleName ["Data","Generics","UniplateStrOn"],ModuleName ["Data","Generics","Biplate"],ModuleName ["Data","Generics","PlateDirect"],ModuleName ["Data","Generics","PlateTypeable"],ModuleName ["Data","Generics","PlateData"]], hiddenModules = [ModuleName ["Data","Generics","PlateInternal"],ModuleName ["Data","Generics","Uniplate","Internal","Data"],ModuleName ["Data","Generics","Uniplate","Internal","DataOnlyOperations"],ModuleName ["Data","Generics","Uniplate","Internal","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], hsLibraries = ["HSuniplate-1.6.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html/uniplate.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html"]}),(InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e",InstalledPackageInfo {installedPackageId = InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e", sourcePackageId = PackageIdentifier {pkgName = PackageName "unix", pkgVersion = Version {versionBranch = [2,5,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "POSIX functionality", description = "This package gives you access to the set of operating system\nservices standardised by POSIX 1003.1b (or the IEEE Portable\nOperating System Interface for Computing Environments -\nIEEE Std. 1003.1).\n\nThe package is not supported under Windows (except under Cygwin).", category = "System", exposed = True, exposedModules = [ModuleName ["System","Posix"],ModuleName ["System","Posix","ByteString"],ModuleName ["System","Posix","Error"],ModuleName ["System","Posix","Resource"],ModuleName ["System","Posix","Time"],ModuleName ["System","Posix","Unistd"],ModuleName ["System","Posix","User"],ModuleName ["System","Posix","Signals"],ModuleName ["System","Posix","Signals","Exts"],ModuleName ["System","Posix","Semaphore"],ModuleName ["System","Posix","SharedMem"],ModuleName ["System","Posix","ByteString","FilePath"],ModuleName ["System","Posix","Directory"],ModuleName ["System","Posix","Directory","ByteString"],ModuleName ["System","Posix","DynamicLinker","Module"],ModuleName ["System","Posix","DynamicLinker","Module","ByteString"],ModuleName ["System","Posix","DynamicLinker","Prim"],ModuleName ["System","Posix","DynamicLinker","ByteString"],ModuleName ["System","Posix","DynamicLinker"],ModuleName ["System","Posix","Files"],ModuleName ["System","Posix","Files","ByteString"],ModuleName ["System","Posix","IO"],ModuleName ["System","Posix","IO","ByteString"],ModuleName ["System","Posix","Env"],ModuleName ["System","Posix","Env","ByteString"],ModuleName ["System","Posix","Process"],ModuleName ["System","Posix","Process","Internals"],ModuleName ["System","Posix","Process","ByteString"],ModuleName ["System","Posix","Temp"],ModuleName ["System","Posix","Temp","ByteString"],ModuleName ["System","Posix","Terminal"],ModuleName ["System","Posix","Terminal","ByteString"]], hiddenModules = [ModuleName ["System","Posix","Directory","Common"],ModuleName ["System","Posix","DynamicLinker","Common"],ModuleName ["System","Posix","Files","Common"],ModuleName ["System","Posix","IO","Common"],ModuleName ["System","Posix","Process","Common"],ModuleName ["System","Posix","Terminal","Common"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], hsLibraries = ["HSunix-2.5.1.1"], extraLibraries = ["dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1/include"], includes = ["HsUnix.h","execvpe.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1/unix.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1"]}),(InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d",InstalledPackageInfo {installedPackageId = InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d", sourcePackageId = PackageIdentifier {pkgName = PackageName "unordered-containers", pkgVersion = Version {versionBranch = [0,2,2,1], versionTags = []}}, license = BSD3, copyright = "2010-2012 Johan Tibell\n2010 Edward Z. Yang", maintainer = "johan.tibell@gmail.com", author = "Johan Tibell", stability = "", homepage = "https://github.com/tibbe/unordered-containers", pkgUrl = "", synopsis = "Efficient hashing-based container types", description = "Efficient hashing-based container types. The containers have been\noptimized for performance critical use, both in terms of large data\nquantities and high speed.\n\nThe declared cost of each operation is either worst-case or\namortized, but remains valid even if structures are shared.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","HashMap","Lazy"],ModuleName ["Data","HashMap","Strict"],ModuleName ["Data","HashSet"]], hiddenModules = [ModuleName ["Data","HashMap","Array"],ModuleName ["Data","HashMap","Base"],ModuleName ["Data","HashMap","PopCount"],ModuleName ["Data","HashMap","Unsafe"],ModuleName ["Data","HashMap","UnsafeShift"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], hsLibraries = ["HSunordered-containers-0.2.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html/unordered-containers.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html"]})]) (fromList [(PackageName "array",fromList [(Version {versionBranch = [0,4,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297", sourcePackageId = PackageIdentifier {pkgName = PackageName "array", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Mutable and immutable arrays", description = "This package defines the classes @IArray@ of immutable arrays and\n@MArray@ of arrays mutable within appropriate monads, as well as\nsome instances of these classes.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Array","Base"],ModuleName ["Data","Array","IArray"],ModuleName ["Data","Array","IO"],ModuleName ["Data","Array","IO","Safe"],ModuleName ["Data","Array","IO","Internals"],ModuleName ["Data","Array","MArray"],ModuleName ["Data","Array","MArray","Safe"],ModuleName ["Data","Array","ST"],ModuleName ["Data","Array","ST","Safe"],ModuleName ["Data","Array","Storable"],ModuleName ["Data","Array","Storable","Safe"],ModuleName ["Data","Array","Storable","Internals"],ModuleName ["Data","Array","Unboxed"],ModuleName ["Data","Array","Unsafe"],ModuleName ["Data","Array"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/array-0.4.0.0"], hsLibraries = ["HSarray-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0/array.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/array-0.4.0.0"]}])]),(PackageName "base",fromList [(Version {versionBranch = [4,5,1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd", sourcePackageId = PackageIdentifier {pkgName = PackageName "base", pkgVersion = Version {versionBranch = [4,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Basic libraries", description = "This package contains the Prelude and its support libraries,\nand a large collection of useful libraries ranging from data\nstructures to parsing combinators and debugging utilities.", category = "", exposed = True, exposedModules = [ModuleName ["Foreign","Concurrent"],ModuleName ["GHC","Arr"],ModuleName ["GHC","Base"],ModuleName ["GHC","Conc"],ModuleName ["GHC","Conc","IO"],ModuleName ["GHC","Conc","Signal"],ModuleName ["GHC","Conc","Sync"],ModuleName ["GHC","ConsoleHandler"],ModuleName ["GHC","Constants"],ModuleName ["GHC","Desugar"],ModuleName ["GHC","Enum"],ModuleName ["GHC","Environment"],ModuleName ["GHC","Err"],ModuleName ["GHC","Exception"],ModuleName ["GHC","Exts"],ModuleName ["GHC","Fingerprint"],ModuleName ["GHC","Fingerprint","Type"],ModuleName ["GHC","Float"],ModuleName ["GHC","Float","ConversionUtils"],ModuleName ["GHC","Float","RealFracMethods"],ModuleName ["GHC","Foreign"],ModuleName ["GHC","ForeignPtr"],ModuleName ["GHC","Handle"],ModuleName ["GHC","IO"],ModuleName ["GHC","IO","Buffer"],ModuleName ["GHC","IO","BufferedIO"],ModuleName ["GHC","IO","Device"],ModuleName ["GHC","IO","Encoding"],ModuleName ["GHC","IO","Encoding","CodePage"],ModuleName ["GHC","IO","Encoding","Failure"],ModuleName ["GHC","IO","Encoding","Iconv"],ModuleName ["GHC","IO","Encoding","Latin1"],ModuleName ["GHC","IO","Encoding","Types"],ModuleName ["GHC","IO","Encoding","UTF16"],ModuleName ["GHC","IO","Encoding","UTF32"],ModuleName ["GHC","IO","Encoding","UTF8"],ModuleName ["GHC","IO","Exception"],ModuleName ["GHC","IO","FD"],ModuleName ["GHC","IO","Handle"],ModuleName ["GHC","IO","Handle","FD"],ModuleName ["GHC","IO","Handle","Internals"],ModuleName ["GHC","IO","Handle","Text"],ModuleName ["GHC","IO","Handle","Types"],ModuleName ["GHC","IO","IOMode"],ModuleName ["GHC","IOArray"],ModuleName ["GHC","IOBase"],ModuleName ["GHC","IORef"],ModuleName ["GHC","Int"],ModuleName ["GHC","List"],ModuleName ["GHC","MVar"],ModuleName ["GHC","Num"],ModuleName ["GHC","PArr"],ModuleName ["GHC","Pack"],ModuleName ["GHC","Ptr"],ModuleName ["GHC","Read"],ModuleName ["GHC","Real"],ModuleName ["GHC","ST"],ModuleName ["GHC","Stack"],ModuleName ["GHC","Stats"],ModuleName ["GHC","Show"],ModuleName ["GHC","Stable"],ModuleName ["GHC","Storable"],ModuleName ["GHC","STRef"],ModuleName ["GHC","TopHandler"],ModuleName ["GHC","Unicode"],ModuleName ["GHC","Weak"],ModuleName ["GHC","Word"],ModuleName ["System","Timeout"],ModuleName ["GHC","Event"],ModuleName ["Control","Applicative"],ModuleName ["Control","Arrow"],ModuleName ["Control","Category"],ModuleName ["Control","Concurrent"],ModuleName ["Control","Concurrent","Chan"],ModuleName ["Control","Concurrent","MVar"],ModuleName ["Control","Concurrent","QSem"],ModuleName ["Control","Concurrent","QSemN"],ModuleName ["Control","Concurrent","SampleVar"],ModuleName ["Control","Exception"],ModuleName ["Control","Exception","Base"],ModuleName ["Control","OldException"],ModuleName ["Control","Monad"],ModuleName ["Control","Monad","Fix"],ModuleName ["Control","Monad","Instances"],ModuleName ["Control","Monad","ST"],ModuleName ["Control","Monad","ST","Safe"],ModuleName ["Control","Monad","ST","Unsafe"],ModuleName ["Control","Monad","ST","Lazy"],ModuleName ["Control","Monad","ST","Lazy","Safe"],ModuleName ["Control","Monad","ST","Lazy","Unsafe"],ModuleName ["Control","Monad","ST","Strict"],ModuleName ["Control","Monad","Zip"],ModuleName ["Data","Bits"],ModuleName ["Data","Bool"],ModuleName ["Data","Char"],ModuleName ["Data","Complex"],ModuleName ["Data","Dynamic"],ModuleName ["Data","Either"],ModuleName ["Data","Eq"],ModuleName ["Data","Data"],ModuleName ["Data","Fixed"],ModuleName ["Data","Foldable"],ModuleName ["Data","Function"],ModuleName ["Data","Functor"],ModuleName ["Data","HashTable"],ModuleName ["Data","IORef"],ModuleName ["Data","Int"],ModuleName ["Data","Ix"],ModuleName ["Data","List"],ModuleName ["Data","Maybe"],ModuleName ["Data","Monoid"],ModuleName ["Data","Ord"],ModuleName ["Data","Ratio"],ModuleName ["Data","STRef"],ModuleName ["Data","STRef","Lazy"],ModuleName ["Data","STRef","Strict"],ModuleName ["Data","String"],ModuleName ["Data","Traversable"],ModuleName ["Data","Tuple"],ModuleName ["Data","Typeable"],ModuleName ["Data","Typeable","Internal"],ModuleName ["Data","Unique"],ModuleName ["Data","Version"],ModuleName ["Data","Word"],ModuleName ["Debug","Trace"],ModuleName ["Foreign"],ModuleName ["Foreign","C"],ModuleName ["Foreign","C","Error"],ModuleName ["Foreign","C","String"],ModuleName ["Foreign","C","Types"],ModuleName ["Foreign","ForeignPtr"],ModuleName ["Foreign","ForeignPtr","Safe"],ModuleName ["Foreign","ForeignPtr","Unsafe"],ModuleName ["Foreign","Marshal"],ModuleName ["Foreign","Marshal","Alloc"],ModuleName ["Foreign","Marshal","Array"],ModuleName ["Foreign","Marshal","Error"],ModuleName ["Foreign","Marshal","Pool"],ModuleName ["Foreign","Marshal","Safe"],ModuleName ["Foreign","Marshal","Utils"],ModuleName ["Foreign","Marshal","Unsafe"],ModuleName ["Foreign","Ptr"],ModuleName ["Foreign","Safe"],ModuleName ["Foreign","StablePtr"],ModuleName ["Foreign","Storable"],ModuleName ["Numeric"],ModuleName ["Prelude"],ModuleName ["System","Console","GetOpt"],ModuleName ["System","CPUTime"],ModuleName ["System","Environment"],ModuleName ["System","Exit"],ModuleName ["System","IO"],ModuleName ["System","IO","Error"],ModuleName ["System","IO","Unsafe"],ModuleName ["System","Info"],ModuleName ["System","Mem"],ModuleName ["System","Mem","StableName"],ModuleName ["System","Mem","Weak"],ModuleName ["System","Posix","Internals"],ModuleName ["System","Posix","Types"],ModuleName ["Text","ParserCombinators","ReadP"],ModuleName ["Text","ParserCombinators","ReadPrec"],ModuleName ["Text","Printf"],ModuleName ["Text","Read"],ModuleName ["Text","Read","Lex"],ModuleName ["Text","Show"],ModuleName ["Text","Show","Functions"],ModuleName ["Unsafe","Coerce"]], hiddenModules = [ModuleName ["GHC","Event","Array"],ModuleName ["GHC","Event","Clock"],ModuleName ["GHC","Event","Control"],ModuleName ["GHC","Event","EPoll"],ModuleName ["GHC","Event","IntMap"],ModuleName ["GHC","Event","Internal"],ModuleName ["GHC","Event","KQueue"],ModuleName ["GHC","Event","Manager"],ModuleName ["GHC","Event","PSQ"],ModuleName ["GHC","Event","Poll"],ModuleName ["GHC","Event","Thread"],ModuleName ["GHC","Event","Unique"],ModuleName ["Control","Monad","ST","Imp"],ModuleName ["Control","Monad","ST","Lazy","Imp"],ModuleName ["Foreign","ForeignPtr","Imp"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0"], hsLibraries = ["HSbase-4.5.1.0"], extraLibraries = ["iconv"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/base-4.5.1.0/include"], includes = ["HsBase.h"], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0/base.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/base-4.5.1.0"]}])]),(PackageName "binary",fromList [(Version {versionBranch = [0,5,1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b", sourcePackageId = PackageIdentifier {pkgName = PackageName "binary", pkgVersion = Version {versionBranch = [0,5,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Lennart Kolmodin, Don Stewart ", author = "Lennart Kolmodin ", stability = "provisional", homepage = "http://code.haskell.org/binary/", pkgUrl = "", synopsis = "Binary serialisation for Haskell values using lazy ByteStrings", description = "Efficient, pure binary serialisation using lazy ByteStrings.\nHaskell values may be encoded to and from binary formats,\nwritten to disk as binary, or sent over the network.\nSerialisation speeds of over 1 G\\/sec have been observed,\nso this library should be suitable for high performance\nscenarios.", category = "Data, Parsing", exposed = True, exposedModules = [ModuleName ["Data","Binary"],ModuleName ["Data","Binary","Put"],ModuleName ["Data","Binary","Get"],ModuleName ["Data","Binary","Builder"],ModuleName ["Data","Binary","Builder","Internal"]], hiddenModules = [ModuleName ["Data","Binary","Builder","Base"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/binary-0.5.1.0"], hsLibraries = ["HSbinary-0.5.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0/binary.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/binary-0.5.1.0"]}])]),(PackageName "bytestring",fromList [(Version {versionBranch = [0,9,2,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065", sourcePackageId = PackageIdentifier {pkgName = PackageName "bytestring", pkgVersion = Version {versionBranch = [0,9,2,1], versionTags = []}}, license = BSD3, copyright = "Copyright (c) Don Stewart 2005-2009,\n(c) Duncan Coutts 2006-2009,\n(c) David Roundy 2003-2005.", maintainer = "dons00@gmail.com, duncan@community.haskell.org", author = "Don Stewart, Duncan Coutts", stability = "", homepage = "http://www.cse.unsw.edu.au/~dons/fps.html", pkgUrl = "", synopsis = "Fast, packed, strict and lazy byte arrays with a list interface", description = "A time and space-efficient implementation of byte vectors using\npacked Word8 arrays, suitable for high performance use, both in terms\nof large data quantities, or high speed requirements. Byte vectors\nare encoded as strict 'Word8' arrays of bytes, and lazy lists of\nstrict chunks, held in a 'ForeignPtr', and can be passed between C\nand Haskell with little effort.\n\nTest coverage data for this library is available at:\n", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","ByteString"],ModuleName ["Data","ByteString","Char8"],ModuleName ["Data","ByteString","Unsafe"],ModuleName ["Data","ByteString","Internal"],ModuleName ["Data","ByteString","Lazy"],ModuleName ["Data","ByteString","Lazy","Char8"],ModuleName ["Data","ByteString","Lazy","Internal"],ModuleName ["Data","ByteString","Fusion"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1"], hsLibraries = ["HSbytestring-0.9.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/bytestring-0.9.2.1/include"], includes = ["fpstring.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1/bytestring.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/bytestring-0.9.2.1"]}])]),(PackageName "containers",fromList [(Version {versionBranch = [0,4,2,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce", sourcePackageId = PackageIdentifier {pkgName = PackageName "containers", pkgVersion = Version {versionBranch = [0,4,2,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "fox@ucw.cz", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Assorted concrete container types", description = "This package contains efficient general-purpose implementations\nof various basic immutable container types. The declared cost of\neach operation is either worst-case or amortized, but remains\nvalid even if structures are shared.", category = "Data Structures", exposed = True, exposedModules = [ModuleName ["Data","Graph"],ModuleName ["Data","Sequence"],ModuleName ["Data","Tree"],ModuleName ["Data","IntMap"],ModuleName ["Data","IntSet"],ModuleName ["Data","Map"],ModuleName ["Data","Set"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/containers-0.4.2.1"], hsLibraries = ["HScontainers-0.4.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1/containers.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/containers-0.4.2.1"]}])]),(PackageName "cpphs",fromList [(Version {versionBranch = [1,14], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "cpphs", pkgVersion = Version {versionBranch = [1,14], versionTags = []}}, license = LGPL Nothing, copyright = "2004-2012, Malcolm Wallace", maintainer = "Malcolm Wallace ", author = "Malcolm Wallace ", stability = "", homepage = "http://haskell.org/cpphs/", pkgUrl = "", synopsis = "A liberalised re-implementation of cpp, the C pre-processor.", description = "Cpphs is a re-implementation of the C pre-processor that is both\nmore compatible with Haskell, and itself written in Haskell so\nthat it can be distributed with compilers.\n\nThis version of the C pre-processor is pretty-much\nfeature-complete and compatible with traditional (K&R)\npre-processors. Additional features include: a plain-text mode;\nan option to unlit literate code files; and an option to turn\noff macro-expansion.", category = "Development", exposed = True, exposedModules = [ModuleName ["Language","Preprocessor","Cpphs"],ModuleName ["Language","Preprocessor","Unlit"]], hiddenModules = [ModuleName ["Language","Preprocessor","Cpphs","CppIfdef"],ModuleName ["Language","Preprocessor","Cpphs","HashDefine"],ModuleName ["Language","Preprocessor","Cpphs","MacroPass"],ModuleName ["Language","Preprocessor","Cpphs","Options"],ModuleName ["Language","Preprocessor","Cpphs","Position"],ModuleName ["Language","Preprocessor","Cpphs","ReadFirst"],ModuleName ["Language","Preprocessor","Cpphs","RunCpphs"],ModuleName ["Language","Preprocessor","Cpphs","SymTab"],ModuleName ["Language","Preprocessor","Cpphs","Tokenise"],ModuleName ["Text","ParserCombinators","HuttonMeijer"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/lib"], hsLibraries = ["HScpphs-1.14"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html/cpphs.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/cpphs-1.14/doc/html"]}])]),(PackageName "data-accessor",fromList [(Version {versionBranch = [0,2,2,3], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb", sourcePackageId = PackageIdentifier {pkgName = PackageName "data-accessor", pkgVersion = Version {versionBranch = [0,2,2,3], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Henning Thielemann ", author = "Henning Thielemann , Luke Palmer ", stability = "", homepage = "http://www.haskell.org/haskellwiki/Record_access", pkgUrl = "", synopsis = "Utilities for accessing and manipulating fields of records", description = "In Haskell 98 the name of a record field\nis automatically also the name of a function which gets the value\nof the according field.\nE.g. if we have\n\ndata Pair a b = Pair\nfirst :: a, second :: b\n\nthen\n\n> first :: Pair a b -> a\n> second :: Pair a b -> b\n\nHowever for setting or modifying a field value\nwe need to use some syntactic sugar, which is often clumsy.\n\nmodifyFirst :: (a -> a) -> (Pair a b -> Pair a b)\nmodifyFirst f r\\@(Pair\nfirst=a\n) = r\nfirst = f a\n\nWith this package you can define record field accessors\nwhich allow setting, getting and modifying values easily.\nThe package clearly demonstrates the power of the functional approach:\nYou can combine accessors of a record and sub-records,\nto make the access look like the fields of the sub-record belong to the main record.\n\nExample:\n\n> *Data.Accessor.Example> (first^:second^=10) (('b',7),\"hallo\")\n> (('b',10),\"hallo\")\n\nYou can easily manipulate record fields in a 'Control.Monad.State.State' monad,\nyou can easily code 'Show' instances that use the Accessor syntax\nand you can parse binary streams into records.\nSee @Data.Accessor.Example@ for demonstration of all features.\n\nIt would be great if in revised Haskell versions the names of record fields\nare automatically 'Data.Accessor.Accessor's\nrather than plain @get@ functions.\nFor now, the package @data-accessor-template@ provides Template Haskell functions\nfor automated generation of 'Data.Acesssor.Accessor's.\nSee also the other @data-accessor@ packages\nthat provide an Accessor interface to other data types.\nThe package @enumset@ provides accessors to bit-packed records.\n\nFor similar packages see @lenses@ and @fclabel@.\nA related concept are editors\n.\nEditors only consist of a modify method\n(and @modify@ applied to a 'const' function is a @set@ function).\nThis way, they can modify all function values of a function at once,\nwhereas an accessor can only change a single function value,\nsay, it can change @f 0 = 1@ to @f 0 = 2@.\nThis way, editors can even change the type of a record or a function.\nAn Arrow instance can be defined for editors,\nbut for accessors only a Category instance is possible ('(.)' method).\nThe reason is the @arr@ method of the @Arrow@ class,\nthat conflicts with the two-way nature (set and get) of accessors.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Accessor"],ModuleName ["Data","Accessor","Basic"],ModuleName ["Data","Accessor","Container"],ModuleName ["Data","Accessor","Show"],ModuleName ["Data","Accessor","Tuple"],ModuleName ["Data","Accessor","BinaryRead"],ModuleName ["Data","Accessor","MonadState"]], hiddenModules = [ModuleName ["Data","Accessor","Example"],ModuleName ["Data","Accessor","Private"],ModuleName ["Data","Accessor","MonadStatePrivate"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/lib"], hsLibraries = ["HSdata-accessor-0.2.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html/data-accessor.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/data-accessor-0.2.2.3/doc/html"]}])]),(PackageName "deepseq",fromList [(Version {versionBranch = [1,3,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6", sourcePackageId = PackageIdentifier {pkgName = PackageName "deepseq", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Deep evaluation of data structures", description = "This package provides methods for fully evaluating data structures\n(\\\"deep evaluation\\\"). Deep evaluation is often used for adding\nstrictness to a program, e.g. in order to force pending exceptions,\nremove space leaks, or force lazy I/O to happen. It is also useful\nin parallel programs, to ensure pending work does not migrate to the\nwrong thread.\n\nThe primary use of this package is via the 'deepseq' function, a\n\\\"deep\\\" version of 'seq'. It is implemented on top of an 'NFData'\ntypeclass (\\\"Normal Form Data\\\", data structures with no unevaluated\ncomponents) which defines strategies for fully evaluating different\ndata types.\n", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","DeepSeq"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/deepseq-1.3.0.0"], hsLibraries = ["HSdeepseq-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0/deepseq.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/deepseq-1.3.0.0"]}])]),(PackageName "derive",fromList [(Version {versionBranch = [2,5,11], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "derive-2.5.11-1af1fcbad163895b9f6c0cc7cfe027c4", sourcePackageId = PackageIdentifier {pkgName = PackageName "derive", pkgVersion = Version {versionBranch = [2,5,11], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/derive/", pkgUrl = "", synopsis = "A program and library to derive instances for data types", description = "Data.Derive is a library and a tool for deriving instances for Haskell programs.\nIt is designed to work with custom derivations, SYB and Template Haskell mechanisms.\nThe tool requires GHC, but the generated code is portable to all compilers.\nWe see this tool as a competitor to DrIFT.", category = "Development", exposed = True, exposedModules = [ModuleName ["Data","DeriveMain"],ModuleName ["Data","DeriveTH"],ModuleName ["Data","DeriveDSL"],ModuleName ["Data","Derive","All"],ModuleName ["Data","Derive","DSL","Apply"],ModuleName ["Data","Derive","DSL","Derive"],ModuleName ["Data","Derive","DSL","DSL"],ModuleName ["Data","Derive","DSL","HSE"],ModuleName ["Data","Derive","DSL","SYB"],ModuleName ["Data","Derive","Instance","Arities"],ModuleName ["Data","Derive","Class","Arities"],ModuleName ["Data","Derive","Class","Default"],ModuleName ["Language","Haskell"],ModuleName ["Language","Haskell","Convert"],ModuleName ["Language","Haskell","TH","All"],ModuleName ["Language","Haskell","TH","Compat"],ModuleName ["Language","Haskell","TH","Data"],ModuleName ["Language","Haskell","TH","ExpandSynonym"],ModuleName ["Language","Haskell","TH","FixedPpr"],ModuleName ["Language","Haskell","TH","Helper"],ModuleName ["Language","Haskell","TH","Peephole"],ModuleName ["Data","Derive","Arbitrary"],ModuleName ["Data","Derive","ArbitraryOld"],ModuleName ["Data","Derive","Arities"],ModuleName ["Data","Derive","Binary"],ModuleName ["Data","Derive","BinaryDefer"],ModuleName ["Data","Derive","Bounded"],ModuleName ["Data","Derive","Data"],ModuleName ["Data","Derive","DataAbstract"],ModuleName ["Data","Derive","Default"],ModuleName ["Data","Derive","Enum"],ModuleName ["Data","Derive","EnumCyclic"],ModuleName ["Data","Derive","Eq"],ModuleName ["Data","Derive","Fold"],ModuleName ["Data","Derive","Foldable"],ModuleName ["Data","Derive","From"],ModuleName ["Data","Derive","Functor"],ModuleName ["Data","Derive","Has"],ModuleName ["Data","Derive","Is"],ModuleName ["Data","Derive","JSON"],ModuleName ["Data","Derive","LazySet"],ModuleName ["Data","Derive","Lens"],ModuleName ["Data","Derive","Monoid"],ModuleName ["Data","Derive","NFData"],ModuleName ["Data","Derive","Ord"],ModuleName ["Data","Derive","Read"],ModuleName ["Data","Derive","Ref"],ModuleName ["Data","Derive","Serial"],ModuleName ["Data","Derive","Serialize"],ModuleName ["Data","Derive","Set"],ModuleName ["Data","Derive","Show"],ModuleName ["Data","Derive","Traversable"],ModuleName ["Data","Derive","Typeable"],ModuleName ["Data","Derive","UniplateDirect"],ModuleName ["Data","Derive","UniplateTypeable"],ModuleName ["Data","Derive","Update"],ModuleName ["Data","Derive","Internal","Derivation"]], hiddenModules = [ModuleName ["Data","Derive","Internal","Instance"],ModuleName ["Data","Derive","Internal","Traversal"],ModuleName ["Derive","Main"],ModuleName ["Derive","Derivation"],ModuleName ["Derive","Flags"],ModuleName ["Derive","Generate"],ModuleName ["Derive","Test"],ModuleName ["Derive","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/lib"], hsLibraries = ["HSderive-2.5.11"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b",InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4",InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html/derive.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/derive-2.5.11/doc/html"]}])]),(PackageName "directory",fromList [(Version {versionBranch = [1,1,0,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691", sourcePackageId = PackageIdentifier {pkgName = PackageName "directory", pkgVersion = Version {versionBranch = [1,1,0,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "library for directory handling", description = "This package provides a library for handling directories.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Directory"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2"], hsLibraries = ["HSdirectory-1.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/directory-1.1.0.2/include"], includes = ["HsDirectory.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2/directory.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/directory-1.1.0.2"]}])]),(PackageName "distributed-process",fromList [(Version {versionBranch = [0,4,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-process-0.4.2-8607f6307a30086acbe051bdd9982dfd", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-process", pkgVersion = Version {versionBranch = [0,4,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "watson.timothy@gmail.com, edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Cloud Haskell: Erlang-style concurrency in Haskell", description = "This is an implementation of Cloud Haskell, as described in\n/Towards Haskell in the Cloud/ by Jeff Epstein, Andrew Black,\nand Simon Peyton Jones\n(),\nalthough some of the details are different. The precise message\npassing semantics are based on /A unified semantics for future Erlang/\nby Hans Svensson, Lars-\197ke Fredlund and Clara Benac Earle.\nYou will probably also want to install a Cloud Haskell backend such\nas distributed-process-simplelocalnet.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Process","Internal","Closure","TH"],ModuleName ["Control","Distributed","Process"],ModuleName ["Control","Distributed","Process","Serializable"],ModuleName ["Control","Distributed","Process","Closure"],ModuleName ["Control","Distributed","Process","Node"],ModuleName ["Control","Distributed","Process","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Internal","CQueue"],ModuleName ["Control","Distributed","Process","Internal","Types"],ModuleName ["Control","Distributed","Process","Internal","Trace"],ModuleName ["Control","Distributed","Process","Internal","Closure","BuiltIn"],ModuleName ["Control","Distributed","Process","Internal","Messaging"],ModuleName ["Control","Distributed","Process","Internal","StrictList"],ModuleName ["Control","Distributed","Process","Internal","StrictMVar"],ModuleName ["Control","Distributed","Process","Internal","WeakTQueue"],ModuleName ["Control","Distributed","Process","Internal","StrictContainerAccessors"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/lib"], hsLibraries = ["HSdistributed-process-0.4.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "data-accessor-0.2.2.3-30fc4efe5e4cc13831f87b4ac86c8ceb",InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29",InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7",InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0",InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html/distributed-process.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-process-0.4.2/doc/html"]}])]),(PackageName "distributed-static",fromList [(Version {versionBranch = [0,2,1,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "distributed-static-0.2.1.1-2ad43fb834445ca66ad899bd00ba1b15", sourcePackageId = PackageIdentifier {pkgName = PackageName "distributed-static", pkgVersion = Version {versionBranch = [0,2,1,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://www.github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Compositional, type-safe, polymorphic static values and closures", description = "/Towards Haskell in the Cloud/ (Epstein et al, Haskell\nSymposium 2011) introduces the concept of /static/ values:\nvalues that are known at compile time. In a distributed\nsetting where all nodes are running the same executable,\nstatic values can be serialized simply by transmitting a\ncode pointer to the value. This however requires special\ncompiler support, which is not yet available in ghc. We\ncan mimick the behaviour by keeping an explicit mapping\n('RemoteTable') from labels to values (and making sure\nthat all distributed nodes are using the same\n'RemoteTable'). In this module we implement this mimickry\nand various extensions: type safety (including for\npolymorphic static values) and compositionality.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Distributed","Static"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/lib"], hsLibraries = ["HSdistributed-static-0.2.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html/distributed-static.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/distributed-static-0.2.1.1/doc/html"]}])]),(PackageName "filepath",fromList [(Version {versionBranch = [1,3,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57", sourcePackageId = PackageIdentifier {pkgName = PackageName "filepath", pkgVersion = Version {versionBranch = [1,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Neil Mitchell", stability = "", homepage = "http://www-users.cs.york.ac.uk/~ndm/filepath/", pkgUrl = "", synopsis = "Library for manipulating FilePaths in a cross platform way.", description = "", category = "System", exposed = True, exposedModules = [ModuleName ["System","FilePath"],ModuleName ["System","FilePath","Posix"],ModuleName ["System","FilePath","Windows"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/filepath-1.3.0.0"], hsLibraries = ["HSfilepath-1.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0/filepath.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/filepath-1.3.0.0"]}])]),(PackageName "ghc-prim",fromList [(Version {versionBranch = [0,2,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7", sourcePackageId = PackageIdentifier {pkgName = PackageName "ghc-prim", pkgVersion = Version {versionBranch = [0,2,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "GHC primitives", description = "GHC primitives.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Prim"],ModuleName ["GHC","Classes"],ModuleName ["GHC","CString"],ModuleName ["GHC","Debug"],ModuleName ["GHC","Generics"],ModuleName ["GHC","Magic"],ModuleName ["GHC","PrimopWrappers"],ModuleName ["GHC","IntWord64"],ModuleName ["GHC","Tuple"],ModuleName ["GHC","Types"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/ghc-prim-0.2.0.0"], hsLibraries = ["HSghc-prim-0.2.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "builtin_rts"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0/ghc-prim.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/ghc-prim-0.2.0.0"]}])]),(PackageName "hashable",fromList [(Version {versionBranch = [1,1,2,5], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c", sourcePackageId = PackageIdentifier {pkgName = PackageName "hashable", pkgVersion = Version {versionBranch = [1,1,2,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "johan.tibell@gmail.com", author = "Milan Straka \nJohan Tibell ", stability = "Provisional", homepage = "http://github.com/tibbe/hashable", pkgUrl = "", synopsis = "A class for types that can be converted to a hash value", description = "This package defines a class, 'Hashable', for types that\ncan be converted to a hash value. This class\nexists for the benefit of hashing-based data\nstructures. The package provides instances for\nbasic types and a way to combine hash values.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Hashable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/lib"], hsLibraries = ["HShashable-1.1.2.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43",InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html/hashable.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/hashable-1.1.2.5/doc/html"]}])]),(PackageName "haskell-src-exts",fromList [(Version {versionBranch = [1,13,5], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "haskell-src-exts-1.13.5-1275270c1e2ea6c7dc6a7ab5d2ae2697", sourcePackageId = PackageIdentifier {pkgName = PackageName "haskell-src-exts", pkgVersion = Version {versionBranch = [1,13,5], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Niklas Broberg ", author = "Niklas Broberg", stability = "Stable", homepage = "http://code.haskell.org/haskell-src-exts", pkgUrl = "", synopsis = "Manipulating Haskell source: abstract syntax, lexer, parser, and pretty-printer", description = "Haskell-Source with Extensions (HSE, haskell-src-exts)\nis an extension of the standard haskell-src package,\nand handles most registered syntactic extensions to Haskell, including:\n\n* Multi-parameter type classes with functional dependencies\n\n* Indexed type families (including associated types)\n\n* Empty data declarations\n\n* GADTs\n\n* Implicit parameters\n\n* Template Haskell\n\nand a few more. All extensions implemented in GHC are supported.\nApart from these standard extensions,\nit also handles regular patterns as per the HaRP extension\nas well as HSX-style embedded XML syntax.", category = "Language", exposed = True, exposedModules = [ModuleName ["Language","Haskell","Exts"],ModuleName ["Language","Haskell","Exts","Lexer"],ModuleName ["Language","Haskell","Exts","Parser"],ModuleName ["Language","Haskell","Exts","Pretty"],ModuleName ["Language","Haskell","Exts","Syntax"],ModuleName ["Language","Haskell","Exts","Extension"],ModuleName ["Language","Haskell","Exts","Build"],ModuleName ["Language","Haskell","Exts","Fixity"],ModuleName ["Language","Haskell","Exts","Comments"],ModuleName ["Language","Haskell","Exts","SrcLoc"],ModuleName ["Language","Haskell","Exts","Annotated"],ModuleName ["Language","Haskell","Exts","Annotated","Syntax"],ModuleName ["Language","Haskell","Exts","Annotated","Fixity"],ModuleName ["Language","Haskell","Exts","Annotated","Build"],ModuleName ["Language","Haskell","Exts","Annotated","ExactPrint"],ModuleName ["Language","Haskell","Exts","Annotated","Simplify"]], hiddenModules = [ModuleName ["Language","Haskell","Exts","ExtScheme"],ModuleName ["Language","Haskell","Exts","ParseMonad"],ModuleName ["Language","Haskell","Exts","ParseSyntax"],ModuleName ["Language","Haskell","Exts","InternalLexer"],ModuleName ["Language","Haskell","Exts","ParseUtils"],ModuleName ["Language","Haskell","Exts","InternalParser"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/lib"], hsLibraries = ["HShaskell-src-exts-1.13.5"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "cpphs-1.14-b2d77a0c7ae3c3df4cd45483fb1410c4",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html/haskell-src-exts.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/haskell-src-exts-1.13.5/doc/html"]}])]),(PackageName "integer-gmp",fromList [(Version {versionBranch = [0,4,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43", sourcePackageId = PackageIdentifier {pkgName = PackageName "integer-gmp", pkgVersion = Version {versionBranch = [0,4,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Integer library based on GMP", description = "This package contains an Integer library based on GMP.", category = "", exposed = True, exposedModules = [ModuleName ["GHC","Integer"],ModuleName ["GHC","Integer","GMP","Internals"],ModuleName ["GHC","Integer","GMP","Prim"],ModuleName ["GHC","Integer","Logarithms"],ModuleName ["GHC","Integer","Logarithms","Internals"]], hiddenModules = [ModuleName ["GHC","Integer","Type"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/integer-gmp-0.4.0.0"], hsLibraries = ["HSinteger-gmp-0.4.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0/integer-gmp.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/integer-gmp-0.4.0.0"]}])]),(PackageName "mtl",fromList [(Version {versionBranch = [2,1,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "mtl-2.1.2-02e701f9b1590ee88a0b5b0bd5d93a29", sourcePackageId = PackageIdentifier {pkgName = PackageName "mtl", pkgVersion = Version {versionBranch = [2,1,2], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Edward Kmett ", author = "Andy Gill", stability = "", homepage = "http://github.com/ekmett/mtl", pkgUrl = "", synopsis = "Monad classes, using functional dependencies", description = "Monad classes using functional dependencies, with instances\nfor various monad transformers, inspired by the paper\n/Functional Programming with Overloading and Higher-Order Polymorphism/,\nby Mark P Jones, in /Advanced School of Functional Programming/, 1995\n().", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Monad","Cont"],ModuleName ["Control","Monad","Cont","Class"],ModuleName ["Control","Monad","Error"],ModuleName ["Control","Monad","Error","Class"],ModuleName ["Control","Monad","Identity"],ModuleName ["Control","Monad","List"],ModuleName ["Control","Monad","RWS"],ModuleName ["Control","Monad","RWS","Class"],ModuleName ["Control","Monad","RWS","Lazy"],ModuleName ["Control","Monad","RWS","Strict"],ModuleName ["Control","Monad","Reader"],ModuleName ["Control","Monad","Reader","Class"],ModuleName ["Control","Monad","State"],ModuleName ["Control","Monad","State","Class"],ModuleName ["Control","Monad","State","Lazy"],ModuleName ["Control","Monad","State","Strict"],ModuleName ["Control","Monad","Trans"],ModuleName ["Control","Monad","Writer"],ModuleName ["Control","Monad","Writer","Class"],ModuleName ["Control","Monad","Writer","Lazy"],ModuleName ["Control","Monad","Writer","Strict"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/lib"], hsLibraries = ["HSmtl-2.1.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html/mtl.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/mtl-2.1.2/doc/html"]}])]),(PackageName "network-transport",fromList [(Version {versionBranch = [0,3,0,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "network-transport-0.3.0.1-ed5dc5a845a1c860f7b1f86e3fad0812", sourcePackageId = PackageIdentifier {pkgName = PackageName "network-transport", pkgVersion = Version {versionBranch = [0,3,0,1], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com, duncan@well-typed.com", author = "Duncan Coutts, Nicolas Wu, Edsko de Vries", stability = "experimental", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Network abstraction layer", description = "\"Network.Transport\" is a Network Abstraction Layer which provides\nthe following high-level concepts:\n\n* Nodes in the network are represented by 'EndPoint's. These are\nheavyweight stateful objects.\n\n* Each 'EndPoint' has an 'EndPointAddress'.\n\n* Connections can be established from one 'EndPoint' to another\nusing the 'EndPointAddress' of the remote end.\n\n* The 'EndPointAddress' can be serialised and sent over the\nnetwork, where as 'EndPoint's and connections cannot.\n\n* Connections between 'EndPoint's are unidirectional and lightweight.\n\n* Outgoing messages are sent via a 'Connection' object that\nrepresents the sending end of the connection.\n\n* Incoming messages for /all/ of the incoming connections on\nan 'EndPoint' are collected via a shared receive queue.\n\n* In addition to incoming messages, 'EndPoint's are notified of\nother 'Event's such as new connections or broken connections.\n\nThis design was heavily influenced by the design of the Common\nCommunication Interface\n().\nImportant design goals are:\n\n* Connections should be lightweight: it should be no problem to\ncreate thousands of connections between endpoints.\n\n* Error handling is explicit: every function declares as part of\nits type which errors it can return (no exceptions are thrown)\n\n* Error handling is \"abstract\": errors that originate from\nimplementation specific problems (such as \"no more sockets\" in\nthe TCP implementation) get mapped to generic errors\n(\"insufficient resources\") at the Transport level.\n\nThis package provides the generic interface only; you will\nprobably also want to install at least one transport\nimplementation (network-transport-*).", category = "Network", exposed = True, exposedModules = [ModuleName ["Network","Transport"],ModuleName ["Network","Transport","Util"],ModuleName ["Network","Transport","Internal"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/lib"], hsLibraries = ["HSnetwork-transport-0.3.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html/network-transport.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/network-transport-0.3.0.1/doc/html"]}])]),(PackageName "old-locale",fromList [(Version {versionBranch = [1,0,0,4], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-locale", pkgVersion = Version {versionBranch = [1,0,0,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "locale library", description = "This package provides the old locale library.\nFor new code, the new locale library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Locale"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-locale-1.0.0.4"], hsLibraries = ["HSold-locale-1.0.0.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4/old-locale.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-locale-1.0.0.4"]}])]),(PackageName "old-time",fromList [(Version {versionBranch = [1,1,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "old-time-1.1.0.0-b8e05d63ad4954b34fe6dc2bcdddfd5d", sourcePackageId = PackageIdentifier {pkgName = PackageName "old-time", pkgVersion = Version {versionBranch = [1,1,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Time library", description = "This package provides the old time library.\nFor new code, the new time library is recommended.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Time"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0"], hsLibraries = ["HSold-time-1.1.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/old-time-1.1.0.0/include"], includes = ["HsTime.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0/old-time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/old-time-1.1.0.0"]}])]),(PackageName "pretty",fromList [(Version {versionBranch = [1,1,1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b", sourcePackageId = PackageIdentifier {pkgName = PackageName "pretty", pkgVersion = Version {versionBranch = [1,1,1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "David Terei ", author = "", stability = "Stable", homepage = "http://github.com/haskell/pretty", pkgUrl = "", synopsis = "Pretty-printing library", description = "This package contains a pretty-printing library, a set of API's\nthat provides a way to easily print out text in a consistent\nformat of your choosing. This is useful for compilers and related\ntools.\n\nThis library was originally designed by John Hughes's and has since\nbeen heavily modified by Simon Peyton Jones.", category = "Text", exposed = True, exposedModules = [ModuleName ["Text","PrettyPrint"],ModuleName ["Text","PrettyPrint","HughesPJ"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/pretty-1.1.1.0"], hsLibraries = ["HSpretty-1.1.1.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0/pretty.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/pretty-1.1.1.0"]}])]),(PackageName "process",fromList [(Version {versionBranch = [1,1,0,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "process-1.1.0.1-f002477a35b880981ab3a429893cfea4", sourcePackageId = PackageIdentifier {pkgName = PackageName "process", pkgVersion = Version {versionBranch = [1,1,0,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Process libraries", description = "This package contains libraries for dealing with system processes.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Process","Internals"],ModuleName ["System","Process"],ModuleName ["System","Cmd"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1"], hsLibraries = ["HSprocess-1.1.0.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/process-1.1.0.1/include"], includes = ["runProcess.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "directory-1.1.0.2-72e928d14fc50f31f7e6404839a15691",InstalledPackageId "filepath-1.3.0.0-f998e5510c76a98913f57b14b4f16c57",InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1/process.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/process-1.1.0.1"]}])]),(PackageName "random",fromList [(Version {versionBranch = [1,0,1,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "random-1.0.1.1-0a3c630945e8d36365483db523a97aa1", sourcePackageId = PackageIdentifier {pkgName = PackageName "random", pkgVersion = Version {versionBranch = [1,0,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "rrnewton@gmail.com", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "random number library", description = "This package provides a basic random number generation\nlibrary, including the ability to split random number\ngenerators.", category = "System", exposed = True, exposedModules = [ModuleName ["System","Random"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/lib"], hsLibraries = ["HSrandom-1.0.1.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html/random.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/random-1.0.1.1/doc/html"]}])]),(PackageName "rank1dynamic",fromList [(Version {versionBranch = [0,1,0,2], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "rank1dynamic-0.1.0.2-061d8f0dfc967624e4c969faf730a0f0", sourcePackageId = PackageIdentifier {pkgName = PackageName "rank1dynamic", pkgVersion = Version {versionBranch = [0,1,0,2], versionTags = []}}, license = BSD3, copyright = "Well-Typed LLP", maintainer = "edsko@well-typed.com", author = "Edsko de Vries", stability = "", homepage = "http://github.com/haskell-distributed/distributed-process", pkgUrl = "", synopsis = "Like Data.Dynamic/Data.Typeable but with support for rank-1 polymorphic types", description = "\"Data.Typeable\" and \"Data.Dynamic\" only support monomorphic types.\nIn this package we provide similar functionality but with\nsupport for rank-1 polymorphic types.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","Rank1Dynamic"],ModuleName ["Data","Rank1Typeable"]], hiddenModules = [], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/lib"], hsLibraries = ["HSrank1dynamic-0.1.0.2"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "binary-0.5.1.0-add1596d9f742ee50cfb7acbd55d7f7b",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html/rank1dynamic.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/rank1dynamic-0.1.0.2/doc/html"]}])]),(PackageName "rts",fromList [(Version {versionBranch = [1,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "builtin_rts", sourcePackageId = PackageIdentifier {pkgName = PackageName "rts", pkgVersion = Version {versionBranch = [1,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "glasgow-haskell-users@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "", category = "", exposed = True, exposedModules = [], hiddenModules = [], trusted = False, importDirs = [], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2"], hsLibraries = ["HSrts"], extraLibraries = ["m","dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/include"], includes = ["Stg.h"], depends = [], hugsOptions = [], ccOptions = [], ldOptions = ["-u","_ghczmprim_GHCziTypes_Izh_static_info","-u","_ghczmprim_GHCziTypes_Czh_static_info","-u","_ghczmprim_GHCziTypes_Fzh_static_info","-u","_ghczmprim_GHCziTypes_Dzh_static_info","-u","_base_GHCziPtr_Ptr_static_info","-u","_base_GHCziWord_Wzh_static_info","-u","_base_GHCziInt_I8zh_static_info","-u","_base_GHCziInt_I16zh_static_info","-u","_base_GHCziInt_I32zh_static_info","-u","_base_GHCziInt_I64zh_static_info","-u","_base_GHCziWord_W8zh_static_info","-u","_base_GHCziWord_W16zh_static_info","-u","_base_GHCziWord_W32zh_static_info","-u","_base_GHCziWord_W64zh_static_info","-u","_base_GHCziStable_StablePtr_static_info","-u","_ghczmprim_GHCziTypes_Izh_con_info","-u","_ghczmprim_GHCziTypes_Czh_con_info","-u","_ghczmprim_GHCziTypes_Fzh_con_info","-u","_ghczmprim_GHCziTypes_Dzh_con_info","-u","_base_GHCziPtr_Ptr_con_info","-u","_base_GHCziPtr_FunPtr_con_info","-u","_base_GHCziStable_StablePtr_con_info","-u","_ghczmprim_GHCziTypes_False_closure","-u","_ghczmprim_GHCziTypes_True_closure","-u","_base_GHCziPack_unpackCString_closure","-u","_base_GHCziIOziException_stackOverflow_closure","-u","_base_GHCziIOziException_heapOverflow_closure","-u","_base_ControlziExceptionziBase_nonTermination_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnMVar_closure","-u","_base_GHCziIOziException_blockedIndefinitelyOnSTM_closure","-u","_base_ControlziExceptionziBase_nestedAtomically_closure","-u","_base_GHCziWeak_runFinalizzerBatch_closure","-u","_base_GHCziTopHandler_flushStdHandles_closure","-u","_base_GHCziTopHandler_runIO_closure","-u","_base_GHCziTopHandler_runNonIO_closure","-u","_base_GHCziConcziIO_ensureIOManagerIsRunning_closure","-u","_base_GHCziConcziSync_runSparks_closure","-u","_base_GHCziConcziSignal_runHandlers_closure","-Wl,-search_paths_first"], frameworkDirs = [], frameworks = [], haddockInterfaces = [], haddockHTMLs = []}])]),(PackageName "stm",fromList [(Version {versionBranch = [2,4], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "stm-2.4-464c94c0d4d2a3fb7bf1cd1f8648d14d", sourcePackageId = PackageIdentifier {pkgName = PackageName "stm", pkgVersion = Version {versionBranch = [2,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "Software Transactional Memory", description = "A modular composable concurrency abstraction.\n\nChanges in version 2.4\n\n* Added \"Control.Concurrent.STM.TQueue\" (a faster @TChan@)\n\n* Added \"Control.Concurrent.STM.TBQueue\" (a bounded channel based on @TQueue@)\n\n* @TChan@ has an @Eq@ instances\n\n* Added @newBroadcastTChan@ and @newBroadcastTChanIO@\n\n* Some performance improvements for @TChan@\n\n* Added @cloneTChan@", category = "Concurrency", exposed = True, exposedModules = [ModuleName ["Control","Concurrent","STM"],ModuleName ["Control","Concurrent","STM","TArray"],ModuleName ["Control","Concurrent","STM","TVar"],ModuleName ["Control","Concurrent","STM","TChan"],ModuleName ["Control","Concurrent","STM","TMVar"],ModuleName ["Control","Concurrent","STM","TQueue"],ModuleName ["Control","Concurrent","STM","TBQueue"],ModuleName ["Control","Monad","STM"]], hiddenModules = [ModuleName ["Control","Sequential","STM"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/lib"], hsLibraries = ["HSstm-2.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html/stm.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/stm-2.4/doc/html"]}])]),(PackageName "syb",fromList [(Version {versionBranch = [0,3,7], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24", sourcePackageId = PackageIdentifier {pkgName = PackageName "syb", pkgVersion = Version {versionBranch = [0,3,7], versionTags = []}}, license = BSD3, copyright = "", maintainer = "generics@haskell.org", author = "Ralf Lammel, Simon Peyton Jones, Jose Pedro Magalhaes", stability = "provisional", homepage = "http://www.cs.uu.nl/wiki/GenericProgramming/SYB", pkgUrl = "", synopsis = "Scrap Your Boilerplate", description = "This package contains the generics system described in the\n/Scrap Your Boilerplate/ papers (see\n).\nIt defines the @Data@ class of types permitting folding and unfolding\nof constructor applications, instances of this class for primitive\ntypes, and a variety of traversals.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics"],ModuleName ["Data","Generics","Basics"],ModuleName ["Data","Generics","Instances"],ModuleName ["Data","Generics","Aliases"],ModuleName ["Data","Generics","Schemes"],ModuleName ["Data","Generics","Text"],ModuleName ["Data","Generics","Twins"],ModuleName ["Data","Generics","Builders"],ModuleName ["Generics","SYB"],ModuleName ["Generics","SYB","Basics"],ModuleName ["Generics","SYB","Instances"],ModuleName ["Generics","SYB","Aliases"],ModuleName ["Generics","SYB","Schemes"],ModuleName ["Generics","SYB","Text"],ModuleName ["Generics","SYB","Twins"],ModuleName ["Generics","SYB","Builders"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/lib"], hsLibraries = ["HSsyb-0.3.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html/syb.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/syb-0.3.7/doc/html"]}])]),(PackageName "template-haskell",fromList [(Version {versionBranch = [2,7,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "template-haskell-2.7.0.0-29110cc89a711d6ab3e7ee0e0a8ee949", sourcePackageId = PackageIdentifier {pkgName = PackageName "template-haskell", pkgVersion = Version {versionBranch = [2,7,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "", description = "Facilities for manipulating Haskell source code using Template Haskell.", category = "", exposed = True, exposedModules = [ModuleName ["Language","Haskell","TH","Syntax","Internals"],ModuleName ["Language","Haskell","TH","Syntax"],ModuleName ["Language","Haskell","TH","PprLib"],ModuleName ["Language","Haskell","TH","Ppr"],ModuleName ["Language","Haskell","TH","Lib"],ModuleName ["Language","Haskell","TH","Quote"],ModuleName ["Language","Haskell","TH"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/template-haskell-2.7.0.0"], hsLibraries = ["HStemplate-haskell-2.7.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "pretty-1.1.1.0-91ed62f0481a81d292d550eec35ee75b"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0/template-haskell.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/template-haskell-2.7.0.0"]}])]),(PackageName "text",fromList [(Version {versionBranch = [0,11,2,3], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "text-0.11.2.3-473d9a1761b27c7315f2ef4569d93c3c", sourcePackageId = PackageIdentifier {pkgName = PackageName "text", pkgVersion = Version {versionBranch = [0,11,2,3], versionTags = []}}, license = BSD3, copyright = "2009-2011 Bryan O'Sullivan, 2008-2009 Tom Harper", maintainer = "Bryan O'Sullivan ", author = "Bryan O'Sullivan ", stability = "", homepage = "https://github.com/bos/text", pkgUrl = "", synopsis = "An efficient packed Unicode text type.", description = "\nAn efficient packed, immutable Unicode text type (both strict and\nlazy), with a powerful loop fusion optimization framework.\n\nThe 'Text' type represents Unicode character strings, in a time and\nspace-efficient manner. This package provides text processing\ncapabilities that are optimized for performance critical use, both\nin terms of large data quantities and high speed.\n\nThe 'Text' type provides character-encoding, type-safe case\nconversion via whole-string case conversion functions. It also\nprovides a range of functions for converting 'Text' values to and from\n'ByteStrings', using several standard encodings.\n\nEfficient locale-sensitive support for text IO is also supported.\n\nThese modules are intended to be imported qualified, to avoid name\nclashes with Prelude functions, e.g.\n\n> import qualified Data.Text as T\n\nTo use an extended and very rich family of functions for working\nwith Unicode text (including normalization, regular expressions,\nnon-standard encodings, text breaking, and locales), see\nthe @text-icu@ package:\n\n\n—— RELEASE NOTES ——\n\nChanges in 0.11.2.0:\n\n* String literals are now converted directly from the format in\nwhich GHC stores them into 'Text', without an intermediate\ntransformation through 'String', and without inlining of\nconversion code at each site where a string literal is declared.\n", category = "Data, Text", exposed = True, exposedModules = [ModuleName ["Data","Text"],ModuleName ["Data","Text","Array"],ModuleName ["Data","Text","Encoding"],ModuleName ["Data","Text","Encoding","Error"],ModuleName ["Data","Text","Foreign"],ModuleName ["Data","Text","IO"],ModuleName ["Data","Text","Internal"],ModuleName ["Data","Text","Lazy"],ModuleName ["Data","Text","Lazy","Builder"],ModuleName ["Data","Text","Lazy","Builder","Int"],ModuleName ["Data","Text","Lazy","Builder","RealFloat"],ModuleName ["Data","Text","Lazy","Encoding"],ModuleName ["Data","Text","Lazy","IO"],ModuleName ["Data","Text","Lazy","Internal"],ModuleName ["Data","Text","Lazy","Read"],ModuleName ["Data","Text","Read"]], hiddenModules = [ModuleName ["Data","Text","Encoding","Fusion"],ModuleName ["Data","Text","Encoding","Fusion","Common"],ModuleName ["Data","Text","Encoding","Utf16"],ModuleName ["Data","Text","Encoding","Utf32"],ModuleName ["Data","Text","Encoding","Utf8"],ModuleName ["Data","Text","Fusion"],ModuleName ["Data","Text","Fusion","CaseMapping"],ModuleName ["Data","Text","Fusion","Common"],ModuleName ["Data","Text","Fusion","Internal"],ModuleName ["Data","Text","Fusion","Size"],ModuleName ["Data","Text","IO","Internal"],ModuleName ["Data","Text","Lazy","Builder","Functions"],ModuleName ["Data","Text","Lazy","Builder","RealFloat","Functions"],ModuleName ["Data","Text","Lazy","Encoding","Fusion"],ModuleName ["Data","Text","Lazy","Fusion"],ModuleName ["Data","Text","Lazy","Search"],ModuleName ["Data","Text","Private"],ModuleName ["Data","Text","Search"],ModuleName ["Data","Text","Unsafe"],ModuleName ["Data","Text","Unsafe","Base"],ModuleName ["Data","Text","UnsafeChar"],ModuleName ["Data","Text","UnsafeShift"],ModuleName ["Data","Text","Util"]], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/lib"], hsLibraries = ["HStext-0.11.2.3"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "array-0.4.0.0-0b6c5ca7e879a14d110ca4c001dd9297",InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "ghc-prim-0.2.0.0-7d3c2c69a5e8257a04b2c679c40e2fa7",InstalledPackageId "integer-gmp-0.4.0.0-af3a28fdc4138858e0c7c5ecc2a64f43"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html/text.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/text-0.11.2.3/doc/html"]}])]),(PackageName "time",fromList [(Version {versionBranch = [1,4], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "time-1.4-d61e2caaa0486655b4e141dc277ed49f", sourcePackageId = PackageIdentifier {pkgName = PackageName "time", pkgVersion = Version {versionBranch = [1,4], versionTags = []}}, license = BSD3, copyright = "", maintainer = "", author = "Ashley Yakeley", stability = "stable", homepage = "http://semantic.org/TimeLib/", pkgUrl = "", synopsis = "A time library", description = "A time library", category = "System", exposed = True, exposedModules = [ModuleName ["Data","Time","Calendar"],ModuleName ["Data","Time","Calendar","MonthDay"],ModuleName ["Data","Time","Calendar","OrdinalDate"],ModuleName ["Data","Time","Calendar","WeekDate"],ModuleName ["Data","Time","Calendar","Julian"],ModuleName ["Data","Time","Calendar","Easter"],ModuleName ["Data","Time","Clock"],ModuleName ["Data","Time","Clock","POSIX"],ModuleName ["Data","Time","Clock","TAI"],ModuleName ["Data","Time","LocalTime"],ModuleName ["Data","Time","Format"],ModuleName ["Data","Time"]], hiddenModules = [ModuleName ["Data","Time","Calendar","Private"],ModuleName ["Data","Time","Calendar","Days"],ModuleName ["Data","Time","Calendar","Gregorian"],ModuleName ["Data","Time","Calendar","JulianYearDay"],ModuleName ["Data","Time","Clock","Scale"],ModuleName ["Data","Time","Clock","UTC"],ModuleName ["Data","Time","Clock","CTimeval"],ModuleName ["Data","Time","Clock","UTCDiff"],ModuleName ["Data","Time","LocalTime","TimeZone"],ModuleName ["Data","Time","LocalTime","TimeOfDay"],ModuleName ["Data","Time","LocalTime","LocalTime"],ModuleName ["Data","Time","Format","Parse"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4"], hsLibraries = ["HStime-1.4"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/time-1.4/include"], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "old-locale-1.0.0.4-5e45cabd3b4fdcad9e353ea3845f5ef7"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4/time.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/time-1.4"]}])]),(PackageName "transformers",fromList [(Version {versionBranch = [0,3,0,0], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "transformers-0.3.0.0-8e66ecc7d4dae2b07b2b5406908c70e4", sourcePackageId = PackageIdentifier {pkgName = PackageName "transformers", pkgVersion = Version {versionBranch = [0,3,0,0], versionTags = []}}, license = BSD3, copyright = "", maintainer = "Ross Paterson ", author = "Andy Gill, Ross Paterson", stability = "", homepage = "", pkgUrl = "", synopsis = "Concrete functor and monad transformers", description = "A portable library of functor and monad transformers, inspired by\nthe paper \\\"Functional Programming with Overloading and Higher-Order\nPolymorphism\\\", by Mark P Jones,\nin /Advanced School of Functional Programming/, 1995\n().\n\nThis package contains:\n\n* the monad transformer class (in \"Control.Monad.Trans.Class\")\n\n* concrete functor and monad transformers, each with associated\noperations and functions to lift operations associated with other\ntransformers.\n\nIt can be used on its own in portable Haskell code, or with the monad\nclasses in the @mtl@ or @monads-tf@ packages, which automatically\nlift operations introduced by monad transformers through other\ntransformers.", category = "Control", exposed = True, exposedModules = [ModuleName ["Control","Applicative","Backwards"],ModuleName ["Control","Applicative","Lift"],ModuleName ["Control","Monad","IO","Class"],ModuleName ["Control","Monad","Trans","Class"],ModuleName ["Control","Monad","Trans","Cont"],ModuleName ["Control","Monad","Trans","Error"],ModuleName ["Control","Monad","Trans","Identity"],ModuleName ["Control","Monad","Trans","List"],ModuleName ["Control","Monad","Trans","Maybe"],ModuleName ["Control","Monad","Trans","Reader"],ModuleName ["Control","Monad","Trans","RWS"],ModuleName ["Control","Monad","Trans","RWS","Lazy"],ModuleName ["Control","Monad","Trans","RWS","Strict"],ModuleName ["Control","Monad","Trans","State"],ModuleName ["Control","Monad","Trans","State","Lazy"],ModuleName ["Control","Monad","Trans","State","Strict"],ModuleName ["Control","Monad","Trans","Writer"],ModuleName ["Control","Monad","Trans","Writer","Lazy"],ModuleName ["Control","Monad","Trans","Writer","Strict"],ModuleName ["Data","Functor","Compose"],ModuleName ["Data","Functor","Constant"],ModuleName ["Data","Functor","Identity"],ModuleName ["Data","Functor","Product"],ModuleName ["Data","Functor","Reverse"]], hiddenModules = [], trusted = False, importDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], libraryDirs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/lib"], hsLibraries = ["HStransformers-0.3.0.0"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html/transformers.haddock"], haddockHTMLs = ["/Library/Haskell/ghc-7.4.2/lib/transformers-0.3.0.0/doc/html"]}])]),(PackageName "uniplate",fromList [(Version {versionBranch = [1,6,7], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "uniplate-1.6.7-a79a8b4834c561964da115d92ceeb9fd", sourcePackageId = PackageIdentifier {pkgName = PackageName "uniplate", pkgVersion = Version {versionBranch = [1,6,7], versionTags = []}}, license = BSD3, copyright = "Neil Mitchell 2006-2012", maintainer = "Neil Mitchell ", author = "Neil Mitchell ", stability = "", homepage = "http://community.haskell.org/~ndm/uniplate/", pkgUrl = "", synopsis = "Help writing simple, concise and fast generic operations.", description = "Uniplate is library for writing simple and concise generic operations.\nUniplate has similar goals to the original Scrap Your Boilerplate work,\nbut is substantially simpler and faster. The Uniplate manual is available at\n.\n\nTo get started with Uniplate you should import one of the three following\nmodules:\n\n* \"Data.Generics.Uniplate.Data\" - to quickly start writing generic functions.\nMost users should start by importing this module.\n\n* \"Data.Generics.Uniplate.Direct\" - a replacement for \"Data.Generics.Uniplate.Data\"\nwith substantially higher performance (around 5 times), but requires writing\ninstance declarations.\n\n* \"Data.Generics.Uniplate.Operations\" - definitions of all the operations defined\nby Uniplate. Both the above two modules re-export this module.\n\nIn addition, some users may want to make use of the following modules:\n\n* \"Data.Generics.Uniplate.Zipper\" - a zipper built on top of Uniplate instances.\n\n* \"Data.Generics.SYB\" - users transitioning from the Scrap Your Boilerplate library.\n\n* \"Data.Generics.Compos\" - users transitioning from the Compos library.\n\n* \"Data.Generics.Uniplate.DataOnly\" - users making use of both @Data@ and @Direct@\nto avoid getting instance conflicts.", category = "Generics", exposed = True, exposedModules = [ModuleName ["Data","Generics","Str"],ModuleName ["Data","Generics","Compos"],ModuleName ["Data","Generics","SYB"],ModuleName ["Data","Generics","Uniplate","Data"],ModuleName ["Data","Generics","Uniplate","Data","Instances"],ModuleName ["Data","Generics","Uniplate","DataOnly"],ModuleName ["Data","Generics","Uniplate","Direct"],ModuleName ["Data","Generics","Uniplate","Operations"],ModuleName ["Data","Generics","Uniplate","Typeable"],ModuleName ["Data","Generics","Uniplate","Zipper"],ModuleName ["Data","Generics","Uniplate"],ModuleName ["Data","Generics","UniplateOn"],ModuleName ["Data","Generics","UniplateStr"],ModuleName ["Data","Generics","UniplateStrOn"],ModuleName ["Data","Generics","Biplate"],ModuleName ["Data","Generics","PlateDirect"],ModuleName ["Data","Generics","PlateTypeable"],ModuleName ["Data","Generics","PlateData"]], hiddenModules = [ModuleName ["Data","Generics","PlateInternal"],ModuleName ["Data","Generics","Uniplate","Internal","Data"],ModuleName ["Data","Generics","Uniplate","Internal","DataOnlyOperations"],ModuleName ["Data","Generics","Uniplate","Internal","Utils"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/lib"], hsLibraries = ["HSuniplate-1.6.7"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "containers-0.4.2.1-75f143aa39a3e77a1ce2300025bdd8ce",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c",InstalledPackageId "syb-0.3.7-60af41a2377e93620710d393692aff24",InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html/uniplate.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/uniplate-1.6.7/doc/html"]}])]),(PackageName "unix",fromList [(Version {versionBranch = [2,5,1,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "unix-2.5.1.1-29636eb78541401e8e00393ef5df097e", sourcePackageId = PackageIdentifier {pkgName = PackageName "unix", pkgVersion = Version {versionBranch = [2,5,1,1], versionTags = []}}, license = BSD3, copyright = "", maintainer = "libraries@haskell.org", author = "", stability = "", homepage = "", pkgUrl = "", synopsis = "POSIX functionality", description = "This package gives you access to the set of operating system\nservices standardised by POSIX 1003.1b (or the IEEE Portable\nOperating System Interface for Computing Environments -\nIEEE Std. 1003.1).\n\nThe package is not supported under Windows (except under Cygwin).", category = "System", exposed = True, exposedModules = [ModuleName ["System","Posix"],ModuleName ["System","Posix","ByteString"],ModuleName ["System","Posix","Error"],ModuleName ["System","Posix","Resource"],ModuleName ["System","Posix","Time"],ModuleName ["System","Posix","Unistd"],ModuleName ["System","Posix","User"],ModuleName ["System","Posix","Signals"],ModuleName ["System","Posix","Signals","Exts"],ModuleName ["System","Posix","Semaphore"],ModuleName ["System","Posix","SharedMem"],ModuleName ["System","Posix","ByteString","FilePath"],ModuleName ["System","Posix","Directory"],ModuleName ["System","Posix","Directory","ByteString"],ModuleName ["System","Posix","DynamicLinker","Module"],ModuleName ["System","Posix","DynamicLinker","Module","ByteString"],ModuleName ["System","Posix","DynamicLinker","Prim"],ModuleName ["System","Posix","DynamicLinker","ByteString"],ModuleName ["System","Posix","DynamicLinker"],ModuleName ["System","Posix","Files"],ModuleName ["System","Posix","Files","ByteString"],ModuleName ["System","Posix","IO"],ModuleName ["System","Posix","IO","ByteString"],ModuleName ["System","Posix","Env"],ModuleName ["System","Posix","Env","ByteString"],ModuleName ["System","Posix","Process"],ModuleName ["System","Posix","Process","Internals"],ModuleName ["System","Posix","Process","ByteString"],ModuleName ["System","Posix","Temp"],ModuleName ["System","Posix","Temp","ByteString"],ModuleName ["System","Posix","Terminal"],ModuleName ["System","Posix","Terminal","ByteString"]], hiddenModules = [ModuleName ["System","Posix","Directory","Common"],ModuleName ["System","Posix","DynamicLinker","Common"],ModuleName ["System","Posix","Files","Common"],ModuleName ["System","Posix","IO","Common"],ModuleName ["System","Posix","Process","Common"],ModuleName ["System","Posix","Terminal","Common"]], trusted = False, importDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], libraryDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1"], hsLibraries = ["HSunix-2.5.1.1"], extraLibraries = ["dl"], extraGHCiLibraries = [], includeDirs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/lib/ghc-7.4.2/unix-2.5.1.1/include"], includes = ["HsUnix.h","execvpe.h"], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "bytestring-0.9.2.1-0044644a71adfe5e950e6c6f6ca13065"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1/unix.haddock"], haddockHTMLs = ["/Library/Frameworks/GHC.framework/Versions/7.4.2-x86_64/usr/share/doc/ghc/html/libraries/unix-2.5.1.1"]}])]),(PackageName "unordered-containers",fromList [(Version {versionBranch = [0,2,2,1], versionTags = []},[InstalledPackageInfo {installedPackageId = InstalledPackageId "unordered-containers-0.2.2.1-d70d5ccb1df11dbbbaac89571b1ee46d", sourcePackageId = PackageIdentifier {pkgName = PackageName "unordered-containers", pkgVersion = Version {versionBranch = [0,2,2,1], versionTags = []}}, license = BSD3, copyright = "2010-2012 Johan Tibell\n2010 Edward Z. Yang", maintainer = "johan.tibell@gmail.com", author = "Johan Tibell", stability = "", homepage = "https://github.com/tibbe/unordered-containers", pkgUrl = "", synopsis = "Efficient hashing-based container types", description = "Efficient hashing-based container types. The containers have been\noptimized for performance critical use, both in terms of large data\nquantities and high speed.\n\nThe declared cost of each operation is either worst-case or\namortized, but remains valid even if structures are shared.", category = "Data", exposed = True, exposedModules = [ModuleName ["Data","HashMap","Lazy"],ModuleName ["Data","HashMap","Strict"],ModuleName ["Data","HashSet"]], hiddenModules = [ModuleName ["Data","HashMap","Array"],ModuleName ["Data","HashMap","Base"],ModuleName ["Data","HashMap","PopCount"],ModuleName ["Data","HashMap","Unsafe"],ModuleName ["Data","HashMap","UnsafeShift"]], trusted = False, importDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], libraryDirs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/lib"], hsLibraries = ["HSunordered-containers-0.2.2.1"], extraLibraries = [], extraGHCiLibraries = [], includeDirs = [], includes = [], depends = [InstalledPackageId "base-4.5.1.0-81d626fb996bc7e140a3fd4481b338cd",InstalledPackageId "deepseq-1.3.0.0-c26e15897417ecd448742528253d68f6",InstalledPackageId "hashable-1.1.2.5-14291f3b4e96b5599759ce7daa2bd37c"], hugsOptions = [], ccOptions = [], ldOptions = [], frameworkDirs = [], frameworks = [], haddockInterfaces = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html/unordered-containers.haddock"], haddockHTMLs = ["/Users/t4/Library/Haskell/ghc-7.4.2/lib/unordered-containers-0.2.2.1/doc/html"]}])])]), pkgDescrFile = Just "./distributed-process-platform.cabal", localPkgDescr = PackageDescription {package = PackageIdentifier {pkgName = PackageName "distributed-process-platform", pkgVersion = Version {versionBranch = [0,1,0], versionTags = []}}, license = BSD3, licenseFile = "LICENCE", copyright = "Tim Watson 2012 - 2013", maintainer = "watson.timothy@gmail.com", author = "Tim Watson", stability = "experimental", testedWith = [(GHC,ThisVersion (Version {versionBranch = [7,4,2], versionTags = []}))], homepage = "http://github.com/haskell-distributed/distributed-process-platform", pkgUrl = "", bugReports = "http://github.com/haskell-distributed/distributed-process-platform/issues", sourceRepos = [SourceRepo {repoKind = RepoHead, repoType = Just Git, repoLocation = Just "https://github.com/haskell-distributed/distributed-process-platform", repoModule = Nothing, repoBranch = Nothing, repoTag = Nothing, repoSubdir = Nothing}], synopsis = "The Cloud Haskell Application Platform", description = "Modelled after Erlang's OTP, this framework provides similar\nfacilities for Cloud Haskell, grouping essential practices\ninto a set of modules and standards designed to help you build\nconcurrent, distributed applications with relative ease.", category = "Control", customFieldsPD = [], buildDepends = [Dependency (PackageName "base") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4], versionTags = []})) (LaterVersion (Version {versionBranch = [4], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4,2], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))], specVersionRaw = Right (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,8], versionTags = []})) (LaterVersion (Version {versionBranch = [1,8], versionTags = []}))), buildType = Just Simple, library = Just (Library {exposedModules = [ModuleName ["Control","Distributed","Process","Platform"],ModuleName ["Control","Distributed","Process","Platform","Async"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncChan"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncSTM"],ModuleName ["Control","Distributed","Process","Platform","Call"],ModuleName ["Control","Distributed","Process","Platform","Test"],ModuleName ["Control","Distributed","Process","Platform","Time"],ModuleName ["Control","Distributed","Process","Platform","Timer"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"]], libExposed = True, libBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src"], otherModules = [ModuleName ["Control","Distributed","Process","Platform","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Platform","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","Internal","Common"],ModuleName ["Control","Distributed","Process","Platform","Async","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [], targetBuildDepends = [Dependency (PackageName "base") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4], versionTags = []})) (LaterVersion (Version {versionBranch = [4], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4,2], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}}), executables = [], testSuites = [TestSuite {testName = "GenServerTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestUtils.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["Control","Distributed","Process","Platform"],ModuleName ["Control","Distributed","Process","Platform","Async"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncChan"],ModuleName ["Control","Distributed","Process","Platform","Async","AsyncSTM"],ModuleName ["Control","Distributed","Process","Platform","Call"],ModuleName ["Control","Distributed","Process","Platform","Test"],ModuleName ["Control","Distributed","Process","Platform","Time"],ModuleName ["Control","Distributed","Process","Platform","Timer"],ModuleName ["Control","Distributed","Process","Platform","Internal","Primitives"],ModuleName ["Control","Distributed","Process","Platform","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","Internal","Common"],ModuleName ["Control","Distributed","Process","Platform","Async","Types"],ModuleName ["TestUtils"],ModuleName ["MathsDemo"],ModuleName ["Counter"],ModuleName ["SimplePool"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False},TestSuite {testName = "AsyncTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestAsync.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["TestUtils"],ModuleName ["TestGenServer"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False},TestSuite {testName = "PrimitivesTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestPrimitives.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["TestUtils"],ModuleName ["TestGenServer"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False},TestSuite {testName = "TimerTests", testInterface = TestSuiteExeV10 (Version {versionBranch = [1,0], versionTags = []}) "TestTimer.hs", testBuildInfo = BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = ["src","tests"], otherModules = [ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Client"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Server"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","Types"],ModuleName ["Control","Distributed","Process","Platform","ManagedProcess","Internal","GenProcess"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [EnableExtension CPP], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,["-Wall","-threaded","-rtsopts","-with-rtsopts=-N","-fno-warn-unused-do-bind"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [("x-uses-tf","true")], targetBuildDepends = [Dependency (PackageName "HUnit") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,2], versionTags = []})) (LaterVersion (Version {versionBranch = [1,2], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2], versionTags = []}))),Dependency (PackageName "ansi-terminal") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))),Dependency (PackageName "base") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,4], versionTags = []})) (LaterVersion (Version {versionBranch = [4,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))) (ThisVersion (Version {versionBranch = [4,5,1,0], versionTags = []}))),Dependency (PackageName "binary") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,5], versionTags = []})) (LaterVersion (Version {versionBranch = [0,5], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,7], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,5,1,0], versionTags = []}))),Dependency (PackageName "containers") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,4], versionTags = []})) (LaterVersion (Version {versionBranch = [0,4], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,6], versionTags = []}))) (ThisVersion (Version {versionBranch = [0,4,2,1], versionTags = []}))),Dependency (PackageName "derive") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,5,11], versionTags = []}))),Dependency (PackageName "distributed-process") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,4,2], versionTags = []}))),Dependency (PackageName "mtl") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [2,1,2], versionTags = []}))),Dependency (PackageName "network") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))),Dependency (PackageName "network-transport") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "network-transport-tcp") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,3], versionTags = []})) (LaterVersion (Version {versionBranch = [0,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,4], versionTags = []}))),Dependency (PackageName "stm") (IntersectVersionRanges (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [2,3], versionTags = []})) (LaterVersion (Version {versionBranch = [2,3], versionTags = []}))) (EarlierVersion (Version {versionBranch = [2,5], versionTags = []}))) (ThisVersion (Version {versionBranch = [2,4], versionTags = []}))),Dependency (PackageName "test-framework") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [0,6], versionTags = []})) (LaterVersion (Version {versionBranch = [0,6], versionTags = []}))) (EarlierVersion (Version {versionBranch = [0,9], versionTags = []}))),Dependency (PackageName "test-framework-hunit") AnyVersion,Dependency (PackageName "transformers") (IntersectVersionRanges AnyVersion (ThisVersion (Version {versionBranch = [0,3,0,0], versionTags = []})))]}, testEnabled = False}], benchmarks = [], dataFiles = [], dataDir = "", extraSrcFiles = [], extraTmpFiles = []}, withPrograms = [("alex",ConfiguredProgram {programId = "alex", programVersion = Just (Version {versionBranch = [3,0,2], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/alex"}}),("ar",ConfiguredProgram {programId = "ar", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/ar"}}),("gcc",ConfiguredProgram {programId = "gcc", programVersion = Nothing, programDefaultArgs = ["-m64"], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/gcc"}}),("ghc",ConfiguredProgram {programId = "ghc", programVersion = Just (Version {versionBranch = [7,4,2], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/ghc"}}),("ghc-pkg",ConfiguredProgram {programId = "ghc-pkg", programVersion = Just (Version {versionBranch = [7,4,2], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/ghc-pkg"}}),("haddock",ConfiguredProgram {programId = "haddock", programVersion = Just (Version {versionBranch = [2,11,0], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/haddock"}}),("happy",ConfiguredProgram {programId = "happy", programVersion = Just (Version {versionBranch = [1,18,10], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/happy"}}),("hpc",ConfiguredProgram {programId = "hpc", programVersion = Just (Version {versionBranch = [0,6], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/hpc"}}),("hsc2hs",ConfiguredProgram {programId = "hsc2hs", programVersion = Just (Version {versionBranch = [0,67], versionTags = []}), programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/hsc2hs"}}),("ld",ConfiguredProgram {programId = "ld", programVersion = Nothing, programDefaultArgs = ["-x","-arch","x86_64"], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/ld"}}),("ranlib",ConfiguredProgram {programId = "ranlib", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/ranlib"}}),("strip",ConfiguredProgram {programId = "strip", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/opt/local/bin/strip"}}),("tar",ConfiguredProgram {programId = "tar", programVersion = Nothing, programDefaultArgs = [], programOverrideArgs = [], programLocation = FoundOnSystem {locationPath = "/usr/bin/tar"}})], withPackageDB = [GlobalPackageDB,UserPackageDB], withVanillaLib = True, withProfLib = True, withSharedLib = False, withDynExe = False, withProfExe = False, withOptimization = NormalOptimisation, withGHCiLib = True, splitObjs = False, stripExes = True, progPrefix = "", progSuffix = ""} \ No newline at end of file From 2e341099f8e915a1cc4944825da53a43aeea6564 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 00:00:54 +0000 Subject: [PATCH 0905/2357] add rvm config --- .rvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .rvmrc diff --git a/.rvmrc b/.rvmrc new file mode 100644 index 00000000..35845a23 --- /dev/null +++ b/.rvmrc @@ -0,0 +1 @@ +rvm use 1.9.2 From 59e944d390b6da644004b49c8142c9a02115732b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 02:47:12 +0000 Subject: [PATCH 0906/2357] update content, docs --- _layouts/documentation.html | 2 + _layouts/tutorial.html | 6 +- documentation.md | 320 ++++++++++++++++++++++++++---------- index.md | 2 +- tutorials/1.tutorial.md | 129 +-------------- wiki/faq.md | 33 ++++ wiki/help.md | 23 +++ wiki/reliability.md | 55 +++++++ 8 files changed, 351 insertions(+), 219 deletions(-) create mode 100644 wiki/faq.md create mode 100644 wiki/help.md create mode 100644 wiki/reliability.md diff --git a/_layouts/documentation.html b/_layouts/documentation.html index de7ecbfb..88b17edc 100644 --- a/_layouts/documentation.html +++ b/_layouts/documentation.html @@ -24,6 +24,8 @@
  • Cloud Haskell Platform
  • Network Abstraction Layer
  • Concurrency and Distribution
  • +
  • What is Serializable
  • +
  • Typed Channels
  • Rethinking the Task Layer
  • diff --git a/_layouts/tutorial.html b/_layouts/tutorial.html index ee2c1ff0..a72f85a6 100644 --- a/_layouts/tutorial.html +++ b/_layouts/tutorial.html @@ -21,11 +21,9 @@ diff --git a/documentation.md b/documentation.md index 216dc5dc..86effec1 100644 --- a/documentation.md +++ b/documentation.md @@ -183,81 +183,174 @@ processes, and can receive messages synchronously from its own mailbox. The conceptual difference between threads and processes is that the latter do not share state, but communicate only via message passing. -Code that is executed in this manner must run in the `Process` monad. +Code that is executed in this manner must run in the `Process` monad. Our +process will look like any other monad code, plus we provide and instance +of `MonadIO` for `Process`, so you can `liftIO` to make IO actions +available. + +Processes reside on nodes, which in our implementation map directly to the +`Control.Distributed.Processes.Node` module. Given a configured +`Network.Transport` backend, starting a new node is fairly simple: + +{% highlight haskell %} +newLocalNode :: Transport -> IO LocalNode +{% endhighlight %} + +Once this function returns, the node will be *up and running* and able to +interact with other nodes and host processes. It is possible to start more +than one node in the same running program, though if you do this they will +continue to send messages to one another using the supplied `Network.Transport` +backend. + +Given a new node, there are two primitives for starting a new process. + +{% highlight haskell %} +forkProcess :: LocalNode -> Process () -> IO ProcessId +runProcess :: LocalNode -> Process () -> IO () +{% endhighlight %} + +Once we've spawned some processes, they can communicate with one another +using the messaging primitives provided by [distributed-processes][2], +which are well documented in the haddocks. + +### What is Serializable Processes can send data if the type implements the `Serializable` typeclass, which is done indirectly by implementing `Binary` and deriving `Typeable`. Implementations are already provided for primitives and some commonly used data structures. As programmers, we see the messages in nice high-level form -(e.g., Int, String, Ping, Pong, etc), however these data have to be encoded -in order to be sent over a network. +(e.g., `Int`, `String`, `Ping`, `Pong`, etc), however these data have to be +encoded in order to be sent over a communications channel. Not all types are `Serializable`, for example concurrency primitives such as `MVar` and `TVar` are meaningless outside the context of threads with a shared memory. Cloud Haskell programs remain free to use these constructs within processes or within processes on the same machine though. If you want to pass data between processes using *ordinary* concurrency primitives such as -`STM` then you're free to do so. Processes spawned locally are free to share -types like `TMVar a` just as normal Haskell threads are. Numerous features -in [distributed-process-platform][3] use this facility, for example the way -that `Control.Distributed.Processes.Platform.Async.AsyncSTM` handles passing -the result of its computation back to the caller, as the following snippet -demonstrates: +`STM` then you're free to do so. Processes spawned locally can share +types such as `TMVar` just as normal Haskell threads would. ----- +### Typed Channels -{% highlight haskell %} - root <- getSelfPid - result <- liftIO $ newEmptyTMVarIO - sigStart <- liftIO $ newEmptyTMVarIO - (sp, rp) <- newChan - - -- listener/response proxy - insulator <- spawnLocal $ do - worker <- spawnLocal $ do - liftIO $ atomically $ takeTMVar sigStart - r <- proc - void $ liftIO $ atomically $ putTMVar result (AsyncDone r) - - sendChan sp worker -- let the parent process know the worker pid - - wref <- monitor worker - rref <- case shouldLink of - True -> monitor root >>= return . Just - False -> return Nothing - finally (pollUntilExit worker result) - (unmonitor wref >> - return (maybe (return ()) unmonitor rref)) - - workerPid <- receiveChan rp - liftIO $ atomically $ putTMVar sigStart () - -- etc .... -{% endhighlight %} +Channels provides an alternative to message transmission with `send` and `expect`. +While `send` and `expect` allow transmission of messages of any `Serializable` +type, channels require a uniform type. Channels work like a distributed equivalent +of Haskell's `Control.Concurrent.Chan`, however they have distinct ends: a single +receiving port and a corollary send port. ----- +Channels provide a nice alternative to *bare send and receive*, which is a bit +*unHaskellish*, because the processes message queue has messages of multiple +types, and we have to do dynamic type checking. -Processes reside on nodes, which in our implementation map directly to the -`Control.Distributed.Processes.Node` module. Given a configured -`Network.Transport` backend, starting a new node is fairly simple: +We create channels with a call to `newChan`, and send/receive on them using the +`{send,receive}Chan` primitives: {% highlight haskell %} -newLocalNode :: Transport -> IO LocalNode +channelsDemo :: Process () +channelsDemo = do + (sp, rp) <- newChan :: Process (SendPort String, ReceivePort String) + + -- send on a channel + spawnLocal $ sendChan sp "hello!" + + -- receive on a channel + m <- receiveChan rp + say $ show m {% endhighlight %} -Once this function returns, the node will be *up and running* and able to -interact with other nodes and host processes. It is possible to start more -than one node in the same running program, though if you do this they will -continue to send messages to one another using the supplied `Network.Transport` -backend. +Channels are particularly useful when you are sending a message that needs a +response, because the code that receives the response knows exactly where it +came from - i.e., it knows that it came from the `SendPort` connected to +the `ReceivePort` on which it just received a response. + +Channels can sometimes allows message types to be simplified, as passing a +`ProcessId` to reply to isn't required. Channels are not so useful when you +need to spawn a process and then send a bunch a messages to it and wait for +replies, because we can’t send the `ReceivePort`. + +ReceivePorts can be merged, so you can listen on several simultaneously. In the +latest version of [distributed-process][2], you can listen for *regular* messages +and on multiple channels at the same time, using `matchChan` in the list of +allowed matches passed `receive`. + +### Linking and monitoring + +Processes can be linked to other processes, nodes or channels. Links are unidirectional, +and guarantee that once the linked object *dies*, the linked process will also be +terminated. Monitors do not cause the *listening* process to exit, but rather they +put a `ProcessMonitorNotification` into the process' mailbox. Linking and monitoring +are foundational tools for *supervising* processes, where a top level process manages +a set of children, starting, stopping and restarting them as necessary. + +### Stopping Processes + +Some processes, like the *outer* process in the previous example, will run until +they've completed and then return their value. This is just as we find with IO action, +and there is an instance of `MonadIO` for the `Process` monad, so you can `liftIO` if +you need to evaluate IO actions. + +Because processes are implemented with `forkIO` we might be tempted to stop +them by throwing an asynchronous exception to the process, but this is almost +certainly the wrong thing to do. Instead we might send a kind of poison pill, +which the process *ought* to handle by shutting down gracefully. Unfortunately +because of the asynchronous nature of sending, this is no good because `send` +will not fail under any circumstances. In fact, because `send` doesn't block, +we therefore have no way to no if the recipient existed at the time we sent the +poison pill. Even if the recipient did exist, we still have no guarantee that +the message we sent actually arrived - the network connection between the nodes +could have broken, for example. Making this *shutdown* protocol synchronous is +no good either - how long would we wait for a reply? Indefinitely? + +Exit signals come in two flavours - those that can +be caught and those that cannot. A call to +`exit :: (Serializable a) => ProcessId -> a -> Process ()` will dispatch an +exit signal to the specified process. These *signals* can be intercepted and +handled by the destination process however, so if you need to terminate the +process in a brutal way, you can use the `kill :: ProcessId -> String -> Process ()` +function, which sends an exit signal that cannot be handled. -Given a new node, there are two primitives for starting a new process. +------ +#### __An important note about exit signals__ + +Exit signals in Cloud Haskell are unlike asynchronous exceptions in regular +haskell code. Whilst processes *can* use asynchronous exceptions - there's +nothing stoping this since the `Process` monad is an instance of `MonadIO` - +exceptions thrown are not bound by the same ordering guarantees as messages +delivered to a process. Link failures and exit signals *might* be implemented +using asynchronous exceptions - that is the case in the current +implementation - but these are implemented in such a fashion that if you +send a message and *then* an exit signal, the message is guaranteed to arrive +first. + +You should avoid throwing your own exceptions in code where possible. Instead, +you should terminate yourself, or another process, using the built-in primitives +`exit`, `kill` and `die`. {% highlight haskell %} -forkProcess :: LocalNode -> Process () -> IO ProcessId -runProcess :: LocalNode -> Process () -> IO () +exit pid reason -- force `pid` to exit - reason can be any `Serializable` message +kill pid reason -- reason is a string - the *kill* signal cannot be caught +die reason -- as 'exit' but kills *us* {% endhighlight %} -The `runProcess` function blocks until the forked process has completed. +The `exit` and `kill` primitives do essentially the same thing, but catching +the specific exception thrown by `kill` is impossible, making `kill` an +*untrappable exit signal*. Of course you could trap **all** exceptions, but +you already know that's a very bad idea right!? + +The `exit` primitive is a little different. This provides support for trapping +exit signals in a generic way, so long as your *exit handler* is able to +recognise the underlying type of the 'exit reason'. This (reason for exiting) +is stored as a raw `Message`, so if your handler takes the appropriate type +as an input (and therefore the `Message` can be decoded and passed to the +handler) then the handler will run. This is pretty much the same approach as +exception handling using `Typeable`, except that we decide whether or not the +exception can be handled based on the type of `reason` instead of the type of +the exception itself. + +Calling `die` will immediately raise an exit signal (i.e., `ProcessExitException`) +in the calling process. + +---- ### Rethinking the Task Layer @@ -280,14 +373,14 @@ Haskell concurrency design patterns alongside. In fact, [distributed-process-platform][18] does not really consider the *task layer* in great detail. We provide an API comparable to remote's `Promise` in [Control.Distributed.Process.Platform.Async][17]. This API however, -is derived from Simon Marlow's [Control.Concurrent.Async][19] package, and does -not limit queries on `Async` handles in the same way as a `Promise` would. -Instead our [API][17] handles both blocking and non-blocking queries, polling +is derived from Simon Marlow's [Control.Concurrent.Async][19] package, and is not +limited to blocking queries on `Async` handles in the same way. Instead our +[API][17] handles both blocking and non-blocking queries, polling and working with lists of `Async` handles. We also eschew throwing exceptions -to indicate asynchronous task failures, instead handling *task* and -connectivity failures using monitors. Users of the API need only concern -themselves with the `AsyncResult`, which encodes the status and (possibly) -outcome of the computation simply. +to indicate asynchronous task failures, instead handling *task* and connectivity +failures using monitors. Users of the API need only concern themselves with the +`AsyncResult`, which encodes the status and (possibly) outcome of the computation +simply. ------ @@ -321,47 +414,96 @@ demoAsync = do ------ -Unlike remote's task layer, we do not exclude IO, allowing tasks to run in -the `Process` monad and execute arbitrary code. Providing a monadic wrapper -around `Async` that disallows side effects is relatively simple, and we -do not consider the presence of side effects a barrier to fault tolerance -and automated process restarts. A thin wrapper API that prevents side effects -in async tasks will be provided in a future release. +Unlike remote's task layer, we do not exclude IO here, allowing tasks to run +in the `Process` monad and execute arbitrary code. Providing a monadic wrapper +around `Async` that disallows side effects is relatively simple. -work is also underway to provide abstractions for managing asynchronous tasks +Work is also underway to provide abstractions for managing asynchronous tasks at a higher level, focussing on workload distribution and load regulation. -#### Fault Tolerance +The kinds of task that can be performed by the async implementations in +[distributed-process-platform][3] are limited only by their return type: +it **must** be `Serializable` - that much should've been obvious by now. +The type of asynchronous task definitions comes in two flavours, one for +local nodes which require no remote-table or static serialisation dictionary, +and another for tasks you wish to execute on remote nodes. + +{% highlight haskell %} +-- | A task to be performed asynchronously. +data AsyncTask a = + AsyncTask + { + asyncTask :: Process a -- ^ the task to be performed + } + | AsyncRemoteTask + { + asyncTaskDict :: Static (SerializableDict a) + -- ^ the serializable dict required to spawn a remote process + , asyncTaskNode :: NodeId + -- ^ the node on which to spawn the asynchronous task + , asyncTaskProc :: Closure (Process a) + -- ^ the task to be performed, wrapped in a closure environment + } +{% endhighlight %} -The [remote][14] task layer implementation imposes a *master-slave* roles on +The API for `Async` is fairly rich, so reading the haddocks is suggested. -* handles fault tolerance by drawing on the [OTP][13] concept of [supervision trees][15] +#### Managed Processes +Looking at *typed channels*, we noted that their insistence on a specific input +domain was more *haskell-ish* than working with bare send and receive primitives. +The `Async` sub-package also provides a type safe interface for receiving data, +although it is limited to running a computation and waiting for its result. -* does not dictate a data centric processing model, though this is supported -* treats promises/futures as a low level, enabling concept -* its APIs coordination patterns -* has more of a focus on rate limiting and overload protection +The [Control.Distributed.Processes.Platform.ManagedProcess][21] API provides a +number of different abstractions that can be used to achieve similar benefits +in your code. It works by introducing a standard protocol between your process +and the *world around*, which governs how to handle request/reply processing, +exit signals, timeouts, sleep/hibernation with `threadDelay` and even provides +hooks that terminating processes can use to clean up residual state. -When it comes to failure recovery, we defer to Erlang's approach for handling -process failures in a generic manner, by drawing on the [OTP][13] concept of -[supervision trees][15]. Erlang's [supervisor module][16] implements a process -which supervises other processes called child processes. The supervisor process -is responsible for starting, stopping, monitoring and even restarting its -child processes. A supervisors *children* can be either worker processes or -supervisors, which allows us to build hierarchical process structures (called -supervision trees in Erlang parlance). +The [API documentation][21] is quite extensive, so here we will simply point +out the obvious differences. A implemented implemented with `ManagedProcess` +can present a type safe API to its callers (and the server side code too!), +although that's not its primary benefit. For a very simplified example: -### Haskell's OTP +{% highlight haskell %} +add :: ProcessId -> Double -> Double -> Process Double +add sid x y = call sid (Add x y) + +divide :: ProcessId -> Double -> Double + -> Process (Either DivByZero Double) +divide sid x y = call sid (Divide x y ) + +launchMathServer :: Process ProcessId +launchMathServer = + let server = statelessProcess { + apiHandlers = [ + handleCall_ (\(Add x y) -> return (x + y)) + , handleCallIf_ (input (\(Divide _ y) -> y /= 0)) handleDivide + , handleCall_ (\(Divide _ _) -> divByZero) + ] + } + in spawnLocal $ start () (statelessInit Infinity) server >> return () + where handleDivide :: Divide -> Process (Either DivByZero Double) + handleDivide (Divide x y) = return $ Right $ x / y + + divByZero :: Process (Either DivByZero Double) + divByZero = return $ Left DivByZero +{% endhighlight %} -Erlang's is a set of libraries and *applications* -designed to help Erlang programmers build reliable systems. These libraries -build on the base primitives for distribution and concurrency, implementing -common patterns and providing skeleton implementations of standard architectural -patterns which are known to work well in practice. +Apart from the types and the imports, that is a complete definition. Whilst +it's not so obvious what's going on here, the key point is that the invocation +of `call` in the client facing API functions handles **all** of the relevant +waiting/blocking, converting the async result and so on. Note that the +*managed process* does not interact with its mailbox at all, but rather +just provides callback functions which take some state and either return a +new state and a reply, or just a new state. The process is *managed* in the +sense that its mailbox is under someone else's control. -The [distributed-process-platform][3] package is designed to meet similar goals, -building on the capabilities of [distributed-process][2]. +A slightly more complex example of the `ManagedProcess` API can be seen in +the [Managed Processes tutorial][22]. The API documentation is available +[here][21]. [1]: http://www.haskell.org/haskellwiki/Cloud_Haskell [2]: https://github.com/haskell-distributed/distributed-process @@ -383,3 +525,5 @@ building on the capabilities of [distributed-process][2]. [18]: https://github.com/haskell-distributed/distributed-process-platform [19]: http://hackage.haskell.org/package/async [20]: /wiki/networktransport.html +[21]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html +[22]: /tutorials/3.managedprocess.html diff --git a/index.md b/index.md index eaf7aaf7..045d862d 100644 --- a/index.md +++ b/index.md @@ -13,7 +13,7 @@ Generic network transport backends have been developed for [TCP](https://github.com/haskell-distibuted/distributed-process/network-transport-tcp) and [in-memory](https://github.com/haskell-distibuted/distributed-process/network-transport-inmemory) messaging, and several other implementations are available including a transport for -[Windows Azure](https://github.com/haskell-distibuted/distributed-process/network-transport-azure). The [Overview](https://github.com/haskell-distibuted/distributed-process/wiki/Overiview) provides links to a number of resources for learning about the conceptual underpinnings of Cloud Haskell, and some [examples](https://github.com/haskell-distibuted/distributed-process/distributed-process-demos). +[Windows Azure](https://github.com/haskell-distibuted/distributed-process/distributed-process-azure). The [Overview](https://github.com/haskell-distibuted/distributed-process/wiki/Overiview) provides links to a number of resources for learning about the conceptual underpinnings of Cloud Haskell, and some [examples](https://github.com/haskell-distibuted/distributed-process/distributed-process-demos). Documentation is available on this site for HEAD, or [hackage](http://hackage.haskell.org/package/distributed-process) for the current and preceding versions of diff --git a/tutorials/1.tutorial.md b/tutorials/1.tutorial.md index dca17cc6..1c30b612 100644 --- a/tutorials/1.tutorial.md +++ b/tutorials/1.tutorial.md @@ -23,7 +23,7 @@ the network-transport-tcp backend, but the simplelocalnet or inmemory backends are also available on github, along with some other experimental options. -### Creating Nodes +### Create a node Cloud Haskell's *lightweight processes* reside on a 'node', which must be initialised with a network transport implementation and a remote table. @@ -46,7 +46,7 @@ main = do And now we have a running node. -### Messages +### Send messages We can start a new lightweight process with `forkProcess`, which takes a node, a `Process` action - because our concurrent code will run in the `Process` @@ -101,7 +101,7 @@ main = do {% endhighlight %} Note that we've used a `receive` class of function this time around. The `match` -construct allows you to construct a list of potential message handling code and +construct allows you to construct a list of potential message handlers and have them evaluated against incoming messages. The first match indicates that, given a tuple `t :: (ProcessId, String)` that we will send the `String` component back to the sender's `ProcessId`. The second match prints out whatever string it @@ -117,61 +117,6 @@ Processes can send data if the type implements the `Serializable` typeclass, whi done indirectly by implementing `Binary` and deriving `Typeable`. Implementations are already provided for primitives and some commonly used data structures. -### Typed Channels - -Channels provides an alternative to message transmission with `send` and `expect`. -While `send` and `expect` allow transmission of messages of any `Serializable` -type, channels require a uniform type. Channels work like a distributed equivalent -of Haskell's `Control.Concurrent.Chan`, however they have distinct ends: a single -receiving port and a corollary send port. - -Channels provide a nice alternative to *bare send and receive*, which is a bit -*unHaskellish*, because the processes message queue has messages of multiple -types, and we have to do dynamic type checking. - -We create channels with a call to `newChan`, and send/receive on them using the -`{send,receive}Chan` primitives: - -{% highlight haskell %} -channelsDemo :: Process () -channelsDemo = do - (sp, rp) <- newChan :: Process (SendPort String, ReceivePort String) - - -- send on a channel - spawnLocal $ sendChan sp "hello!" - - -- receive on a channel - m <- receiveChan rp - say $ show m -{% endhighlight %} - -Channels are particularly useful when you are sending a message that needs a -response, because the code that receives the response knows exactly where it -came from. Channels can sometimes allows message types to be simplified, as -passing a `ProcessId` to reply to isn't required. Channels are not so useful -when you need to spawn a process and then send a bunch a messages to it and -wait for replies, because we can’t send the `ReceivePort`. - -ReceivePorts can be merged, so you can listen on several simultaneously. In the -latest version of [distributed-process][2], you can listen for *regular* messages -and on multiple channels at the same time, using `matchChan` in the list of -allowed matches passed `receive`. - -The [Control.Distributed.Process.Platform.Async][3] API provides an alternative -type safe mechanism for receiving data in request/reply scenarios. This relies -on spawning insulating processes and using either channels or STM internally, -so whilst it provides a neat API, there are some overheads involved. The -`ManagedProcess` API uses this mechanism to great effect for dealing with -client/server style interactions. See the [ManagedProcess.Client][4] APIs and -platform [documentation](/documentation.html) for further details. - -### Linking and monitoring - -Processes can be linked to other processes, nodes or channels. Links are unidirectional, -and guarantee that once the linked object *dies*, the linked process will also be -terminated. Monitors do not cause the *listening* process to exit, but rather they -put a `ProcessMonitorNotification` into the process' mailbox. - ### Spawning Remote Processes In order to spawn a process on a node we need something of type `Closure (Process ())`. @@ -218,74 +163,6 @@ main = do -- etc {% endhighlight %} -### Stopping Processes - -Some processes, like the *outer* process in the previous example, will run until -they've completed and then return their value. This is just as we find with IO action, -and there is an instance of `MonadIO` for the `Process` monad, so you can `liftIO` if you -need to evaluate IO actions. - -Because processes are implemented with `forkIO` we might be tempted to stop -them by throwing an asynchronous exception to the process, but this is almost -certainly the wrong thing to do. Instead we might send a kind of poison pill, -which the process *ought* to handle by shutting down gracefully. Unfortunately -because of the asynchronous nature of sending, this is no good because `send` -will not fail under any circumstances. In fact, because `send` doesn't block, -we therefore have no way to no if the recipient existed at the time we sent the -poison pill. Even if the recipient did exist, we still have no guarantee that -the message we sent actually arrived - the network connection between the nodes -could have broken, for example. Making this *shutdown* protocol synchronous is -no good either - how long would we wait for a reply? Indefinitely? - -Exit signals come in two flavours - those that can -be caught and those that cannot. A call to -`exit :: (Serializable a) => ProcessId -> a -> Process ()` will dispatch an -exit signal to the specified process. These *signals* can be intercepted and -handled by the destination process however, so if you need to terminate the -process in a brutal way, you can use the `kill :: ProcessId -> String -> Process ()` -function, which sends an exit signal that cannot be handled. - ------- -#### __An important note about exit signals__ - -Exit signals in Cloud Haskell are unlike asynchronous exceptions in regular -haskell code. Whilst processes *can* use asynchronous exceptions - there's -nothing stoping this since the `Process` monad is an instance of `MonadIO` - -exceptions thrown are not bound by the same ordering guarantees as messages -delivered to a process. Link failures and exit signals *might* be implemented -using asynchronous exceptions - that is the case in the current -implementation - but these are implemented in such a fashion that if you -send a message and *then* an exit signal, the message is guaranteed to arrive -first. - -You should avoid throwing your own exceptions in code where possible. Instead, -you should terminate yourself, or another process, using the built-in primitives -`exit`, `kill` and `die`. - -{% highlight haskell %} -exit pid reason -- force `pid` to exit - reason can be any `Serializable` message -kill pid reason -- reason is a string - the *kill* signal cannot be caught -die reason -- as 'exit' but kills *us* -{% endhighlight %} - -The `exit` and `kill` primitives do essentially the same thing, but catching -the specific exception thrown by `kill` is impossible, making `kill` an -*untrappable exit signal*. Of course you could trap **all** exceptions, but -you already know that's a very bad idea right!? - -The `exit` primitive is a little different. This provides support for trapping -exit signals in a generic way, so long as your *exit handler* is able to -recognise the underlying type of the 'exit reason'. This (reason for exiting) -is stored as a raw `Message`, so if your handler takes the appropriate type -as an input (and therefore the `Message` can be decoded and passed to the -handler) then the handler will run. This is pretty much the same approach as -exception handling using `Typeable`, except that we decide whether or not the -exception can be handled based on the type of `reason` instead of the type of -the exception itself. - -Calling `die` will immediately raise an exit signal (i.e., `ProcessExitException`) -in the calling process. - ------ [1]: /static/doc/distributed-process/Control-Distributed-Process.html#v:Message diff --git a/wiki/faq.md b/wiki/faq.md new file mode 100644 index 00000000..11e87671 --- /dev/null +++ b/wiki/faq.md @@ -0,0 +1,33 @@ +--- +layout: wiki +title: FAQ +wiki: FAQ +--- + +### FAQ + +#### Do I need to install a network-transport backend? + +Yes. The `Network.Transport` component provides only the API - an actual +backend that implements this will be required in order to start a CH node. + +#### What guarantees are there for message ordering, sending, etc + +Take a look at the formal semantics for answers to *all* such questions. +They're actually quite pithy and readable, and fairly honest about where +there are still gaps. + +You can find them via the *resources* tab just above this page. + +#### Will I have to register a Jira account to submit issues? + +Yes, you will need to provide a name and email address. + +#### Why are you using Jira instead of Github Issues? It seems more complicated. + +Jira **is** a bit more complicated than github's bug tracker, and it's certainly +not perfect. It is, however, a lot better suited to managing and planning a +project of this size. Cloud Haskell consists of no less than **13** individual +projects at this time, and that's not to mention some of the experimental ones +that have been developed by community members and *might* end up being absorbed +by the team. diff --git a/wiki/help.md b/wiki/help.md new file mode 100644 index 00000000..f51fc8f7 --- /dev/null +++ b/wiki/help.md @@ -0,0 +1,23 @@ +--- +layout: wiki +title: Getting Help +wiki: Help +--- + +### Help + +If the documentation doesn't answer your question, queries about Cloud Haskell +can be directed to the Parallel Haskell Mailing List +(parallel-haskell@googlegroups.com), which is pretty active. If you think +you've found a bug, or would like to request a new feature, please visit the +[Jira Issue Tracker](https://cloud-haskell.atlassian.net) and submit a bug. +You **will** need to register with your email address to create new issues, +though you can freely browse the existing tickets without doing so. + + +### Getting Involved + +If you're interested in hacking Cloud Haskell then please read the +[Contributing](/wiki/contributing.html) wiki page. Additional help can be obtained through the +[Developers Forum/Mailing List](https://groups.google.com/forum/?fromgroups=#!forum/cloud-haskell-developers) +or Parallel Haskell mailing list. diff --git a/wiki/reliability.md b/wiki/reliability.md new file mode 100644 index 00000000..d9cae3d6 --- /dev/null +++ b/wiki/reliability.md @@ -0,0 +1,55 @@ +--- +layout: wiki +title: Fault Tolerance +wiki: reliability +--- + +### reliability + +We do not consider the presence of side effects a barrier to fault tolerance +and automated process restarts. We **do** recognise that it's easier to +reason about restarting remote processes if they're stateless, and so we +provide a wrapper for the `ManagedProcess` API that ensures all user defined +callbacks are side effect free. + +The choice, about whether or not it is safe to restart a process that *might* +produce side effects, is left to the user. The `ManagedProcess` API provides +explicit support for evaluating user defined callbacks when a process has +decided (for some reason) to shut down. We also give the user options about +how to initialise and/or re-initialise a process that has been previously +terminated. + +When it comes to failure recovery, we defer to Erlang's approach for handling +process failures in a generic manner, by drawing on the [OTP][13] concept of +[supervision trees][15]. Erlang's [supervisor module][16] implements a process +which supervises other processes called child processes. The supervisor process +is responsible for starting, stopping, monitoring and even restarting its +child processes. A supervisors *children* can be either worker processes or +supervisors, which allows us to build hierarchical process structures (called +supervision trees in Erlang parlance). + +The supervision APIs are a work in progress. + +[1]: http://www.haskell.org/haskellwiki/Cloud_Haskell +[2]: https://github.com/haskell-distributed/distributed-process +[3]: https://github.com/haskell-distributed/distributed-process-platform +[4]: http://hackage.haskell.org/package/distributed-static +[5]: http://hackage.haskell.org/package/rank1dynamic +[6]: http://hackage.haskell.org/packages/network-transport +[7]: http://hackage.haskell.org/packages/network-transport-tcp +[8]: https://github.com/haskell-distributed/distributed-process/network-transport-inmemory +[9]: https://github.com/haskell-distributed/distributed-process/network-transport-composed +[10]: http://hackage.haskell.org/package/distributed-process-simplelocalnet +[11]: http://hackage.haskell.org/package/distributed-process-azure +[12]: http://research.microsoft.com/en-us/um/people/simonpj/papers/parallel/remote.pdf +[13]: http://en.wikipedia.org/wiki/Open_Telecom_Platform +[14]: http://hackage.haskell.org/packages/remote +[15]: http://www.erlang.org/doc/design_principles/sup_princ.html +[16]: http://www.erlang.org/doc/man/supervisor.html +[17]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html +[18]: https://github.com/haskell-distributed/distributed-process-platform +[19]: http://hackage.haskell.org/package/async +[20]: /wiki/networktransport.html +[21]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html +[22]: /tutorials/3.managedprocess.html + From e5d18d611490557eb70a67075dd6e020d359e87e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 19:43:39 +0000 Subject: [PATCH 0907/2357] adjust the docs for Async/Task --- documentation.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/documentation.md b/documentation.md index 216dc5dc..fa5dc47f 100644 --- a/documentation.md +++ b/documentation.md @@ -278,15 +278,15 @@ defined by Erlang's [Open Telecom Platform][13], taking in some well established Haskell concurrency design patterns alongside. In fact, [distributed-process-platform][18] does not really consider the -*task layer* in great detail. We provide an API comparable to remote's +*task layer* in great detail. We do provide an API comparable to remote's `Promise` in [Control.Distributed.Process.Platform.Async][17]. This API however, is derived from Simon Marlow's [Control.Concurrent.Async][19] package, and does -not limit queries on `Async` handles in the same way as a `Promise` would. -Instead our [API][17] handles both blocking and non-blocking queries, polling -and working with lists of `Async` handles. We also eschew throwing exceptions -to indicate asynchronous task failures, instead handling *task* and -connectivity failures using monitors. Users of the API need only concern -themselves with the `AsyncResult`, which encodes the status and (possibly) +not limit queries on `Async` handles to blocking receive semantics, in the same +way as `Promise` does. Instead our [API][17] handles both blocking and non-blocking +queries, polling and working with lists of `Async` handles. We also eschew +throwing exceptions to indicate asynchronous task failures, instead handling +*task* and connectivity failures using monitors. Users of the API need only +concern themselves with the `AsyncResult`, which encodes the status and (possibly) outcome of the computation simply. ------ @@ -325,19 +325,24 @@ Unlike remote's task layer, we do not exclude IO, allowing tasks to run in the `Process` monad and execute arbitrary code. Providing a monadic wrapper around `Async` that disallows side effects is relatively simple, and we do not consider the presence of side effects a barrier to fault tolerance -and automated process restarts. A thin wrapper API that prevents side effects -in async tasks will be provided in a future release. +and automated process restarts. Erlang does not forbid *IO* in its processes, +and yet that doesn't render supervision trees ineffective. They key is to +provide a rich enough API that statefull processes can recognise whether or +not they need to provide idempotent initialisation routines. -work is also underway to provide abstractions for managing asynchronous tasks -at a higher level, focussing on workload distribution and load regulation. +The utility of preventing side effects using the type system is, however, not +to be sniffed at. A substrate of the `ManagedProcess` API is under development +that provides a *safe process* abstraction in which side effect free computations +can be embedded, whilst reaping the benefits of the framework. #### Fault Tolerance The [remote][14] task layer implementation imposes a *master-slave* roles on +its users. We eschew this overlay in favour of letting the Cloud Haskell +backend decide on whether such roles and appropriate to the given application +topology. * handles fault tolerance by drawing on the [OTP][13] concept of [supervision trees][15] - - * does not dictate a data centric processing model, though this is supported * treats promises/futures as a low level, enabling concept * its APIs coordination patterns From a62a9ffea8de1cd208b40047f0800dc9e1e7d2b1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 20:10:37 +0000 Subject: [PATCH 0908/2357] Add monitorAsync API and refactor SimplePool Not only can the SimplePool test/example use monitors instead of a process identifier, it can be simplified as 'bump' will never run with 'slots > 1' --- src/Control/Distributed/Process/Async.hs | 5 +++ tests/SimplePool.hs | 48 ++++++++++-------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Control/Distributed/Process/Async.hs b/src/Control/Distributed/Process/Async.hs index e7656c78..2ce898a7 100644 --- a/src/Control/Distributed/Process/Async.hs +++ b/src/Control/Distributed/Process/Async.hs @@ -48,6 +48,7 @@ module Control.Distributed.Process.Platform.Async , asyncLinkedChan , task , remoteTask + , monitorAsync -- * Cancelling asynchronous operations , cancel , cancelWait @@ -137,6 +138,10 @@ remoteTask :: Static (SerializableDict a) -> AsyncTask a remoteTask = AsyncRemoteTask +-- | Given an 'Async' handle, monitor the worker process. +monitorAsync :: Async a -> Process MonitorRef +monitorAsync = monitor . asyncWorker + -- | Check whether an 'Async' handle has completed yet. The status of the -- action is encoded in the returned 'AsyncResult'. If the action has not -- completed, the result will be 'AsyncPending', or one of the other diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 1c9f4cbb..78fa1aa7 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -32,7 +32,7 @@ type SimpleTask a = Closure (Process a) data Pool a = Pool { poolSize :: PoolSize - , active :: [(ProcessId, Recipient, Async a)] + , active :: [(MonitorRef, Recipient, Async a)] , accepted :: [(Recipient, Closure (Process a))] } deriving (Typeable) @@ -89,9 +89,8 @@ acceptTask s@(Pool sz' runQueue taskQueue) from task' = False -> do proc <- unClosure task' asyncHandle <- async proc - pid <- return $ asyncWorker asyncHandle - taskEntry <- return (pid, from, asyncHandle) - _ <- monitor pid + ref <- monitorAsync asyncHandle + taskEntry <- return (ref, from, asyncHandle) return s { active = (taskEntry:runQueue) } -- /info/ handler: a worker has exited, process the AsyncResult and send a reply @@ -101,8 +100,8 @@ taskComplete :: forall a . Serializable a -> ProcessMonitorNotification -> Process (ProcessAction (Pool a)) taskComplete s@(Pool _ runQ _) - (ProcessMonitorNotification _ pid _) = - let worker = findWorker pid runQ in + (ProcessMonitorNotification ref _ _) = + let worker = findWorker ref runQ in case worker of Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue Nothing -> continue s @@ -116,27 +115,20 @@ taskComplete s@(Pool _ runQ _) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - bump :: Pool a -> (ProcessId, Recipient, Async a) -> Process (Pool a) - bump st@(Pool maxSz runQueue _) worker = - let runLen = (length runQueue) - 1 - runQ2 = deleteFromRunQueue worker runQueue - slots = (maxSz - runLen) - in fillSlots slots st { active = runQ2 } - - fillSlots :: Int -> Pool a -> Process (Pool a) - fillSlots _ st'@(Pool _ _ []) = return st' - fillSlots 0 st' = return st' - fillSlots n st'@(Pool _ _ ((tr,tc):ts)) = - let ns = st' { accepted = ts } - in acceptTask ns tr tc >>= fillSlots (n-1) - -findWorker :: ProcessId - -> [(ProcessId, Recipient, Async a)] - -> Maybe (ProcessId, Recipient, Async a) -findWorker key = find (\(pid,_,_) -> pid == key) - -deleteFromRunQueue :: (ProcessId, Recipient, Async a) - -> [(ProcessId, Recipient, Async a)] - -> [(ProcessId, Recipient, Async a)] + bump :: Pool a -> (MonitorRef, Recipient, Async a) -> Process (Pool a) + bump st@(Pool _ runQueue acc) worker = + let runQ2 = deleteFromRunQueue worker runQueue in + case acc of + [] -> return st { active = runQ2 } + ((tr,tc):ts) -> acceptTask (st { accepted = ts, active = runQ2 }) tr tc + +findWorker :: MonitorRef + -> [(MonitorRef, Recipient, Async a)] + -> Maybe (MonitorRef, Recipient, Async a) +findWorker key = find (\(ref,_,_) -> ref == key) + +deleteFromRunQueue :: (MonitorRef, Recipient, Async a) + -> [(MonitorRef, Recipient, Async a)] + -> [(MonitorRef, Recipient, Async a)] deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ From eeeac337189ef7e642e89795414b0b83e26d1d66 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 20:10:37 +0000 Subject: [PATCH 0909/2357] Add monitorAsync API and refactor SimplePool Not only can the SimplePool test/example use monitors instead of a process identifier, it can be simplified as 'bump' will never run with 'slots > 1' --- tests/SimplePool.hs | 48 +++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 1c9f4cbb..78fa1aa7 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -32,7 +32,7 @@ type SimpleTask a = Closure (Process a) data Pool a = Pool { poolSize :: PoolSize - , active :: [(ProcessId, Recipient, Async a)] + , active :: [(MonitorRef, Recipient, Async a)] , accepted :: [(Recipient, Closure (Process a))] } deriving (Typeable) @@ -89,9 +89,8 @@ acceptTask s@(Pool sz' runQueue taskQueue) from task' = False -> do proc <- unClosure task' asyncHandle <- async proc - pid <- return $ asyncWorker asyncHandle - taskEntry <- return (pid, from, asyncHandle) - _ <- monitor pid + ref <- monitorAsync asyncHandle + taskEntry <- return (ref, from, asyncHandle) return s { active = (taskEntry:runQueue) } -- /info/ handler: a worker has exited, process the AsyncResult and send a reply @@ -101,8 +100,8 @@ taskComplete :: forall a . Serializable a -> ProcessMonitorNotification -> Process (ProcessAction (Pool a)) taskComplete s@(Pool _ runQ _) - (ProcessMonitorNotification _ pid _) = - let worker = findWorker pid runQ in + (ProcessMonitorNotification ref _ _) = + let worker = findWorker ref runQ in case worker of Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue Nothing -> continue s @@ -116,27 +115,20 @@ taskComplete s@(Pool _ runQ _) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - bump :: Pool a -> (ProcessId, Recipient, Async a) -> Process (Pool a) - bump st@(Pool maxSz runQueue _) worker = - let runLen = (length runQueue) - 1 - runQ2 = deleteFromRunQueue worker runQueue - slots = (maxSz - runLen) - in fillSlots slots st { active = runQ2 } - - fillSlots :: Int -> Pool a -> Process (Pool a) - fillSlots _ st'@(Pool _ _ []) = return st' - fillSlots 0 st' = return st' - fillSlots n st'@(Pool _ _ ((tr,tc):ts)) = - let ns = st' { accepted = ts } - in acceptTask ns tr tc >>= fillSlots (n-1) - -findWorker :: ProcessId - -> [(ProcessId, Recipient, Async a)] - -> Maybe (ProcessId, Recipient, Async a) -findWorker key = find (\(pid,_,_) -> pid == key) - -deleteFromRunQueue :: (ProcessId, Recipient, Async a) - -> [(ProcessId, Recipient, Async a)] - -> [(ProcessId, Recipient, Async a)] + bump :: Pool a -> (MonitorRef, Recipient, Async a) -> Process (Pool a) + bump st@(Pool _ runQueue acc) worker = + let runQ2 = deleteFromRunQueue worker runQueue in + case acc of + [] -> return st { active = runQ2 } + ((tr,tc):ts) -> acceptTask (st { accepted = ts, active = runQ2 }) tr tc + +findWorker :: MonitorRef + -> [(MonitorRef, Recipient, Async a)] + -> Maybe (MonitorRef, Recipient, Async a) +findWorker key = find (\(ref,_,_) -> ref == key) + +deleteFromRunQueue :: (MonitorRef, Recipient, Async a) + -> [(MonitorRef, Recipient, Async a)] + -> [(MonitorRef, Recipient, Async a)] deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ From 0e5d6e0465bae3581900c785d21aa324fc7e492d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 20:10:37 +0000 Subject: [PATCH 0910/2357] Add monitorAsync API and refactor SimplePool Not only can the SimplePool test/example use monitors instead of a process identifier, it can be simplified as 'bump' will never run with 'slots > 1' --- tests/SimplePool.hs | 48 +++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 1c9f4cbb..78fa1aa7 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -32,7 +32,7 @@ type SimpleTask a = Closure (Process a) data Pool a = Pool { poolSize :: PoolSize - , active :: [(ProcessId, Recipient, Async a)] + , active :: [(MonitorRef, Recipient, Async a)] , accepted :: [(Recipient, Closure (Process a))] } deriving (Typeable) @@ -89,9 +89,8 @@ acceptTask s@(Pool sz' runQueue taskQueue) from task' = False -> do proc <- unClosure task' asyncHandle <- async proc - pid <- return $ asyncWorker asyncHandle - taskEntry <- return (pid, from, asyncHandle) - _ <- monitor pid + ref <- monitorAsync asyncHandle + taskEntry <- return (ref, from, asyncHandle) return s { active = (taskEntry:runQueue) } -- /info/ handler: a worker has exited, process the AsyncResult and send a reply @@ -101,8 +100,8 @@ taskComplete :: forall a . Serializable a -> ProcessMonitorNotification -> Process (ProcessAction (Pool a)) taskComplete s@(Pool _ runQ _) - (ProcessMonitorNotification _ pid _) = - let worker = findWorker pid runQ in + (ProcessMonitorNotification ref _ _) = + let worker = findWorker ref runQ in case worker of Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue Nothing -> continue s @@ -116,27 +115,20 @@ taskComplete s@(Pool _ runQ _) respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) respond _ _ = die $ TerminateOther "IllegalState" - bump :: Pool a -> (ProcessId, Recipient, Async a) -> Process (Pool a) - bump st@(Pool maxSz runQueue _) worker = - let runLen = (length runQueue) - 1 - runQ2 = deleteFromRunQueue worker runQueue - slots = (maxSz - runLen) - in fillSlots slots st { active = runQ2 } - - fillSlots :: Int -> Pool a -> Process (Pool a) - fillSlots _ st'@(Pool _ _ []) = return st' - fillSlots 0 st' = return st' - fillSlots n st'@(Pool _ _ ((tr,tc):ts)) = - let ns = st' { accepted = ts } - in acceptTask ns tr tc >>= fillSlots (n-1) - -findWorker :: ProcessId - -> [(ProcessId, Recipient, Async a)] - -> Maybe (ProcessId, Recipient, Async a) -findWorker key = find (\(pid,_,_) -> pid == key) - -deleteFromRunQueue :: (ProcessId, Recipient, Async a) - -> [(ProcessId, Recipient, Async a)] - -> [(ProcessId, Recipient, Async a)] + bump :: Pool a -> (MonitorRef, Recipient, Async a) -> Process (Pool a) + bump st@(Pool _ runQueue acc) worker = + let runQ2 = deleteFromRunQueue worker runQueue in + case acc of + [] -> return st { active = runQ2 } + ((tr,tc):ts) -> acceptTask (st { accepted = ts, active = runQ2 }) tr tc + +findWorker :: MonitorRef + -> [(MonitorRef, Recipient, Async a)] + -> Maybe (MonitorRef, Recipient, Async a) +findWorker key = find (\(ref,_,_) -> ref == key) + +deleteFromRunQueue :: (MonitorRef, Recipient, Async a) + -> [(MonitorRef, Recipient, Async a)] + -> [(MonitorRef, Recipient, Async a)] deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ From 3e9e4feef6595b9faacd3f2dd6c7b2126896ba98 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 21:25:25 +0000 Subject: [PATCH 0911/2357] add initial managed process tutorial skeleton --- _layouts/managedprocess.html | 36 +++++++++++++++++++++++++++++++++++ documentation.md | 4 ++-- tutorials/3.managedprocess.md | 32 +++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 _layouts/managedprocess.html create mode 100644 tutorials/3.managedprocess.md diff --git a/_layouts/managedprocess.html b/_layouts/managedprocess.html new file mode 100644 index 00000000..9e7bd435 --- /dev/null +++ b/_layouts/managedprocess.html @@ -0,0 +1,36 @@ + + + + {% include head.html %} + + + + + {% include nav.html %} +
    + +
    +
    +
    + +
    + {{ content }} +
    +
    +
    + {% include footer.html %} + {% include js.html %} + + diff --git a/documentation.md b/documentation.md index 2f106202..a9051b90 100644 --- a/documentation.md +++ b/documentation.md @@ -511,8 +511,8 @@ just provides callback functions which take some state and either return a new state and a reply, or just a new state. The process is *managed* in the sense that its mailbox is under someone else's control. -A slightly more complex example of the `ManagedProcess` API can be seen in -the [Managed Processes tutorial][22]. The API documentation is available +More complex examples of the `ManagedProcess` API can be seen in the +[Managed Processes tutorial][22]. API documentation for HEAD is available [here][21]. [1]: http://www.haskell.org/haskellwiki/Cloud_Haskell diff --git a/tutorials/3.managedprocess.md b/tutorials/3.managedprocess.md new file mode 100644 index 00000000..66c84734 --- /dev/null +++ b/tutorials/3.managedprocess.md @@ -0,0 +1,32 @@ +--- +layout: managedprocess +categories: tutorial +title: Managed Process Tutorial +--- + +### Introduction + +In order to explore the `ManagedProcess` API, we will present a simple +example taken from the test suite, which exercises some of the more +interesting features. + +Let's imagine we want to execute tasks on an arbitrary node, using a +mechanism much as we would with the `call` API from distributed-process. +As with `call`, we want the caller to block whilst the remote task is +executing, but we also want to put an upper bound on the number of +concurrent tasks. We will use `ManagedProcess` to implement a generic +task server with the following characteristics + +* requests to enqueue a task are handled immediately +* callers will block until the task completes (or fails) +* an upper bound is placed on the number of concurrent running tasks + +Once the upper bound is reached, tasks will be queued up for later +execution, and only when we drop below the limit will tasks be taken +from the backlog and executed. + +`ManagedProcess` provides a basic protocol for *server-like* processes +such as this, based on the synchronous `call` and asynchronous `cast` +functions. Although `call` is synchronous, communication with the +*server process* is out of band, both from the client and the server's +point of view. From 49aef21447e05be89c9c2b82c27bcb3242050339 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 21:35:20 +0000 Subject: [PATCH 0912/2357] add to the managed process tutorial --- tutorials/3.managedprocess.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tutorials/3.managedprocess.md b/tutorials/3.managedprocess.md index 66c84734..c01c3f81 100644 --- a/tutorials/3.managedprocess.md +++ b/tutorials/3.managedprocess.md @@ -10,6 +10,12 @@ In order to explore the `ManagedProcess` API, we will present a simple example taken from the test suite, which exercises some of the more interesting features. +The main idea behind `ManagedProcess` is to separate the functional +and non-functional aspects of a process. By functional, we mean whatever +application specific task the process performs, and by non-functional +we mean the *concurrency* or, more precisely, handling of the process' +mailbox. + Let's imagine we want to execute tasks on an arbitrary node, using a mechanism much as we would with the `call` API from distributed-process. As with `call`, we want the caller to block whilst the remote task is @@ -29,4 +35,21 @@ from the backlog and executed. such as this, based on the synchronous `call` and asynchronous `cast` functions. Although `call` is synchronous, communication with the *server process* is out of band, both from the client and the server's -point of view. +point of view. The server implementation chooses whether to reply to +a call request immediately, or defer its reply until a later stage +and go back to receiving messages in the meanwhile. + +### Implementation Sketch + +We start out with some types: the tasks we perform and the maximum +pool size: + +{% highlight haskell %} +type PoolSize = Int +type SimpleTask a = Closure (Process a) +{% endhighlight %} + +To submit a task, our clients will submit an action in the process +monad, wrapped in a `Closure` environment. We will use the `Addressable` +typeclass to allow clients to specify the server's location in whatever +manner suits them. From bc6ee1d8d0fbd9b9d202038198ed653c2e75dcf8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 21:35:44 +0000 Subject: [PATCH 0913/2357] update simple pool exampe --- distributed-process-platform.cabal | 3 +- tests/SimplePool.hs | 44 +++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 438678d1..4c01c7dc 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -150,7 +150,8 @@ test-suite GenServerTests stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.9, test-framework-hunit, - transformers + transformers, + ghc-prim hs-source-dirs: src, tests diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 78fa1aa7..6deb2086 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -1,8 +1,8 @@ -{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DeriveGeneric #-} -- | Simple bounded (size) worker pool that accepts tasks and blocks -- the caller until they've completed. Partly a /spike/ for that 'Task' API @@ -18,18 +18,33 @@ import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable import Control.Exception hiding (catch) -import Data.Binary() +import Data.Binary import Data.List ( deleteBy , find ) import Data.Typeable +import GHC.Generics (Generic) + import Prelude hiding (catch) type PoolSize = Int type SimpleTask a = Closure (Process a) +data GetStats = GetStats + deriving (Typeable, Generic) + +instance Binary GetStats + +data PoolStats = PoolStats { + maxJobs :: Int + , activeJobs :: Int + , queuedJobs :: Int + } deriving (Typeable, Generic) + +instance Binary PoolStats + data Pool a = Pool { poolSize :: PoolSize , active :: [(MonitorRef, Recipient, Async a)] @@ -40,7 +55,8 @@ poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) poolServer = defaultProcess { apiHandlers = [ - handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + , handleCall poolStatsRequest ] , infoHandlers = [ handleInfo taskComplete @@ -52,22 +68,30 @@ simplePool :: forall a . (Serializable a) => PoolSize -> ProcessDefinition (Pool a) -> Process (Either (InitResult (Pool a)) TerminateReason) -simplePool sz server = - start sz init' server - `catch` (\(e :: SomeException) -> do - say $ "terminating with " ++ (show e) - liftIO $ throwIO e) +simplePool sz server = start sz init' server where init' :: PoolSize -> Process (InitResult (Pool a)) init' sz' = return $ InitOk (Pool sz' [] []) Infinity -- enqueues the task in the pool and blocks -- the caller until the task is complete -executeTask :: Serializable a - => ProcessId +executeTask :: forall s a . (Addressable s, Serializable a) + => s -> Closure (Process a) -> Process (Either String a) executeTask sid t = call sid t +-- internal / server-side API + +poolStatsRequest :: (Serializable a) + => Pool a + -> GetStats + -> Process (ProcessReply (Pool a) PoolStats) +poolStatsRequest st GetStats = + let sz = poolSize st + ac = length (active st) + pj = length (accepted st) + in reply (PoolStats sz ac pj) st + -- /call/ handler: accept a task and defer responding until "later" storeTask :: Serializable a => Pool a From c4267719a21a0fd2568f79dd5948e548cec88ea5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 21:35:44 +0000 Subject: [PATCH 0914/2357] update simple pool exampe --- distributed-process-platform.cabal | 3 +- tests/SimplePool.hs | 44 +++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 438678d1..4c01c7dc 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -150,7 +150,8 @@ test-suite GenServerTests stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.9, test-framework-hunit, - transformers + transformers, + ghc-prim hs-source-dirs: src, tests diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 78fa1aa7..6deb2086 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -1,8 +1,8 @@ -{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DeriveGeneric #-} -- | Simple bounded (size) worker pool that accepts tasks and blocks -- the caller until they've completed. Partly a /spike/ for that 'Task' API @@ -18,18 +18,33 @@ import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable import Control.Exception hiding (catch) -import Data.Binary() +import Data.Binary import Data.List ( deleteBy , find ) import Data.Typeable +import GHC.Generics (Generic) + import Prelude hiding (catch) type PoolSize = Int type SimpleTask a = Closure (Process a) +data GetStats = GetStats + deriving (Typeable, Generic) + +instance Binary GetStats + +data PoolStats = PoolStats { + maxJobs :: Int + , activeJobs :: Int + , queuedJobs :: Int + } deriving (Typeable, Generic) + +instance Binary PoolStats + data Pool a = Pool { poolSize :: PoolSize , active :: [(MonitorRef, Recipient, Async a)] @@ -40,7 +55,8 @@ poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) poolServer = defaultProcess { apiHandlers = [ - handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + , handleCall poolStatsRequest ] , infoHandlers = [ handleInfo taskComplete @@ -52,22 +68,30 @@ simplePool :: forall a . (Serializable a) => PoolSize -> ProcessDefinition (Pool a) -> Process (Either (InitResult (Pool a)) TerminateReason) -simplePool sz server = - start sz init' server - `catch` (\(e :: SomeException) -> do - say $ "terminating with " ++ (show e) - liftIO $ throwIO e) +simplePool sz server = start sz init' server where init' :: PoolSize -> Process (InitResult (Pool a)) init' sz' = return $ InitOk (Pool sz' [] []) Infinity -- enqueues the task in the pool and blocks -- the caller until the task is complete -executeTask :: Serializable a - => ProcessId +executeTask :: forall s a . (Addressable s, Serializable a) + => s -> Closure (Process a) -> Process (Either String a) executeTask sid t = call sid t +-- internal / server-side API + +poolStatsRequest :: (Serializable a) + => Pool a + -> GetStats + -> Process (ProcessReply (Pool a) PoolStats) +poolStatsRequest st GetStats = + let sz = poolSize st + ac = length (active st) + pj = length (accepted st) + in reply (PoolStats sz ac pj) st + -- /call/ handler: accept a task and defer responding until "later" storeTask :: Serializable a => Pool a From 658bc1b9d7130db675aea5f57c14a5bb84e14f8b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 21:35:44 +0000 Subject: [PATCH 0915/2357] update simple pool exampe --- distributed-process-platform.cabal | 3 +- tests/SimplePool.hs | 44 +++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 438678d1..4c01c7dc 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -150,7 +150,8 @@ test-suite GenServerTests stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.9, test-framework-hunit, - transformers + transformers, + ghc-prim hs-source-dirs: src, tests diff --git a/tests/SimplePool.hs b/tests/SimplePool.hs index 78fa1aa7..6deb2086 100644 --- a/tests/SimplePool.hs +++ b/tests/SimplePool.hs @@ -1,8 +1,8 @@ -{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DeriveGeneric #-} -- | Simple bounded (size) worker pool that accepts tasks and blocks -- the caller until they've completed. Partly a /spike/ for that 'Task' API @@ -18,18 +18,33 @@ import Control.Distributed.Process.Platform.ManagedProcess import Control.Distributed.Process.Platform.Time import Control.Distributed.Process.Serializable import Control.Exception hiding (catch) -import Data.Binary() +import Data.Binary import Data.List ( deleteBy , find ) import Data.Typeable +import GHC.Generics (Generic) + import Prelude hiding (catch) type PoolSize = Int type SimpleTask a = Closure (Process a) +data GetStats = GetStats + deriving (Typeable, Generic) + +instance Binary GetStats + +data PoolStats = PoolStats { + maxJobs :: Int + , activeJobs :: Int + , queuedJobs :: Int + } deriving (Typeable, Generic) + +instance Binary PoolStats + data Pool a = Pool { poolSize :: PoolSize , active :: [(MonitorRef, Recipient, Async a)] @@ -40,7 +55,8 @@ poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) poolServer = defaultProcess { apiHandlers = [ - handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + , handleCall poolStatsRequest ] , infoHandlers = [ handleInfo taskComplete @@ -52,22 +68,30 @@ simplePool :: forall a . (Serializable a) => PoolSize -> ProcessDefinition (Pool a) -> Process (Either (InitResult (Pool a)) TerminateReason) -simplePool sz server = - start sz init' server - `catch` (\(e :: SomeException) -> do - say $ "terminating with " ++ (show e) - liftIO $ throwIO e) +simplePool sz server = start sz init' server where init' :: PoolSize -> Process (InitResult (Pool a)) init' sz' = return $ InitOk (Pool sz' [] []) Infinity -- enqueues the task in the pool and blocks -- the caller until the task is complete -executeTask :: Serializable a - => ProcessId +executeTask :: forall s a . (Addressable s, Serializable a) + => s -> Closure (Process a) -> Process (Either String a) executeTask sid t = call sid t +-- internal / server-side API + +poolStatsRequest :: (Serializable a) + => Pool a + -> GetStats + -> Process (ProcessReply (Pool a) PoolStats) +poolStatsRequest st GetStats = + let sz = poolSize st + ac = length (active st) + pj = length (accepted st) + in reply (PoolStats sz ac pj) st + -- /call/ handler: accept a task and defer responding until "later" storeTask :: Serializable a => Pool a From 129ebd170efe235bd9d5d1fb62c8af61784d9314 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Thu, 31 Jan 2013 21:35:44 +0000 Subject: [PATCH 0916/2357] update simple pool exampe --- distributed-process-platform.cabal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distributed-process-platform.cabal b/distributed-process-platform.cabal index 438678d1..4c01c7dc 100644 --- a/distributed-process-platform.cabal +++ b/distributed-process-platform.cabal @@ -150,7 +150,8 @@ test-suite GenServerTests stm >= 2.3 && < 2.5, test-framework >= 0.6 && < 0.9, test-framework-hunit, - transformers + transformers, + ghc-prim hs-source-dirs: src, tests From 7e1534690c5a7fe8ff72aff461e7059867e8babd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 00:50:21 +0000 Subject: [PATCH 0917/2357] update managed process tutorial --- _layouts/managedprocess.html | 5 + documentation.md | 8 + tutorials/3.managedprocess.md | 326 ++++++++++++++++++++++++++++++++-- 3 files changed, 326 insertions(+), 13 deletions(-) diff --git a/_layouts/managedprocess.html b/_layouts/managedprocess.html index 9e7bd435..00cee174 100644 --- a/_layouts/managedprocess.html +++ b/_layouts/managedprocess.html @@ -22,6 +22,11 @@ diff --git a/documentation.md b/documentation.md index a9051b90..7bab71b4 100644 --- a/documentation.md +++ b/documentation.md @@ -515,6 +515,14 @@ More complex examples of the `ManagedProcess` API can be seen in the [Managed Processes tutorial][22]. API documentation for HEAD is available [here][21]. +### Supervision Trees + +TBC + +### Process Groups + +TBC + [1]: http://www.haskell.org/haskellwiki/Cloud_Haskell [2]: https://github.com/haskell-distributed/distributed-process [3]: https://github.com/haskell-distributed/distributed-process-platform diff --git a/tutorials/3.managedprocess.md b/tutorials/3.managedprocess.md index c01c3f81..14b3095a 100644 --- a/tutorials/3.managedprocess.md +++ b/tutorials/3.managedprocess.md @@ -6,10 +6,6 @@ title: Managed Process Tutorial ### Introduction -In order to explore the `ManagedProcess` API, we will present a simple -example taken from the test suite, which exercises some of the more -interesting features. - The main idea behind `ManagedProcess` is to separate the functional and non-functional aspects of a process. By functional, we mean whatever application specific task the process performs, and by non-functional @@ -33,16 +29,19 @@ from the backlog and executed. `ManagedProcess` provides a basic protocol for *server-like* processes such as this, based on the synchronous `call` and asynchronous `cast` -functions. Although `call` is synchronous, communication with the -*server process* is out of band, both from the client and the server's -point of view. The server implementation chooses whether to reply to -a call request immediately, or defer its reply until a later stage -and go back to receiving messages in the meanwhile. +functions used by code we provide to client clients and matching +*handler* functions in the process itself, for which there is a similar +API on the *server*. Although `call` is a synchronous protocol, +communication with the *server process* is out of band, both from the +client and the server's point of view. The server implementation chooses +whether to reply to a call request immediately, or defer its reply until +a later stage and go back to receiving other messages in the meanwhile. -### Implementation Sketch +### Implementing the client -We start out with some types: the tasks we perform and the maximum -pool size: +Before we figure out the shape of our state, let's think about the types +we'll need to consume in the server process: the tasks we perform and the +maximum pool size. {% highlight haskell %} type PoolSize = Int @@ -52,4 +51,305 @@ type SimpleTask a = Closure (Process a) To submit a task, our clients will submit an action in the process monad, wrapped in a `Closure` environment. We will use the `Addressable` typeclass to allow clients to specify the server's location in whatever -manner suits them. +manner suits them: + +{% highlight haskell %} +-- enqueues the task in the pool and blocks +-- the caller until the task is complete +executeTask :: forall s a . (Addressable s, Serializable a) + => s + -> Closure (Process a) + -> Process (Either String a) +executeTask sid t = call sid t +{% endhighlight %} + +That's it for the client! Note that the type signature we expose to +our consumers is specific, and that we do not expose them to either +arbitrary messages arriving in their mailbox or to exceptions being +thrown in their thread. Instead we return an `Either`. + +There are several varieties of the `call` API that deal with error +handling in different ways. Consult the haddocks for more info about +these. + +### Implementing the server + +Back on the server, we write a function that takes our state and an +input message - in this case, the `Closure` we've been sent - and +have that update the process' state and possibility launch the task +if we have enough spare capacity. + +{% highlight haskell %} +data Pool a = Pool a +{% endhighlight %} + +I've called the state type `Pool` as we're providing a fixed size resource +pool from the consumer's perspective. We could think of this as a bounded +size latch or barrier of sorts, but that conflates the example a bit too +much. We parameterise the state by the type of data that can be returned +by submitted tasks. + +The updated pool must store the task **and** the caller (so we can reply +once the task is complete). The `ManagedProcess.Server` API will provide us +with a `Recipient` value which can be used to reply to the caller at a later +time, so we'll make use of that here. + +{% highlight haskell %} +acceptTask :: Serializable a + => Pool a + -> Recipient + -> Closure (Process a) + -> Process (Pool a) +{% endhighlight %} + +For our example we will avoid using even vaguely exotic types to manage our +process' internal state, and stick to simple property lists. This is hardly +efficient, but that's fine for a test/demo. + +{% highlight haskell %} +data Pool a = Pool { + poolSize :: PoolSize + , accepted :: [(Recipient, Closure (Process a))] + } deriving (Typeable) +{% endhighlight %} + +### Making use of Async + +So **how** can we execute this `Closure (Process a)` without blocking the server +process itself? We will use the `Control.Distributed.Process.Platform.Async` API +to execute the task asynchronously and provide a means for waiting on the result. + +In order to use the `Async` handle to get the result of the computation once it's +complete, we'll have to hang on to a reference. We also need a way to associate the +submitter with the handle, so we end up with one field for the active (running) +tasks and another for the queue of accepted (but inactive) ones, like so... + +{% highlight haskell %} +data Pool a = Pool { + poolSize :: PoolSize + , active :: [(Recipient, Async a)] + , accepted :: [(Recipient, Closure (Process a))] + } deriving (Typeable) +{% endhighlight %} + +To turn that `Closure` environment into a thunk we can evaluate, we'll use the +built in `unClosure` function, and we'll pass the thunk to `async` and get back +a handle to the async task. + +{% highlight haskell %} +proc <- unClosure task' +asyncHandle <- async proc +{% endhighlight %} + +Of course, we decided that we wouldn't block on each `Async` handle, and we're not +able to sit in a *loop* polling all the handles representing tasks we're running, +because no submissions would be handled whilst spinning and waiting for results. +We're relying on monitors instead, so we need to store the `MonitorRef` so we know +which monitor signal relates to which async task (and recipient). + +{% highlight haskell %} +data Pool a = Pool { + poolSize :: PoolSize + , active :: [(MonitorRef, Recipient, Async a)] + , accepted :: [(Recipient, Closure (Process a))] + } deriving (Typeable) +{% endhighlight %} + +Finally we can implement the `acceptTask` function. + +{% highlight haskell %} +acceptTask :: Serializable a + => Pool a + -> Recipient + -> Closure (Process a) + -> Process (Pool a) +acceptTask s@(Pool sz' runQueue taskQueue) from task' = + let currentSz = length runQueue + in case currentSz >= sz' of + True -> do + return $ s { accepted = ((from, task'):taskQueue) } + False -> do + proc <- unClosure task' + asyncHandle <- async proc + ref <- monitorAsync asyncHandle + taskEntry <- return (ref, from, asyncHandle) + return s { active = (taskEntry:runQueue) } +{% endhighlight %} + +If we're at capacity, we add the task (and caller) to the `accepted` queue, +otherwise we launch and monitor the task using `async` and stash the monitor +ref, caller ref and the async handle together in the `active` field. Prepending +to the list of active/running tasks is a somewhat arbitrary choice. One might +argue that heuristically, the younger a task is the less likely it is that it +will run for a long time. Either way, I've done this to avoid cluttering the +example other data structures, so we can focus on the `ManagedProcess` APIs +only. + +Now we will write a function that handles the results. When the monitor signal +arrives, we use the async handle to obtain the result and send it back to the caller. +Because, even if we were running at capacity, we've now seen a task complete (and +therefore reduce the number of active tasks by one), we will also pull off a pending +task from the backlog (i.e., accepted), if any exists, and execute it. As with the +active task list, we're going to take from the backlog in FIFO order, which is +almost certainly not what you'd want in a real application, but that's not the +point of the example either. + +The steps then, are + +1. find the async handle for the monitor ref +2. pull the result out of it +3. send the result to the client +4. bump another task from the backlog (if there is one) +5. carry on + +This chain then, looks like `wait h >>= respond c >> bump s t >>= continue`. + +Item (3) requires special API support from `ManagedProcess`, because we're not +just sending *any* message back to the caller. We're replying to a `call` +that has already taken place and is, in fact, still running. The API call for +this is `replyTo`. + +{% highlight haskell %} +taskComplete :: forall a . Serializable a + => Pool a + -> ProcessMonitorNotification + -> Process (ProcessAction (Pool a)) +taskComplete s@(Pool _ runQ _) + (ProcessMonitorNotification ref _ _) = + let worker = findWorker ref runQ in + case worker of + Just t@(_, c, h) -> wait h >>= respond c >> bump s t >>= continue + Nothing -> continue s + where + respond :: Recipient + -> AsyncResult a + -> Process () + respond c (AsyncDone r) = replyTo c ((Right r) :: (Either String a)) + respond c (AsyncFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond c (AsyncLinkFailed d) = replyTo c ((Left (show d)) :: (Either String a)) + respond _ _ = die $ TerminateOther "IllegalState" + + bump :: Pool a -> (MonitorRef, Recipient, Async a) -> Process (Pool a) + bump st@(Pool _ runQueue acc) worker = + let runQ2 = deleteFromRunQueue worker runQueue in + case acc of + [] -> return st { active = runQ2 } + ((tr,tc):ts) -> acceptTask (st { accepted = ts, active = runQ2 }) tr tc + +findWorker :: MonitorRef + -> [(MonitorRef, Recipient, Async a)] + -> Maybe (MonitorRef, Recipient, Async a) +findWorker key = find (\(ref,_,_) -> ref == key) + +deleteFromRunQueue :: (MonitorRef, Recipient, Async a) + -> [(MonitorRef, Recipient, Async a)] + -> [(MonitorRef, Recipient, Async a)] +deleteFromRunQueue c@(p, _, _) runQ = deleteBy (\_ (b, _, _) -> b == p) c runQ +{% endhighlight %} + +That was pretty simple. We've deal with mapping the `AsyncResult` to `Either` values, +which we *could* have left to the caller, but this makes the client facing API much +simpler to work with. + +### Wiring up handlers + +The `ProcessDefinition` takes a number of different kinds of handler. The only ones +we care about are the call handler for submission handling, and the handler that +deals with monitor signals. + +Call and cast handlers live in the `apiHandlers` list of a `ProcessDefinition` and +must have the type `Dispatcher s` where `s` is the state type for the process. We +cannot construct a `Dispatcher` ourselves, but a range of functions in the +`ManagedProcess.Server` module exist to lift functions like the ones we've just +defined. The particular function we need is `handleCallFrom`, which works with +functions over the state, `Recipient` and the call data/message. All the varieties +of `handleCall` need to return a `ProcessReply`, which has the following type + +{% highlight haskell %} +data ProcessReply s a = + ProcessReply a (ProcessAction s) + | NoReply (ProcessAction s) +{% endhighlight %} + +There are also various utility function in the API to construct a `ProcessAction` +and we will make use of `noReply_` here, which constructs `NoReply` for us and +presets the `ProcessAction` to `ProcessContinue`, which goes back to receiving +messages without further action. We already have a function over the right input +domain which evaluates to a new state so we end up with: + +{% highlight haskell %} +storeTask :: Serializable a + => Pool a + -> Recipient + -> Closure (Process a) + -> Process (ProcessReply (Pool a) ()) +storeTask s r c = acceptTask s r c >>= noReply_ +{% endhighlight %} + +In order to spell things out for the compiler, we need to put a type signature +in place at the call site too, so our final construct is + +{% highlight haskell %} +handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) +{% endhighlight %} + +No such thing is required for `taskComplete`, as there's no ambiguity about its +type. Our process definition is finished, and here it is: + +{% highlight haskell %} +poolServer :: forall a . (Serializable a) => ProcessDefinition (Pool a) +poolServer = + defaultProcess { + apiHandlers = [ + handleCallFrom (\s f (p :: Closure (Process a)) -> storeTask s f p) + ] + , infoHandlers = [ + handleInfo taskComplete + ] + } :: ProcessDefinition (Pool a) +{% endhighlight %} + +Starting the pool is fairly simple and `ManagedProcess` has some utilities to help. + +{% highlight haskell %} +simplePool :: forall a . (Serializable a) + => PoolSize + -> ProcessDefinition (Pool a) + -> Process (Either (InitResult (Pool a)) TerminateReason) +simplePool sz server = start sz init' server + where init' :: PoolSize -> Process (InitResult (Pool a)) + init' sz' = return $ InitOk (Pool sz' [] []) Infinity +{% endhighlight %} + +### Putting it all together + +Starting up a pool locally or on a remote node is just a matter of using `spawn` +or `spawnLocal` with `simplePool`. The second argument should specify the type of +results, e.g., + +{% highlight haskell %} +let s' = poolServer :: ProcessDefinition (Pool String) +in simplePool s s' +{% endhighlight %} + +Defining tasks is as simple as making them remote-worthy: + +{% highlight haskell %} +sampleTask :: (TimeInterval, String) -> Process String +sampleTask (t, s) = sleep t >> return s + +$(remotable ['sampleTask]) +{% endhighlight %} + +And executing them is just as simple too. Given a pool which has been registered +locally as "mypool", we can simply call it directly: + +{% highlight haskell %} +job <- return $ ($(mkClosure 'sampleTask) (seconds 2, "foobar")) +call "mypool" job >>= wait >>= stash result +{% endhighlight %} + +Hopefully this has demonstrated a few benefits of the `ManagedProcess` API, although +it's really just scratching the surface. We have focussed on the code that matters - +state transitions and decision making, without getting bogged down (much) with receiving +or sending messages, apart from using some simple APIs when we needed to. From a96e47695c9532c10da6281f8743e9e417cbc0fa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 09:13:39 +0000 Subject: [PATCH 0918/2357] udpate managedprocess tutorial with performance notes --- _layouts/managedprocess.html | 1 + tutorials/3.managedprocess.md | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/_layouts/managedprocess.html b/_layouts/managedprocess.html index 00cee174..9786a381 100644 --- a/_layouts/managedprocess.html +++ b/_layouts/managedprocess.html @@ -27,6 +27,7 @@
  • Making use of Async
  • Wiring up handlers
  • Putting it all together
  • +
  • Performance Considerations
  • diff --git a/tutorials/3.managedprocess.md b/tutorials/3.managedprocess.md index 14b3095a..a1e90fa8 100644 --- a/tutorials/3.managedprocess.md +++ b/tutorials/3.managedprocess.md @@ -353,3 +353,24 @@ Hopefully this has demonstrated a few benefits of the `ManagedProcess` API, alth it's really just scratching the surface. We have focussed on the code that matters - state transitions and decision making, without getting bogged down (much) with receiving or sending messages, apart from using some simple APIs when we needed to. + +### Performance Considerations + +We did not take much care over our choice of data structures. Might this have profound +consequences for clients? The LIFO nature of the pending backlog is surprising, but +we can change that quite easily by changing data structures. + +What's perhaps more of a concern is the cost of using `Async` everywhere - remember +we used this in the *server* to handle concurrently executing tasks and obtaining +their results. The `Async` module is also used by `ManagedProcess` to handle the +`call` mechanism, and there *are* some overheads to using it. An invocation of +`async` will create two new processes: one to perform the calculation and another +to monitor the first and handle failure and/or cancellation. Spawning processes is +cheap, but not free as each process is a haskell thread, plus some additional book +keeping data. + +The cost of spawning two processes for each computation/task might represent just that +bit too much overhead for some applications. In our next tutorial, we'll look at the +`Control.Distributed.Process.Platform.Task` API, which looks a lot like `Async` but +manages exit signals in a single thread and makes configurable task pools and task +supervision strategy part of its API. From b4e794fa14164f166c3cdf65b0747a4f68557e60 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 18:53:54 +0000 Subject: [PATCH 0919/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ rank1dynamic.cabal | 9 +++++++-- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..81b637e8 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### rank1dynamic + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - mailto:parallel-haskell@googlegroups.com. + +### License + +rank1dynamic is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..becf3477 --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos +distributed-process-simplelocalnet +distributed-static +network-transport +network-transport-composed +network-transport-inmemory +network-transport-tcp +network-transport-tests + diff --git a/rank1dynamic.cabal b/rank1dynamic.cabal index e876821d..dbc7901c 100644 --- a/rank1dynamic.cabal +++ b/rank1dynamic.cabal @@ -4,16 +4,21 @@ Synopsis: Like Data.Dynamic/Data.Typeable but with support for rank-1 Description: "Data.Typeable" and "Data.Dynamic" only support monomorphic types. In this package we provide similar functionality but with support for rank-1 polymorphic types. -Homepage: http://github.com/haskell-distributed/distributed-process +Homepage: http://haskell-distributed.github.com License: BSD3 License-File: LICENSE Author: Edsko de Vries -Maintainer: edsko@well-typed.com +Maintainer: edsko@well-typed.com, watson.timothy@gmail.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/RANKNDYN Copyright: Well-Typed LLP Category: Data Build-Type: Simple Cabal-Version: >=1.8 +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/rank1dynamic + Library Exposed-Modules: Data.Rank1Dynamic, Data.Rank1Typeable From 8ec996916eb38c0a18b0e2a3f8b4ccf449947d30 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 18:55:09 +0000 Subject: [PATCH 0920/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ network-transport-tests.cabal | 8 ++++++-- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..e098b44c --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### network-transport-tests + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - mailto:parallel-haskell@googlegroups.com. + +### License + +network-transport-tests is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..7193339d --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos +distributed-process-simplelocalnet +distributed-static +network-transport +network-transport-composed +network-transport-inmemory +network-transport-tcp + +rank1dynamic diff --git a/network-transport-tests.cabal b/network-transport-tests.cabal index b4cf6f1b..81669e82 100644 --- a/network-transport-tests.cabal +++ b/network-transport-tests.cabal @@ -2,16 +2,20 @@ name: network-transport-tests version: 0.1.0.1 synopsis: Unit tests for Network.Transport implementations -- description: -homepage: http://github.com/haskell-distributed/distributed-process +homepage: http://haskell-distributed.github.com license: BSD3 license-file: LICENSE author: Edsko de Vries -maintainer: edsko@well-typed.com +maintainer: edsko@well-typed.com, watson.timothy@gmail.com copyright: Well-Typed LLP category: Network build-type: Simple cabal-version: >=1.8 +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/network-transport-tests + library exposed-modules: Network.Transport.Tests, Network.Transport.Tests.Multicast, From 94283da51deb4c8182134838ade623f4c850b7cf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 18:59:07 +0000 Subject: [PATCH 0921/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ network-transport-tcp.cabal | 9 ++++----- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..43b54e4d --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### network-transport-tcp + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - mailto:parallel-haskell@googlegroups.com. + +### License + +network-transport-tcp is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..14360d1f --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos +distributed-process-simplelocalnet +distributed-static +network-transport +network-transport-composed +network-transport-inmemory + +network-transport-tests +rank1dynamic diff --git a/network-transport-tcp.cabal b/network-transport-tcp.cabal index 6ba726d0..34cb7eea 100644 --- a/network-transport-tcp.cabal +++ b/network-transport-tcp.cabal @@ -6,10 +6,10 @@ License: BSD3 License-file: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, duncan@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com, watson.timothy@gmail.com Stability: experimental -Homepage: http://github.com/haskell-distributed/distributed-process -Bug-Reports: mailto:edsko@well-typed.com +Homepage: http://haskell-distributed.github.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/NTTCP Synopsis: TCP instantiation of Network.Transport Description: TCP instantiation of Network.Transport Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 @@ -17,8 +17,7 @@ Category: Network Source-Repository head Type: git - Location: https://github.com/haskell-distributed/distributed-process - SubDir: network-transport-tcp + Location: https://github.com/haskell-distributed/network-transport-tcp Flag use-mock-network Description: Use mock network implementation (for testing) From 91b380077ffb1597b142378b6022eaaadb9e75f1 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:03:52 +0000 Subject: [PATCH 0922/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ network-transport-inmemory.cabal | 10 +++++++--- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..730b8d02 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### network-transport-inmemory + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - mailto:parallel-haskell@googlegroups.com. + +### License + +network-transport-inmemory is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..f253f06c --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos +distributed-process-simplelocalnet +distributed-static +network-transport +network-transport-composed + +network-transport-tcp +network-transport-tests +rank1dynamic diff --git a/network-transport-inmemory.cabal b/network-transport-inmemory.cabal index 9f819971..11d211f9 100644 --- a/network-transport-inmemory.cabal +++ b/network-transport-inmemory.cabal @@ -6,15 +6,19 @@ License: BSD3 License-file: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, duncan@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com, watson.timothy@gmail.com Stability: experimental -Homepage: http://github.com/haskell-distributed/distributed-process -Bug-Reports: mailto:edsko@well-typed.com +Homepage: http://haskell-distributed.github.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/NTINMEM Synopsis: In-memory instantiation of Network.Transport Description: In-memory instantiation of Network.Transport Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 Category: Network +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/network-transport-inmemory + Library Build-Depends: base >= 4.3 && < 5, network-transport >= 0.3 && < 0.4, From dd192bea1bd2672a531edd1e4dee33e7e24fdeb0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:06:57 +0000 Subject: [PATCH 0923/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ distributed-static.cabal | 9 +++++++-- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..0ec0aec2 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### distributed-static + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - mailto:parallel-haskell@googlegroups.com. + +### License + +distributed-static is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..21271215 --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos +distributed-process-simplelocalnet + +network-transport +network-transport-composed +network-transport-inmemory +network-transport-tcp +network-transport-tests +rank1dynamic diff --git a/distributed-static.cabal b/distributed-static.cabal index 19be4de4..5a1d7741 100644 --- a/distributed-static.cabal +++ b/distributed-static.cabal @@ -14,16 +14,21 @@ Description: /Towards Haskell in the Cloud/ (Epstein et al, Haskell 'RemoteTable'). In this module we implement this mimickry and various extensions: type safety (including for polymorphic static values) and compositionality. -Homepage: http://www.github.com/haskell-distributed/distributed-process +Homepage: http://haskell-distributed.github.com License: BSD3 License-File: LICENSE Author: Edsko de Vries -Maintainer: edsko@well-typed.com +Maintainer: edsko@well-typed.com, watson.timothy@gmail.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/DS Copyright: Well-Typed LLP Category: Control Build-Type: Simple Cabal-Version: >=1.8 +Source-Repository head + Type: git + Location: https://github.com/haskell-distributed/distributed-static + Library Exposed-Modules: Control.Distributed.Static Build-Depends: base >= 4 && < 5, From 8c2a0821ea30f55ec4504ddb793f2aed918a0e4a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:09:25 +0000 Subject: [PATCH 0924/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ network-transport.cabal | 9 ++++----- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..f075634f --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### network-transport + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - parallel-haskell@googlegroups.com. + +### License + +network-transport is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..61b0a4a7 --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos +distributed-process-simplelocalnet +distributed-static + +-composed +-inmemory +-tcp +-tests +rank1dynamic diff --git a/network-transport.cabal b/network-transport.cabal index c4558864..71372b8e 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -6,10 +6,10 @@ License: BSD3 License-File: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, duncan@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com, watson.timothy@gmail.com Stability: experimental -Homepage: http://github.com/haskell-distributed/distributed-process -Bug-Reports: mailto:edsko@well-typed.com +Homepage: http://haskell-distributed.github.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/NT Synopsis: Network abstraction layer Description: "Network.Transport" is a Network Abstraction Layer which provides the following high-level concepts: @@ -60,8 +60,7 @@ Category: Network Source-Repository head Type: git - Location: https://github.com/haskell-distributed/distributed-process - SubDir: network-transport + Location: https://github.com/haskell-distributed/network-transport Library Build-Depends: base >= 4.3 && < 5, From d5820c60ba4617fb22c5bda8df4157d853e8961c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:10:49 +0000 Subject: [PATCH 0925/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 ++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++ REPOS | 11 ++++++++++ distributed-process-simplelocalnet.cabal | 7 +++--- 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..cc469a1f --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### distributed-process-simplelocalnet + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - parallel-haskell@googlegroups.com. + +### License + +distributed-process-simplelocalnet is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..f002c968 --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api +distributed-process-azure +distributed-process-demos + +distributed-static +network-transport +network-transport-composed +network-transport-inmemory +network-transport-tcp +network-transport-tests +rank1dynamic diff --git a/distributed-process-simplelocalnet.cabal b/distributed-process-simplelocalnet.cabal index beaec836..802ede1d 100644 --- a/distributed-process-simplelocalnet.cabal +++ b/distributed-process-simplelocalnet.cabal @@ -8,8 +8,8 @@ Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries Maintainer: watson.timothy@gmail.com, edsko@well-typed.com, duncan@well-typed.com Stability: experimental -Homepage: http://github.com/haskell-distributed/distributed-process -Bug-Reports: http://github.com/haskell-distributed/distributed-process/issues +Homepage: http://haskell-distributed.github.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/DPSLN Synopsis: Simple zero-configuration backend for Cloud Haskell Description: Simple backend based on the TCP transport which offers node discovery based on UDP multicast. This is a zero-configuration @@ -20,8 +20,7 @@ Category: Control Source-Repository head Type: git - Location: https://github.com/haskell-distributed/distributed-process - SubDir: distributed-process-simplelocalnet + Location: https://github.com/haskell-distributed/distributed-process-simplelocalnet Flag build-example Default: False From 07da8cf05cbd37c29ff93c9a23a921a9989c823b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:13:04 +0000 Subject: [PATCH 0926/2357] split repository from distributed-process and add standard CI artefacts --- Makefile | 27 +++++++++++++++++++++++++++ README.md | 20 ++++++++++++++++++++ REPOS | 11 +++++++++++ distributed-process-azure.cabal | 9 ++++----- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 REPOS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed740f00 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# CI build + +GHC ?= $(shell which ghc) +CABAL ?= $(shell which cabal) + +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + +.PHONY: all +all: $(REPOS) + +$(REPOS): + git clone $(BASE_GIT)/$@.git + +.PHONY: install +install: $(REPOS) + $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall + $(CABAL) install + +.PHONY: ci +ci: install test + +.PHONY: test +test: + $(CABAL) configure --enable-tests + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/README.md b/README.md new file mode 100644 index 00000000..862cd1e6 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +### distributed-process-azure + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the +[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in +the first instance - parallel-haskell@googlegroups.com. + +### License + +distributed-process-azure is made available under a BSD-3 license. diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..b231d697 --- /dev/null +++ b/REPOS @@ -0,0 +1,11 @@ +azure-service-api + +distributed-process-demos +distributed-process-simplelocalnet +distributed-static +network-transport +network-transport-composed +network-transport-inmemory +network-transport-tcp +network-transport-tests +rank1dynamic diff --git a/distributed-process-azure.cabal b/distributed-process-azure.cabal index d2e6561d..2a325cd0 100644 --- a/distributed-process-azure.cabal +++ b/distributed-process-azure.cabal @@ -6,10 +6,10 @@ License: BSD3 License-File: LICENSE Copyright: Well-Typed LLP Author: Duncan Coutts, Nicolas Wu, Edsko de Vries -Maintainer: edsko@well-typed.com, duncan@well-typed.com +Maintainer: edsko@well-typed.com, duncan@well-typed.com, watson.timothy@gmail.com Stability: experimental -Homepage: http://github.com/haskell-distributed/distributed-process -Bug-Reports: mailto:edsko@well-typed.com +Homepage: http://haskell-distributed.github.com +Bug-Reports: https://cloud-haskell.atlassian.net/browse/DPAZURE Synopsis: Microsoft Azure backend for Cloud Haskell Description: This is a proof of concept Azure backend for Cloud Haskell. It provides just enough functionality to run Cloud Haskell @@ -23,8 +23,7 @@ Category: Control Source-Repository head Type: git - Location: https://github.com/haskell-distributed/distributed-process - SubDir: distributed-process-azure + Location: https://github.com/haskell-distributed/distributed-process-azure Flag build-demos description: Build the demos From 03ea83691184e468b731c128a5b587c71a1ff3bd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:18:30 +0000 Subject: [PATCH 0927/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 862cd1e6..56f2bc9b 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From 4b359853f6b19cfcee5a940194f0b15d1ac11986 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:19:03 +0000 Subject: [PATCH 0928/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc469a1f..ce76ba41 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From f8d671b3a2c7698d1a087e653cad99a486991505 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:19:19 +0000 Subject: [PATCH 0929/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f075634f..5eb57b54 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From dfea0ed6b1ebb03f42aafe310eace5759991aa9d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:19:34 +0000 Subject: [PATCH 0930/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ec0aec2..f55344e3 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - mailto:parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From b1fa27df5ade37698bac19934b0a20f4a809d6b8 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:20:06 +0000 Subject: [PATCH 0931/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 730b8d02..87c7c7cd 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - mailto:parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From ab224e896f53d8bcbc0fe6743c58fdc0f95af5a3 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:20:22 +0000 Subject: [PATCH 0932/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81b637e8..4b65ed0e 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - mailto:parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From 758ca29705e3f012084ca9461a14dcbd454330fa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:20:34 +0000 Subject: [PATCH 0933/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e098b44c..67d625d3 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - mailto:parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From 7ac55701b13eba34be0129ae7918f2eb9943804a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 19:20:45 +0000 Subject: [PATCH 0934/2357] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 43b54e4d..7947914a 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit issues. Anyone can browse, although you'll need to provide an email address and create an account in order to submit new issues. -If you'd like to talk to a human, please contact us at the -[parallel-haskell mailing list](parallel-haskell@googlegroups.com) in -the first instance - mailto:parallel-haskell@googlegroups.com. +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. ### License From c8c71e440b3748855f3444db6867cba3f1604c4e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 23:36:25 +0000 Subject: [PATCH 0935/2357] tidy up Makefile, remove REPOS --- Makefile | 11 ++--------- REPOS | 11 ----------- 2 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 REPOS diff --git a/Makefile b/Makefile index ed740f00..64488a69 100644 --- a/Makefile +++ b/Makefile @@ -14,14 +14,7 @@ $(REPOS): .PHONY: install install: $(REPOS) - $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall - $(CABAL) install + $(CABAL) install --with-ghc=$(GHC) .PHONY: ci -ci: install test - -.PHONY: test -test: - $(CABAL) configure --enable-tests - $(CABAL) build - $(CABAL) test --show-details=always +ci: install diff --git a/REPOS b/REPOS deleted file mode 100644 index becf3477..00000000 --- a/REPOS +++ /dev/null @@ -1,11 +0,0 @@ -azure-service-api -distributed-process-azure -distributed-process-demos -distributed-process-simplelocalnet -distributed-static -network-transport -network-transport-composed -network-transport-inmemory -network-transport-tcp -network-transport-tests - From 3283f4d047be9f809768f96d139d3c3fd01b378a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Fri, 1 Feb 2013 23:41:13 +0000 Subject: [PATCH 0936/2357] tidy up Makefile, remove REPOS --- Makefile | 22 +++------------------- REPOS | 11 ----------- 2 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 REPOS diff --git a/Makefile b/Makefile index ed740f00..8001840a 100644 --- a/Makefile +++ b/Makefile @@ -3,25 +3,9 @@ GHC ?= $(shell which ghc) CABAL ?= $(shell which cabal) -BASE_GIT := git://github.com/haskell-distributed -REPOS=$(shell cat REPOS | sed '/^$$/d') - -.PHONY: all -all: $(REPOS) - -$(REPOS): - git clone $(BASE_GIT)/$@.git - .PHONY: install -install: $(REPOS) - $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall - $(CABAL) install +install: + $(CABAL) install --with-ghc=$(GHC) .PHONY: ci -ci: install test - -.PHONY: test -test: - $(CABAL) configure --enable-tests - $(CABAL) build - $(CABAL) test --show-details=always +ci: install diff --git a/REPOS b/REPOS deleted file mode 100644 index 61b0a4a7..00000000 --- a/REPOS +++ /dev/null @@ -1,11 +0,0 @@ -azure-service-api -distributed-process-azure -distributed-process-demos -distributed-process-simplelocalnet -distributed-static - --composed --inmemory --tcp --tests -rank1dynamic From cacb70adef5933ac1a3715a0ed63c0832ba4242d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 3 Feb 2013 15:45:15 +0000 Subject: [PATCH 0937/2357] update travis config --- .travis.yml | 2 ++ network-transport.cabal | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..795eb09e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +language: haskell +script: "make ci" diff --git a/network-transport.cabal b/network-transport.cabal index 71372b8e..c03d8cba 100644 --- a/network-transport.cabal +++ b/network-transport.cabal @@ -56,7 +56,7 @@ Description: "Network.Transport" is a Network Abstraction Layer which provides probably also want to install at least one transport implementation (network-transport-*). Tested-With: GHC==7.0.4 GHC==7.2.2 GHC==7.4.1 GHC==7.4.2 -Category: Network +Category: Network Source-Repository head Type: git From f14fe8e9d9c65837dc40c12a93ddd0cdabcad9eb Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 3 Feb 2013 15:48:35 +0000 Subject: [PATCH 0938/2357] add ci status to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eb57b54..db4723f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### network-transport +### network-transport [![travis](https://secure.travis-ci.org/haskell-distributed/network-transport.png)](http://travis-ci.org/haskell-distributed/network-transport) This repository is part of Cloud Haskell. From fc38ebddb6e98e0c6c3e920212137eda8a8db78f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 3 Feb 2013 15:53:56 +0000 Subject: [PATCH 0939/2357] add ci config --- .travis.yml | 2 ++ Makefile | 11 +---------- README.md | 3 ++- 3 files changed, 5 insertions(+), 11 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..795eb09e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +language: haskell +script: "make ci" diff --git a/Makefile b/Makefile index 64488a69..8001840a 100644 --- a/Makefile +++ b/Makefile @@ -3,17 +3,8 @@ GHC ?= $(shell which ghc) CABAL ?= $(shell which cabal) -BASE_GIT := git://github.com/haskell-distributed -REPOS=$(shell cat REPOS | sed '/^$$/d') - -.PHONY: all -all: $(REPOS) - -$(REPOS): - git clone $(BASE_GIT)/$@.git - .PHONY: install -install: $(REPOS) +install: $(CABAL) install --with-ghc=$(GHC) .PHONY: ci diff --git a/README.md b/README.md index 4b65ed0e..ddf77853 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -### rank1dynamic +### rank1dynamic [![travis](https://secure.travis-ci.org/haskell-distributed/rank1dynamic.png)](http://travis-ci.org/haskell-distributed/rank1dynamic) + This repository is part of Cloud Haskell. From 8879225c85f3d433997677d1486ee88e18b9d6ec Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Sun, 3 Feb 2013 16:22:41 +0000 Subject: [PATCH 0940/2357] setup ci config --- .travis.yml | 6 ++++++ Makefile | 10 ++++++++-- REPOS | 9 --------- 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..52e8e88f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: haskell +install: + - cabal install data-accessor-transformers + - cabal install lockfree-queue + - cabal install test-framework-quickcheck2 +script: "make ci" diff --git a/Makefile b/Makefile index ed740f00..8deda61f 100644 --- a/Makefile +++ b/Makefile @@ -11,17 +11,23 @@ all: $(REPOS) $(REPOS): git clone $(BASE_GIT)/$@.git + $(CABAL) install --with-ghc=$(GHC) ./$@ --force-reinstalls .PHONY: install install: $(REPOS) - $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall $(CABAL) install .PHONY: ci -ci: install test +ci: $(REPOS) test .PHONY: test test: $(CABAL) configure --enable-tests $(CABAL) build $(CABAL) test --show-details=always + +.PHONY: itest +itest: + $(CABAL) configure --enable-tests -f use-mock-network + $(CABAL) build + $(CABAL) test --show-details=always diff --git a/REPOS b/REPOS index 14360d1f..35f7a707 100644 --- a/REPOS +++ b/REPOS @@ -1,11 +1,2 @@ -azure-service-api -distributed-process-azure -distributed-process-demos -distributed-process-simplelocalnet -distributed-static network-transport -network-transport-composed -network-transport-inmemory - network-transport-tests -rank1dynamic From 90258b9ebbfed7144f4e9874311fec8096dd6ba0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 08:32:43 +0000 Subject: [PATCH 0941/2357] don't rely on travis 'install' hooks --- .travis.yml | 4 ---- Makefile | 8 +++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 52e8e88f..795eb09e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,2 @@ language: haskell -install: - - cabal install data-accessor-transformers - - cabal install lockfree-queue - - cabal install test-framework-quickcheck2 script: "make ci" diff --git a/Makefile b/Makefile index 8deda61f..718d642e 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,14 @@ install: $(REPOS) .PHONY: ci ci: $(REPOS) test +.PHONY: deps +deps: + $(CABAL) install data-accessor-transformers + $(CABAL) install lockfree-queue + $(CABAL) install test-framework-quickcheck2 + .PHONY: test -test: +test: deps $(CABAL) configure --enable-tests $(CABAL) build $(CABAL) test --show-details=always From 1c22717b586b78ae6b15d4997e05eeee12d341cc Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 08:52:33 +0000 Subject: [PATCH 0942/2357] add build status to the readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7947914a..1e4fc664 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -### network-transport-tcp +### network-transport-tcp [![travis](https://secure.travis-ci.org/haskell-distributed/network-transport-tcp.png?branch=master,development)](http://travis-ci.org/haskell-distributed/network-transport-tcp) + This repository is part of Cloud Haskell. From c498b739e8212e89705c8ff522833e28c9a6884f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 08:55:40 +0000 Subject: [PATCH 0943/2357] Make build status image refer only to mainline branches We don't want to show failing topic branches in the status image for each project's readme. [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db4723f4..077d5444 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### network-transport [![travis](https://secure.travis-ci.org/haskell-distributed/network-transport.png)](http://travis-ci.org/haskell-distributed/network-transport) +### network-transport [![travis](https://secure.travis-ci.org/haskell-distributed/network-transport.png?branch=master,development)](http://travis-ci.org/haskell-distributed/network-transport) This repository is part of Cloud Haskell. From 373f8c1f97a8d99aee5232ccb78d79479a7374a6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 09:58:11 +0000 Subject: [PATCH 0944/2357] minimise wiki content where possible --- documentation.md | 5 ++ wiki/contributing.md | 114 ++++++++++++++++++++++++++++++++++++++- wiki/faq.md | 20 ++++++- wiki/help.md | 23 -------- wiki/maintainers.md | 87 ++++++++++++++++++++++++++++-- wiki/otherprotocols.md | 33 ++++++++++++ wiki/reliability.md | 32 +++-------- wiki/style.md | 120 ----------------------------------------- 8 files changed, 258 insertions(+), 176 deletions(-) delete mode 100644 wiki/help.md create mode 100644 wiki/otherprotocols.md delete mode 100644 wiki/style.md diff --git a/documentation.md b/documentation.md index 7bab71b4..fef5a6a6 100644 --- a/documentation.md +++ b/documentation.md @@ -22,6 +22,11 @@ growing number of features for * working with several network transport implementations (and more in the pipeline) * supporting *static* values (required for remote communication) +There is a recent +[presentation](http://sneezy.cs.nott.ac.uk/fun/2012-02/coutts-2012-02-28.pdf) +on Cloud Haskell and this reimplementation, which is worth reading in conjunction +with the documentation and wiki pages on this website.. + Cloud Haskell comprises the following components, some of which are complete, others experimental. diff --git a/wiki/contributing.md b/wiki/contributing.md index c238dfe0..01d8ab27 100644 --- a/wiki/contributing.md +++ b/wiki/contributing.md @@ -102,5 +102,115 @@ github issue. ### General Style -Please carefully review the [Style Guide](/wiki/style.html) and stick to the -conventions set out there as best you can. +A lot of this **is** taken from the GHC Coding Style entry [here](http://hackage.haskell.org/trac/ghc/wiki/Commentary/CodingStyle). +In particular, please follow **all** the advice on that wiki page when it comes +to including comments in your code. + +I am also grateful to @tibbe for his +[haskell-style-guide](https://github.com/tibbe/haskell-style-guide), from +which some of these rules have been taken. + +As a general rule, stick to the same coding style as is already used in the file +you're editing. It **is** much better to write code that is transparent than to +write code that is short. Please don't assume everyone is a minimalist - self +explanatory code is **much** better in the long term than pithy one-liners. +Having said that, we *do* like reusing abstractions where doing so adds to the +clarity of the code as well as minimising repetitious boilerplate. + +### Formatting + +#### Line Length + +Maximum line length is *80 characters*. This might seem antiquated +to you, but some of us do things like github pull-request code +reviews on our mobile devices on the way to work, and long lines +make this horrendously difficult. Besides which, some of us are +also emacs users and have this rule set up for all of our source +code editing modes. + +#### Indentation + +Tabs are illegal. Use **only** spaces for indenting. +Indentation is usually 2 spaces, with 4 spaces used in some places. +We're pretty chilled about this, but try to remain consistent. + +#### Blank Lines + +One blank line between top-level definitions. No blank lines between +type signatures and function definitions. Add one blank line between +functions in a type class instance declaration if the functions bodies +are large. As always, use your judgement. + +#### Whitespace + +Do not introduce trailing whitespace. If you find trailing whitespace, +feel free to strip it out - in a separate commit of course! + +Surround binary operators with a single space on either side. Use +your better judgement for the insertion of spaces around arithmetic +operators but always be consistent about whitespace on either side of +a binary operator. + +#### Alignment + +When it comes to alignment, there's probably a mix of things in the codebase +right now. Personally, I tend not to align import statements as these change +quite frequently and it is pain keeping the indentation consistent. + +The one exception to this is probably imports/exports, which we *are* a +bit finicky about: + +{% highlight haskell %} +import qualified Foo.Bar.Baz as Bz +import Data.Binary + ( Binary (..), + , getWord8 + , putWord8 + ) +import Data.Blah +import Data.Boom (Typeable) +{% endhighlight %} + +Personally I don't care *that much* about alignment for other things, +but as always, try to follow the convention in the file you're editing +and don't change things just for the sake of it. + +### Comments + +#### Punctuation + +Write proper sentences; start with a capital letter and use proper +punctuation. + +#### Top-Level Definitions + +Comment every top level function (particularly exported functions), +and provide a type signature; use Haddock syntax in the comments. +Comment every exported data type. Function example: + +{% highlight haskell %} +-- | Send a message on a socket. The socket must be in a connected +-- state. Returns the number of bytes sent. Applications are +-- responsible for ensuring that all data has been sent. +send :: Socket -- ^ Connected socket + -> ByteString -- ^ Data to send + -> IO Int -- ^ Bytes sent +{% endhighlight %} + +For functions the documentation should give enough information to +apply the function without looking at the function's definition. + +### Naming + +Use `mixedCase` when naming functions and `CamelCase` when naming data +types. + +For readability reasons, don't capitalize all letters when using an +abbreviation. For example, write `HttpServer` instead of +`HTTPServer`. Exception: Two letter abbreviations, e.g. `IO`. + +#### Modules + +Use singular when naming modules e.g. use `Data.Map` and +`Data.ByteString.Internal` instead of `Data.Maps` and +`Data.ByteString.Internals`. diff --git a/wiki/faq.md b/wiki/faq.md index 11e87671..9ad6f53e 100644 --- a/wiki/faq.md +++ b/wiki/faq.md @@ -1,6 +1,6 @@ --- layout: wiki -title: FAQ +title: FAQ/Getting Help wiki: FAQ --- @@ -31,3 +31,21 @@ project of this size. Cloud Haskell consists of no less than **13** individual projects at this time, and that's not to mention some of the experimental ones that have been developed by community members and *might* end up being absorbed by the team. + +### Help + +If the documentation doesn't answer your question, queries about Cloud Haskell +can be directed to the Parallel Haskell Mailing List +(parallel-haskell@googlegroups.com), which is pretty active. If you think +you've found a bug, or would like to request a new feature, please visit the +[Jira Issue Tracker](https://cloud-haskell.atlassian.net) and submit a bug. +You **will** need to register with your email address to create new issues, +though you can freely browse the existing tickets without doing so. + + +### Getting Involved + +If you're interested in hacking Cloud Haskell then please read the +[Contributing](/wiki/contributing.html) wiki page. Additional help can be obtained through the +[Developers Forum/Mailing List](https://groups.google.com/forum/?fromgroups=#!forum/cloud-haskell-developers) +or Parallel Haskell mailing list. diff --git a/wiki/help.md b/wiki/help.md deleted file mode 100644 index f51fc8f7..00000000 --- a/wiki/help.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -layout: wiki -title: Getting Help -wiki: Help ---- - -### Help - -If the documentation doesn't answer your question, queries about Cloud Haskell -can be directed to the Parallel Haskell Mailing List -(parallel-haskell@googlegroups.com), which is pretty active. If you think -you've found a bug, or would like to request a new feature, please visit the -[Jira Issue Tracker](https://cloud-haskell.atlassian.net) and submit a bug. -You **will** need to register with your email address to create new issues, -though you can freely browse the existing tickets without doing so. - - -### Getting Involved - -If you're interested in hacking Cloud Haskell then please read the -[Contributing](/wiki/contributing.html) wiki page. Additional help can be obtained through the -[Developers Forum/Mailing List](https://groups.google.com/forum/?fromgroups=#!forum/cloud-haskell-developers) -or Parallel Haskell mailing list. diff --git a/wiki/maintainers.md b/wiki/maintainers.md index 41152e33..0258e547 100644 --- a/wiki/maintainers.md +++ b/wiki/maintainers.md @@ -6,10 +6,54 @@ wiki: Maintainers ### Maintainers -This guide is specifically for maintainers, and outlines the -development process and in particular, the branching strategy. +This part of the guide is specifically for maintainers, and +outlines the development process and in particular, the branching +strategy. We also point out Cloud Haskell's various bits of +infrastructure as they stand at the moment. -#### Branching/Merging Policy +---- +#### Releases + +All releases are published to [hackage][3]. At some point we may start to +make *nightly builds* available on this website. + +---- + +#### Community + +We keep in touch through the [parallel-haskell google group][7], +and once you've joined the group, by posting to the mailing list address: +parallel-haskell@googlegroups.com. This is a group for **all** things related +to concurrent and parallel Haskell. There is also a maintainer/developer +centric [cloud-haskell-developers google group][9], which is more for +in-depth questions about contributing to or maintaining Cloud Haskell. + +You might also find some of us hanging out at #haskell-distributed on +freenode from time to time. + +We have a twitter account! [@CloudHaskell](https://twitter.com/CloudHaskell) +will be used to make announcements (of new releases, etc) on a regular basis. + +---- + +### Bug/Issue Tracking and Continuous Integration + +Our bug tracker is a hosted/on-demand Jira, for which a free open source +project license was kindly donated by [Atlassian][6]. You can browse all +issues without logging in, however to report new issues/bugs you will +need to provide an email address and create an account. + +If you have any difficulties doing so, please post an email to the +[parallel-haskell mailing list][7] at parallel-haskell@googlegroups.com. + +We currently use [travis-ci][11] for continuous integration. We do however, +have an open source license for Atlassian Bamboo, which we will be migrating +to over time - this process is quite involved so we don't have a picture of +the timescales yet. + +---- + +### Branching/Merging Policy The master branch is the **stable** branch, and should always be in a *releasable* state. This means that on the whole, only small @@ -37,6 +81,25 @@ you're planning on doing lots of squashing, then work in a branch and don't commit directly to development - and **definitely** not to master. +#### Committing without triggering CI builds + +Whilst we're on travis-ci, you can do this by putting the text +`[ci skip]` anywhere in the commit message. Please, please +**do not** put this on the first line of the commit message. + +Once we migrate to Bamboo, this may change. + +#### Changing Jira bugs/issues via commit messages + +You can make complex changes to one or more Jira issues with a single +commit message. As with skipping CI builds, please **do not** put this +messy text into the first line of your commit messages. + +Details of the format/syntax required to do this can be found on +[this Jira documentation page](https://confluence.atlassian.com/display/AOD/Processing+JIRA+issues+with+commit+messages) + +---- + #### Follow the Contributing guidelines What's good for the goose... @@ -45,7 +108,11 @@ What's good for the goose... Currently this is a manual process. If you don't sed/awk out the reference/link paths, it'll be a mess. We will add a script to -handle this some time soon. +handle this some time soon. I tend to only update the static +documentation for d-p and d-p-platform, at least until the process has +been automated. I also do this *only* for mainline branches (i.e., +for development and master), although again, automation could solve +a lot of issues there. There is also an open ticket to set up nightly builds, which will update the HEAD haddocks (on the website) and produce an 'sdist' @@ -94,3 +161,15 @@ feature that makes this very easy. After that, it's time to tweet about the release, post to the parallel-haskell mailing list, blog etc. Spread the word. + +[1]: https://github.com/haskell-distributed +[2]: https://github.com/haskell-distributed/haskell-distributed.github.com +[3]: http://hackage.haskell.org +[4]: http://git-scm.com/book/en/Git-Basics-Tagging +[5]: https://cloud-haskell.atlassian.net/secure/Dashboard.jspa +[6]: http://atlassian.com/ +[7]: https://groups.google.com/forum/?fromgroups=#!forum/parallel-haskell +[8]: /team.html +[9]: https://groups.google.com/forum/?fromgroups#!forum/cloud-haskell-developers +[10]: http://en.wikipedia.org/wiki/Greenwich_Mean_Time +[11]: https://travis-ci.org/ diff --git a/wiki/otherprotocols.md b/wiki/otherprotocols.md new file mode 100644 index 00000000..76cd3d8c --- /dev/null +++ b/wiki/otherprotocols.md @@ -0,0 +1,33 @@ +--- +layout: wiki +title: Applications and other protocols +wiki: Applications +--- + +### Applications + +If you are using `Network.Transport` in your application, or if you are writing (or interested in writing) `Network.Transport` support for another protocol, please add a brief description to this page so that we can coordinate the efforts. + +### HdpH + +**H**askell **D**istributed **P**arallel **H**askell is a shallowly embedded parallel extension of Haskell that supports high-level semi-explicit parallelism. HdpH has a distributed memory model, that manages computations on more than one multicore node. In addition _high-level semi-explicit parallelism_ is supported by providing `spark` for implicit task placement, alleviating the job of dynamic load management to HdpH. Explicit task placement is supported with the `pushTo` primitive. Lastly, HdpH supports the node <-> node transfer of polymorphic closures. This enables the definition of both evaluation strategies and algorithmic skeletons by using a small set of polymorphic coordination primitives. + +Efforts to adopt these new transport abstractions into HdpH are a work-in-progress, and early testing is showing positive performance results. When this work is complete, the upstream HdpH repository will be updated. + +* Paper presented at IFL 2011 [Implementing a High-level Distributed-Memory Parallel Haskell in Haskell](http://www.macs.hw.ac.uk/~pm175/papers/Maier_Trinder_IFL2011_XT.pdf) +* HdpH source code [HdpH GitHub repository](https://github.com/PatrickMaier/HdpH) + +# Protocol Implementations + +## CCI + +The [CCI](http://cci-forum.com) project is an open-source communication interface that aims to +provide a simple and portable API, high performance, scalability for +the largest deployments, and robustness in the presence of faults. It +is developed and maintained by a partnership of research, academic, +and industry members. + +[Parallel Scientific](http://www.parsci.com) is contributing with CCI development and promoting +its adoption as a portable API. We have developed [Haskell bindings](https://www.assembla.com/code/cci-haskell/git/nodes) for +the CCI alpha implementation, and are keen to have a CCI backend for +Cloud Haskell as resources allow. diff --git a/wiki/reliability.md b/wiki/reliability.md index d9cae3d6..9e9cc056 100644 --- a/wiki/reliability.md +++ b/wiki/reliability.md @@ -4,7 +4,7 @@ title: Fault Tolerance wiki: reliability --- -### reliability +### Reliability We do not consider the presence of side effects a barrier to fault tolerance and automated process restarts. We **do** recognise that it's easier to @@ -20,8 +20,8 @@ how to initialise and/or re-initialise a process that has been previously terminated. When it comes to failure recovery, we defer to Erlang's approach for handling -process failures in a generic manner, by drawing on the [OTP][13] concept of -[supervision trees][15]. Erlang's [supervisor module][16] implements a process +process failures in a generic manner, by drawing on the [OTP][1] concept of +[supervision trees][2]. Erlang's [supervisor module][3] implements a process which supervises other processes called child processes. The supervisor process is responsible for starting, stopping, monitoring and even restarting its child processes. A supervisors *children* can be either worker processes or @@ -30,26 +30,6 @@ supervision trees in Erlang parlance). The supervision APIs are a work in progress. -[1]: http://www.haskell.org/haskellwiki/Cloud_Haskell -[2]: https://github.com/haskell-distributed/distributed-process -[3]: https://github.com/haskell-distributed/distributed-process-platform -[4]: http://hackage.haskell.org/package/distributed-static -[5]: http://hackage.haskell.org/package/rank1dynamic -[6]: http://hackage.haskell.org/packages/network-transport -[7]: http://hackage.haskell.org/packages/network-transport-tcp -[8]: https://github.com/haskell-distributed/distributed-process/network-transport-inmemory -[9]: https://github.com/haskell-distributed/distributed-process/network-transport-composed -[10]: http://hackage.haskell.org/package/distributed-process-simplelocalnet -[11]: http://hackage.haskell.org/package/distributed-process-azure -[12]: http://research.microsoft.com/en-us/um/people/simonpj/papers/parallel/remote.pdf -[13]: http://en.wikipedia.org/wiki/Open_Telecom_Platform -[14]: http://hackage.haskell.org/packages/remote -[15]: http://www.erlang.org/doc/design_principles/sup_princ.html -[16]: http://www.erlang.org/doc/man/supervisor.html -[17]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-Async.html -[18]: https://github.com/haskell-distributed/distributed-process-platform -[19]: http://hackage.haskell.org/package/async -[20]: /wiki/networktransport.html -[21]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html -[22]: /tutorials/3.managedprocess.html - +[1]: http://en.wikipedia.org/wiki/Open_Telecom_Platform +[2]: http://www.erlang.org/doc/design_principles/sup_princ.html +[3]: http://www.erlang.org/doc/man/supervisor.html diff --git a/wiki/style.md b/wiki/style.md deleted file mode 100644 index 70098a14..00000000 --- a/wiki/style.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -layout: wiki -title: Style Guide -wiki: Style ---- - -### Style - -A lot of this **is** taken from the GHC Coding Style entry [here](http://hackage.haskell.org/trac/ghc/wiki/Commentary/CodingStyle). -In particular, please follow **all** the advice on that wiki page when it comes -to including comments in your code. - -I am also grateful to @tibbe for his -[haskell-style-guide](https://github.com/tibbe/haskell-style-guide), from -which some of these rules have been taken. - -As a general rule, stick to the same coding style as is already used in the file -you're editing. It **is** much better to write code that is transparent than to -write code that is short. Please don't assume everyone is a minimalist - self -explanatory code is **much** better in the long term than pithy one-liners. -Having said that, we *do* like reusing abstractions where doing so adds to the -clarity of the code as well as minimising repetitious boilerplate. - -### Formatting - -#### Line Length - -Maximum line length is *80 characters*. This might seem antiquated -to you, but some of us do things like github pull-request code -reviews on our mobile devices on the way to work, and long lines -make this horrendously difficult. Besides which, some of us are -also emacs users and have this rule set up for all of our source -code editing modes. - -#### Indentation - -Tabs are illegal. Use **only** spaces for indenting. -Indentation is usually 2 spaces, with 4 spaces used in some places. -We're pretty chilled about this, but try to remain consistent. - -#### Blank Lines - -One blank line between top-level definitions. No blank lines between -type signatures and function definitions. Add one blank line between -functions in a type class instance declaration if the functions bodies -are large. As always, use your judgement. - -#### Whitespace - -Do not introduce trailing whitespace. If you find trailing whitespace, -feel free to strip it out - in a separate commit of course! - -Surround binary operators with a single space on either side. Use -your better judgement for the insertion of spaces around arithmetic -operators but always be consistent about whitespace on either side of -a binary operator. - -#### Alignment - -When it comes to alignment, there's probably a mix of things in the codebase -right now. Personally, I tend not to align import statements as these change -quite frequently and it is pain keeping the indentation consistent. - -The one exception to this is probably imports/exports, which we *are* a -bit finicky about: - -{% highlight haskell %} -import qualified Foo.Bar.Baz as Bz -import Data.Binary - ( Binary (..), - , getWord8 - , putWord8 - ) -import Data.Blah -import Data.Boom (Typeable) -{% endhighlight %} - -Personally I don't care *that much* about alignment for other things, -but as always, try to follow the convention in the file you're editing -and don't change things just for the sake of it. - -### Comments - -#### Punctuation - -Write proper sentences; start with a capital letter and use proper -punctuation. - -#### Top-Level Definitions - -Comment every top level function (particularly exported functions), -and provide a type signature; use Haddock syntax in the comments. -Comment every exported data type. Function example: - -{% highlight haskell %} --- | Send a message on a socket. The socket must be in a connected --- state. Returns the number of bytes sent. Applications are --- responsible for ensuring that all data has been sent. -send :: Socket -- ^ Connected socket - -> ByteString -- ^ Data to send - -> IO Int -- ^ Bytes sent -{% endhighlight %} - -For functions the documentation should give enough information to -apply the function without looking at the function's definition. - -### Naming - -Use `mixedCase` when naming functions and `CamelCase` when naming data -types. - -For readability reasons, don't capitalize all letters when using an -abbreviation. For example, write `HttpServer` instead of -`HTTPServer`. Exception: Two letter abbreviations, e.g. `IO`. - -#### Modules - -Use singular when naming modules e.g. use `Data.Map` and -`Data.ByteString.Internal` instead of `Data.Maps` and -`Data.ByteString.Internals`. From deec645ad92eba6d55b65db26a71304e7734aa1a Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 10:00:29 +0000 Subject: [PATCH 0945/2357] tweak travis notifications [ci skip] --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 795eb09e..bad4a75d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,10 @@ language: haskell script: "make ci" +notifications: + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true + email: + recipients: + - cloud.haskell@gmail.com From 16d2e97f0b291e292b06036e2cebd8fafe661f7d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 10:01:35 +0000 Subject: [PATCH 0946/2357] tweak travis notifications [ci skip] --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 795eb09e..bad4a75d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,10 @@ language: haskell script: "make ci" +notifications: + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true + email: + recipients: + - cloud.haskell@gmail.com From 77dfcc58d57b39a7e64be3693e1f0c2727e0c15b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 10:02:36 +0000 Subject: [PATCH 0947/2357] tweak travis notifications [ci skip] --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 795eb09e..bad4a75d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,10 @@ language: haskell script: "make ci" +notifications: + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true + email: + recipients: + - cloud.haskell@gmail.com From a0be00f853f45eeda1319b2d58210e8ea92fb3cd Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 14:52:59 +0000 Subject: [PATCH 0948/2357] tweak notifications for travis --- .travis.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index bad4a75d..5f8657f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: haskell script: "make ci" notifications: - irc: - channels: - - "irc.freenode.org#haskell-distributed" - use_notice: true - email: - recipients: - - cloud.haskell@gmail.com + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true \ No newline at end of file From 301a98530fbb0cfc805ba8284afa9e6931b3878b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 15:01:30 +0000 Subject: [PATCH 0949/2357] merge travis config changes from master [ci skip] --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5f8657f2..439e6e0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ notifications: irc: channels: - "irc.freenode.org#haskell-distributed" - use_notice: true \ No newline at end of file + use_notice: true From d589951066028eb46c60d345f397efa2e933155c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 16:55:35 +0000 Subject: [PATCH 0950/2357] configure travis ci --- .travis.yml | 10 ++++++++++ Makefile | 12 +++--------- REPOS | 10 ---------- 3 files changed, 13 insertions(+), 19 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..439e6e0c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: haskell +script: "make ci" +notifications: + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true diff --git a/Makefile b/Makefile index ed740f00..0e484add 100644 --- a/Makefile +++ b/Makefile @@ -11,17 +11,11 @@ all: $(REPOS) $(REPOS): git clone $(BASE_GIT)/$@.git + cd $@ && $(CABAL) install --with-ghc=$(GHC) --force-reinstalls .PHONY: install install: $(REPOS) - $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall - $(CABAL) install + $(CABAL) install --force-reinstalls .PHONY: ci -ci: install test - -.PHONY: test -test: - $(CABAL) configure --enable-tests - $(CABAL) build - $(CABAL) test --show-details=always +ci: install diff --git a/REPOS b/REPOS index 21271215..fe796b31 100644 --- a/REPOS +++ b/REPOS @@ -1,11 +1 @@ -azure-service-api -distributed-process-azure -distributed-process-demos -distributed-process-simplelocalnet - -network-transport -network-transport-composed -network-transport-inmemory -network-transport-tcp -network-transport-tests rank1dynamic From ad41482036dc8f193e332fb0181f4856dde45494 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 17:02:36 +0000 Subject: [PATCH 0951/2357] add build status to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f55344e3..f82866dc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -### distributed-static +### distributed-static [![travis](https://secure.travis-ci.org/haskell-distributed/distributed-static.png?branch=master,development)](http://travis-ci.org/haskell-distributed/distributed-static) + This repository is part of Cloud Haskell. From d1f86a4a926afbf672a0398bf6c2b8b5c957371c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 17:23:37 +0000 Subject: [PATCH 0952/2357] setup travis ci --- .travis.yml | 10 ++++++++++ Makefile | 2 +- REPOS | 9 --------- 3 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..439e6e0c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: haskell +script: "make ci" +notifications: + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true diff --git a/Makefile b/Makefile index ed740f00..e074cb05 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,10 @@ all: $(REPOS) $(REPOS): git clone $(BASE_GIT)/$@.git + cd $@ && $(CABAL) install --with-ghc=$(GHC) --force-reinstall .PHONY: install install: $(REPOS) - $(CABAL) install --with-ghc=$(GHC) $(REPOS) --reinstall $(CABAL) install .PHONY: ci diff --git a/REPOS b/REPOS index f253f06c..35f7a707 100644 --- a/REPOS +++ b/REPOS @@ -1,11 +1,2 @@ -azure-service-api -distributed-process-azure -distributed-process-demos -distributed-process-simplelocalnet -distributed-static network-transport -network-transport-composed - -network-transport-tcp network-transport-tests -rank1dynamic From 085658d663ed0927387ec1cec07eb32b1cab0dc9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 17:25:40 +0000 Subject: [PATCH 0953/2357] add ci status image to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 87c7c7cd..470c5abe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -### network-transport-inmemory +### network-transport-inmemory [![travis](https://secure.travis-ci.org/haskell-distributed/network-transport-inmemory.png?branch=master,development)](http://travis-ci.org/haskell-distributed/network-transport-inmemory) + This repository is part of Cloud Haskell. From ba84097c3ca0ebaf109f591659017d1150352f46 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:02:17 +0000 Subject: [PATCH 0954/2357] fix broken links, add semantics --- _layouts/site.html | 2 +- _layouts/{tutorial.html => tutorial1.html} | 0 _layouts/{nt_tutorial.html => tutorial2.html} | 0 .../{managedprocess.html => tutorial3.html} | 0 documentation.md | 4 +- index.md | 8 +- semantics/CloudHaskellSemantics.pdf | Bin 0 -> 167226 bytes semantics/CloudHaskellSemantics.tex | 706 ++++++++++++++++++ semantics/Makefile | 11 + semantics/references.bib | 72 ++ static/tutorial/tutorial-client.hs | 24 + static/tutorial/tutorial-server.hs | 54 ++ tutorials/{1.tutorial.md => tutorial1.md} | 2 +- tutorials/{2.nt_tutorial.md => tutorial2.md} | 2 +- .../{3.managedprocess.md => tutorial3.md} | 2 +- wiki/networktransport.md | 277 +++++++ 16 files changed, 1154 insertions(+), 10 deletions(-) rename _layouts/{tutorial.html => tutorial1.html} (100%) rename _layouts/{nt_tutorial.html => tutorial2.html} (100%) rename _layouts/{managedprocess.html => tutorial3.html} (100%) create mode 100644 semantics/CloudHaskellSemantics.pdf create mode 100644 semantics/CloudHaskellSemantics.tex create mode 100644 semantics/Makefile create mode 100644 semantics/references.bib create mode 100644 static/tutorial/tutorial-client.hs create mode 100644 static/tutorial/tutorial-server.hs rename tutorials/{1.tutorial.md => tutorial1.md} (99%) rename tutorials/{2.nt_tutorial.md => tutorial2.md} (99%) rename tutorials/{3.managedprocess.md => tutorial3.md} (99%) diff --git a/_layouts/site.html b/_layouts/site.html index ec81725e..7e2f86d7 100644 --- a/_layouts/site.html +++ b/_layouts/site.html @@ -21,7 +21,7 @@ diff --git a/_layouts/tutorial.html b/_layouts/tutorial1.html similarity index 100% rename from _layouts/tutorial.html rename to _layouts/tutorial1.html diff --git a/_layouts/nt_tutorial.html b/_layouts/tutorial2.html similarity index 100% rename from _layouts/nt_tutorial.html rename to _layouts/tutorial2.html diff --git a/_layouts/managedprocess.html b/_layouts/tutorial3.html similarity index 100% rename from _layouts/managedprocess.html rename to _layouts/tutorial3.html diff --git a/documentation.md b/documentation.md index fef5a6a6..c7138137 100644 --- a/documentation.md +++ b/documentation.md @@ -535,8 +535,8 @@ TBC [5]: http://hackage.haskell.org/package/rank1dynamic [6]: http://hackage.haskell.org/packages/network-transport [7]: http://hackage.haskell.org/packages/network-transport-tcp -[8]: https://github.com/haskell-distributed/distributed-process/network-transport-inmemory -[9]: https://github.com/haskell-distributed/distributed-process/network-transport-composed +[8]: https://github.com/haskell-distributed/network-transport-inmemory +[9]: https://github.com/haskell-distributed/network-transport-composed [10]: http://hackage.haskell.org/package/distributed-process-simplelocalnet [11]: http://hackage.haskell.org/package/distributed-process-azure [12]: http://research.microsoft.com/en-us/um/people/simonpj/papers/parallel/remote.pdf diff --git a/index.md b/index.md index 045d862d..a74f02b6 100644 --- a/index.md +++ b/index.md @@ -4,16 +4,16 @@ title: Home --- Cloud Haskell: Erlang-style concurrent and distributed programming in Haskell. The Cloud Haskell Platform consists of a -[generic network transport API](https://github.com/haskell-distibuted/distributed-process/network-transport), +[generic network transport API](https://github.com/haskell-distibuted/network-transport), libraries for sending [static closures](https://github.com/haskell-distibuted/distributed-process/distributed-process-static) to remote nodes, a rich [API for distributed programming](https://github.com/haskell-distibuted/distributed-process/distributed-process) and a set of [Platform Libraries](https://github.com/haskell-distibuted/distributed-process-platform), modelled after Erlang's [Open Telecom Platform](http://www.erlang.org/doc/). Generic network transport backends have been developed for -[TCP](https://github.com/haskell-distibuted/distributed-process/network-transport-tcp) and -[in-memory](https://github.com/haskell-distibuted/distributed-process/network-transport-inmemory) +[TCP](https://github.com/haskell-distibuted/network-transport-tcp) and +[in-memory](https://github.com/haskell-distibuted/network-transport-inmemory) messaging, and several other implementations are available including a transport for -[Windows Azure](https://github.com/haskell-distibuted/distributed-process/distributed-process-azure). The [Overview](https://github.com/haskell-distibuted/distributed-process/wiki/Overiview) provides links to a number of resources for learning about the conceptual underpinnings of Cloud Haskell, and some [examples](https://github.com/haskell-distibuted/distributed-process/distributed-process-demos). +[Windows Azure](https://github.com/haskell-distibuted/distributed-process-azure). The [Overview](https://github.com/haskell-distibuted/distributed-process/wiki/Overiview) provides links to a number of resources for learning about the conceptual underpinnings of Cloud Haskell, and some [examples](https://github.com/haskell-distibuted/distributed-process/distributed-process-demos). Documentation is available on this site for HEAD, or [hackage](http://hackage.haskell.org/package/distributed-process) for the current and preceding versions of diff --git a/semantics/CloudHaskellSemantics.pdf b/semantics/CloudHaskellSemantics.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6f01eb85dc30a0abf201ea29abc1a9bc530cb675 GIT binary patch literal 167226 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 z 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�G4a1d<IACqr>G!rr)ME{+uUH0#n1?6Vd7eZXBG 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 JRabvEE&%SN8}\!\!\texttt{>}\!\texttt{=}} {#2}} +\newcommand{\sReturn}{\mathtt{return}} +\newcommand{\sThrow}[1]{\mathtt{throw} \; {#1}} +\newcommand{\sCatch}[2]{\mathtt{catch} \; {#1} \; {#2}} + +\newcommand{\sExpect}{\mathtt{expect}} +\newcommand{\sSend}{\mathtt{send}} +\newcommand{\sSpawn}{\mathtt{spawn}} +\newcommand{\sLink}{\mathtt{link}} +\newcommand{\sReconnect}{\mathtt{reconnect}} +\newcommand{\sNewChan}{\mathtt{newChan}} +\newcommand{\sSendChan}{\mathtt{sendChan}} +\newcommand{\sReceiveChan}{\mathtt{receiveChan}} +\newcommand{\sMonitor}{\mathtt{monitor}} + +\DeclareMathOperator{\sNodeOf}{node} +\DeclareMathOperator{\sProcessOf}{process} + +\newcommand{\sSpawned}{\mathtt{spawned}} + +\newcommand{\sExtend}[1]{\mathrel{\triangleright_{#1}}} + +\newcommand{\sPar}{\mathrel{\parallel}} +\newcommand{\sProc}[2]{{#1}_{#2}} + +\newcommand{\sNid}{\ensuremath{\mathit{nid}}} +\newcommand{\sPid}{\ensuremath{\mathit{pid}}} +\newcommand{\sCid}{\ensuremath{\mathit{cid}}} + +\newcommand{\sId}{\ensuremath{\mathit{id}}} +\newcommand{\sRef}{\ensuremath{\mathit{ref}}} + +\newcommand{\sLinks}{\ensuremath{\mathit{links}}} +\newcommand{\sNode}[3]{\left[{#1} ; {#2}\right]_{#3}} +\newcommand{\sSystem}[4]{\left\langle #1 ; #2 ; #3 ; #4 \right\rangle} +\newcommand{\sNodes}{\mathcal{N}} +\newcommand{\sQueue}{\mathcal{Q}} +\newcommand{\sProcesses}{\mathcal{P}} +\newcommand{\sBlacklist}{\mathcal{B}} +\newcommand{\sMonitors}{\mathcal{M}} + +\newcommand{\sJust}[1]{\mathtt{Just} \; {#1}} +\newcommand{\sNothing}{\mathtt{Nothing}} + +\newcommand{\sCtxt}[1]{\mathbb{#1}} + +\newcommand{\sSenders}{\mathit{senders}} + +\newcommand{\OR}{\mathrel{|}} +\newcommand{\where}{\mathrel{|}} + +\floatstyle{boxed} +\restylefloat{figure} + +\lstset{basicstyle=\ttfamily\small} + +\begin{document} + +\title{Cloud Haskell Semantics (DRAFT)} +\author{Well-Typed LLP} +\date{\today} + +\maketitle + +\section{Introduction} + +Cloud Haskell brings Erlang-style concurrency to Haskell in the form of a +library.\footnote{\texttt{http://hackage.haskell.org/package/distributed-process}} +The original publication \cite{cloudhaskell} is a good introduction to Cloud +Haskell, and is accompanied by an ever-growing number of resouces, collected on +the Haskell +Wiki.\footnote{\texttt{http://www.haskell.org/haskellwiki/Cloud\_Haskell}} The +current document augments the original publication about Cloud Haskell by +giving a precise semantics to the Cloud Haskell primitives. It is meant as a +reference, not an introduction. + +The original Cloud Haskell paper stipulates that messages are ``asynchronous, +reliable, and buffered'', but does not describe how this can be achieved. +Understanding ``reliable'' to mean ``reliable ordered'' (or ``TCP-like''), +the reliability of message delivery comes from the reliability of the +underlying network protocol---up to a point. + +The problem is that the underlying network protocol is connection-oriented, but +Cloud Haskell is not. Intuitively, when $P$ sends a message to $Q$, we open a +reliable-ordered connection from $P$ to $Q$. Reliability of message delivery +now follows from reliability of the network protocol, until $P$ somehow gets +disconnected from $Q$. If $P$ now sends another message to $Q$, the +implementation cannot simply reconnect: after all, some messages that were sent +on the first connection might not have been delivered. This means that $P$ +might send $m_1, m_2, m_3$ to $Q$, but $Q$ will receive $m_1, +m_3$.\footnote{Indeed, message passing in Erlang is ordered but unreliable for +the same reason \cite{erlang}.} + +One (non-)solution is for $P$ to buffer all messages it sends to $Q$, and +remove messages from this buffer only when $Q$ acknowledges that it received +them. On a reconnect $P$ must ask which message $Q$ last received, and +retransmit the rest. This means that when $P$ gets disconnected from $Q$, it +must infinitely buffer all messages sent to $Q$ (until a connection is +reestablished). However, infinite buffering is too strong a requirement; +moreover, this is unsatisfactory because it means implementing a reliable +protocol on top of the underlying reliable network protocol. We would like a +different solution. + +Instead, Cloud Haskell does \emph{not} attempt to reconnect automatically, but +provides a $\sReconnect$ primitive which gives programmers the option of +reconnecting manually. This is an explicit acknowledgement from the programmer +that message loss might occur, and forces them to consider how such loss might +be dealt with. + +The semantics we present is based on \cite{unified}, which we will refer to the +as the ``Unified'' semantics. However, we will present the semantics in a more +``Haskell'' style following the semantics for STM \cite{stm}. It differs from +the Unified semantics in that +% +\begin{enumerate} +\item We introduce an explicit notion of \textit{reconnecting} (with potential +message loss) +\item We simplify the semantics: we ``flatten'' sets of nodes of processes as +sets of processes (but assume a mapping from process identifiers to node +identifiers), do not have per-process mailboxes (but only the system queue or +``ether'') and do not have an explicit concept of node controllers +\end{enumerate} +% +Our semantics differs from the STM semantics in that we pretend that the Cloud +Haskell \texttt{Process} monad is the top-level monad, and do not consider the +\texttt{IO} monad at all. Current imprecisions with respect to the ``real'' +Cloud Haskell are +% +\begin{enumerate} +\item We ignore the issue of serializability, other than to say that the +semantics will get stuck when trying to send a non-serializable payload; +consequently, we do not formalize $\mathtt{static}$ +\item We do not formalize all Cloud Haskell primitives (merging of ports, +``advanced messaging'', and others) +\item Some of the concepts that we do formalize are lower-level concepts; for +instance, the primitive $\sSpawn$ that we formalize is asynchronous (following +the Unified semantics); a synchronous construct can be derived. +\end{enumerate} + +\section{Preliminaries} + +Cloud Haskell Processes run on \emph{nodes}. Processes communicate by sending +messages to each other (directly or using typed channels). Processes can also +send messages to nodes (for instance, a request to spawn or monitor a process). + +We assume disjoint countable sets $\mathtt{NodeId}$, $\mathtt{ProcessId}$, and +$\mathtt{ChannelId}$, changed over by \sNid, \sPid{} and \sCid{} respectively, +and representing process identifiers, node identifiers, and (typed) channel +identifiers. We assume the existence of total functions +% +\begin{equation*} +\begin{array}{r@{\;:\;}l} +\sNodeOf & (\mathtt{ProcessId} \uplus \mathtt{ChannelId}) \rightarrow \mathtt{NodeId} \\ +\sProcessOf & \mathtt{ChannelId} \rightarrow \mathtt{ProcessId} +\end{array} +\end{equation*} +% +and define +$$\mathtt{Identifier} = \mathtt{NodeId} \uplus \mathtt{ProcessId} \uplus \mathtt{ChannelId}$$ +and let $\sId$ range over $\mathtt{Identifier}$. + +We represent a process as a pair $\sProc{M}{\sPid}$ of a term $M$ and a process +ID $\sPid$. We will denote a set of processes as +% + $$\sProc{M}{\sPid} \sPar \cdots \sPar \sProc{N}{\sPid'}$$ +% +A \emph{system} + $\sSystem{\sProcesses}{\sQueue}{\sBlacklist}{\sMonitors}$ +is a tuple containing a set $\sProcesses$ of processes, a \emph{system +queue} $\sQueue$, a \emph{blacklist} $\sBlacklist$, and a set of monitors +$\sMonitors$. +The set of monitors $\sMonitors$ is a set of tuples + $(\sId_\mathit{to}, \sPid_\mathit{fr}, \sNid, \sRef)$ +which records that node \sNid{} knows that process $\sPid_\mathit{fr}$ is +monitoring $\sId_\mathit{to}$ ($\sRef$ is the monitor reference). The system +queue is a set of triples $(\sId_\mathit{to}, \sId_\mathit{fr}, +\mathit{message})$ of messages that have been sent but not yet processed. The +blacklist records disconnections and is represented as a of pairs +$(\sId_\mathit{to}, \sId_\mathit{fr})$. + +\section{Semantics} + +We follow the STM semantics as closely as possible. The language is the same +except for the primitives considered, and we use the same concept of evaluation +contexts: + +\begin{equation*} +\begin{array}{llll} +\text{value} & V & ::= & \sId \OR + \sLam{x}{M} \OR + \sReturn \; M \OR + \sBind{M}{N} \OR + \sThrow{M} \OR \sCatch{M}{N} \OR \\ + &&& \sExpect \OR + \sSend \; \sPid \; M \OR + \sSpawn \; \sNid \; M \OR + \sMonitor \; \sId +\\ +\text{term} & M, N & ::= & x \OR + V \OR + \sApp{M}{N} \OR + \cdots \\ +\\ +\text{Evaluation} & \sCtxt{E} & ::= & [] \OR \sBind{\sCtxt{E}}{M} \OR \sCatch{\sCtxt{E}}{M} \\ +\text{context} & \sCtxt{P}_\sPid & ::= & \sCtxt{E}[]_\sPid \sPar \sProcesses +\end{array} +\end{equation*} +% +Indeed, the ``administrative'' transitions are identical +(Figure~\ref{fig:administrative}). + +\begin{figure} +\small +\begin{equation*} +\frac{ + \llbracket M \rrbracket = V \qquad + M \neq V +}{ + M \rightarrow V +} \textsc{Eval} +\qquad +\frac{ + M \rightarrow N +}{ + \sSystem{\sCtxt{P}[M]_\sPid} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[N]_\sPid} + {\sQueue} + {\sBlacklist} + {\sMonitors} +} \textsc{Admin} +\end{equation*} +% +\begin{equation*} +\begin{array}{r@{\;\rightarrow\;}ll@{\hspace{2em}}r@{\;\rightarrow\;}ll} +\sBind{\sReturn \; N}{M} & M \; N & \textsc{Bind} & \sCatch{(\sReturn \; M)}{N} & \sReturn \; M & \textsc{Catch}_1 \\ +\sBind{\sThrow{N}}{M} & \sThrow{N} & \textsc{Throw} & \sCatch{(\sThrow M)}{N} & N \; M & \textsc{Catch}_2 \\ +\end{array} +\end{equation*} +% +\caption{\label{fig:administrative}Administrative Transitions} +\end{figure} + +\subsection{Disconnect and Reconnect} + +Figure~\ref{fig:disconnect} gives the semantics for disconnecting and +reconnecting. Rule \textsc{Disconnect} models random network disconnect +between nodes $\sNid_1$ and $\sNid_2$. We assume that entire \emph{nodes} get +disconnected from each other, not individual processes. \emph{Reconnecting} +however is on a per connection basis (\textsc{Recon-Ex}). Connections to and +from node controllers can be implicitly reconnected ($\textsc{Recon-Im}_1$ and +$\textsc{Recon-Im}_2$). + +\begin{figure} +\small +\begin{equation*} +\frac{ + \sNid_1 \neq \sNid_2 +}{ + \sSystem{\sProcesses}{\sQueue}{\sBlacklist}{\sMonitors} +\rightarrow + \sSystem{\sProcesses}{\sQueue}{\sBlacklist \cup (\overline{\sNid_1} \times \overline{\sNid_2})}{\sMonitors} +} \textsc{Disconnect} +\end{equation*} +% +\begin{equation*} +\frac{ +}{ + \sSystem{\sCtxt{P}[ \sReconnect \; \sId_\mathit{to} ]_{\sPid_\mathit{fr}}} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; () ]_\sPid} + {\sQueue} + {\sBlacklist \backslash (\sId_\mathit{to}, \sPid_\mathit{fr})} + {\sMonitors} +} \textsc{Recon-Ex} +\end{equation*} +% +\begin{equation*} +\frac{ +}{ + \sSystem{\sProcesses}{\sQueue}{\sBlacklist, (\sNid_\mathit{to}, \sId_\mathit{fr})}{\sMonitors} +\rightarrow + \sSystem{\sProcesses}{\sQueue}{\sBlacklist}{\sMonitors} +} \textsc{Recon-Im}_1 +\qquad +\frac{ +}{ + \sSystem{\sProcesses}{\sQueue}{\sBlacklist, (\sId_\mathit{to}, \sNid_\mathit{fr})}{\sMonitors} +\rightarrow + \sSystem{\sProcesses}{\sQueue}{\sBlacklist}{\sMonitors} +} \textsc{Recon-Im}_2 +\end{equation*} +% +where +% +\begin{equation*} +\overline{\sNid} \subset \mathtt{Identifier} + = \{ \sNid \} \cup + \{ \sPid \where \sNodeOf(\sPid) = \sNid \} \cup + \{ \sCid \where \sNodeOf(\sCid) = \sNid \} +\end{equation*} +\caption{\label{fig:disconnect}Disconnect and Reconnect} +\end{figure} + +\subsection{Communication} + +The semantics for the basic primitives are listed in Figure~\ref{fig:basic}. +Once a connection has been blacklisted, no further messages can be sent across +that connection (until an explicit or implicit reconnect). + +Message passing is ordered but only for a given sender and receiver; no +ordering guarantees exist between messages sent by different processes. For +that reason rules \textsc{Expect}, \textsc{ReceiveChan} and \textsc{Spawn-Exec} +choose the \emph{first} message of a \emph{randomly chosen} sender. + +Since the semantics is not type-driven, we represent typed channels simply as +an identifier with an annotation whether it is the send-end ($\sCid^s$) or the +receive end ($\sCid^r$) of the channel (rules \textsc{NewChan}, +\textsc{SendChan} and \textsc{ReceiveChan}). + +Spawning finally is asynchronous. When process $P$ spawns process $Q$ on node +$\sNid$ (rule \textsc{Spawn-Async}) a message is sent to node $\sNid$ (which +may, of course, never arrive). When the remote node receives the message and +actually spawns $Q$ (rule \textsc{Spawn-Exec}) it sends a message back to $P$ +with $Q$'s process ID. In Cloud Haskell this primitive is called +\texttt{spawnAsync}, and the Cloud Haskell \texttt{spawn} primitive is defined +in terms of \texttt{spawnAsync} (we do not consider the synchronous version in +this document). + +\begin{figure} +\small +\begin{equation*} +\frac{ +}{ + \sSystem{\sCtxt{P}[ \sSend \; \sPid_\mathit{to} \; M ]_{\sPid_\mathit{fr}}} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; () ]_{\sPid_\mathit{fr}}} + {\sQueue \sExtend{\sBlacklist} (\sPid_\mathit{to}, \sPid_\mathit{fr}, M)} + {\sBlacklist} + {\sMonitors} +} \textsc{Send} +\end{equation*} +% +\begin{equation*} +\frac{ + \sId_{\mathit{fr}} \notin \sSenders(\sQueue) +}{ + \sSystem{\sCtxt{P}[ \sExpect ]_{\sPid_\mathit{to}}} + {\sQueue, (\sPid_\mathit{to}, \sId_\mathit{fr}, M), \sQueue'} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; M ]_{\sPid_\mathit{to}}} + {\sQueue, \sQueue'} + {\sBlacklist} + {\sMonitors} +} \textsc{Expect} +\end{equation*} +% +\begin{equation*} +\frac{ + \sCid \text{ fresh} +\qquad + \sProcessOf(\sCid) = \sPid +}{ + \sSystem{\sCtxt{P}[\sNewChan]_\sPid} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[\sReturn \; (\sCid^s, \sCid^r)]_\sPid} + {\sQueue} + {\sBlacklist} + {\sMonitors} +} \textsc{NewChan} +\end{equation*} + +\begin{equation*} +\frac{ +}{ + \sSystem{\sCtxt{P}[ \sSendChan \; \sCid_\mathit{to}^s \; M ]_{\sPid_\mathit{fr}}} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; () ]_{\sPid_\mathit{fr}}} + {\sQueue \sExtend{\sBlacklist} (\sCid_\mathit{to}, \sPid_\mathit{fr}, M)} + {\sBlacklist} + {\sMonitors} +} \textsc{SendChan} +\end{equation*} + +\begin{equation*} +\frac{ + \sPid_{\mathit{fr}} \notin \sSenders(\sQueue) +}{ + \sSystem{\sCtxt{P}[ \sReceiveChan \; \sCid_\mathit{to}^r ]_{\sPid}} + {\sQueue, (\sCid_\mathit{to}, \sPid_\mathit{fr}, M), \sQueue'} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; M ]_{\sPid}} + {\sQueue, \sQueue'} + {\sBlacklist} + {\sMonitors} +} \textsc{ReceiveChan} +\end{equation*} +% +\begin{equation*} +\frac{ + \sRef \text{ fresh} +}{ + \sSystem{\sCtxt{P}[ \sSpawn \; \sNid \; M ]_\sPid} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; \sRef ]_\sPid} + {\sQueue \sExtend{\sBlacklist} (\sNid, \sPid, \sSpawn \; \sRef \; M)} + {\sBlacklist} + {\sMonitors} +} \textsc{Spawn-Async} +\end{equation*} +% +\begin{equation*} +\frac{ + \sPid \notin \sSenders(\sQueue) +\qquad + \sPid' \text{ fresh} +\qquad + \sNodeOf(\sPid') = \sNid +}{ + \sSystem{\sProcesses} + {\sQueue, (\sNid, \sPid, \sSpawn \; \sRef \; M), \sQueue'} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sProcesses \sPar M_{\sPid'}} + {\sQueue, \sQueue' \sExtend{\sBlacklist} (\sPid, \sNid, \sSpawned \; \sRef \; \sPid')} + {\sBlacklist} + {\sMonitors} +} \textsc{Spawn-Exec} +\end{equation*} +% +where +% +\begin{equation*} +\begin{array}{l@{\;=\;}l@{\hspace{3em}}l} + \sQueue \sExtend{\sBlacklist} (\sId_\mathit{to}, \sId_\mathit{fr}, M) +& + \sQueue, (\sId_\mathit{to}, \sId_\mathit{fr}, M) +& + \text{if } (\sId_\mathit{to}, \sId_\mathit{fr}) \notin \sBlacklist +\\ + \sQueue \sExtend{\sBlacklist} (\sId_\mathit{to}, \sId_\mathit{fr}, M) +& + \sQueue +& + \text{otherwise} +\end{array} +\end{equation*} +% +$(\sExtend{\sBlacklist})$ is only defined for serializable payloads. +\caption{\label{fig:basic}Basic Primitives} +\end{figure} + +\subsection{Monitoring} + +When process $P$ on node $\sNid_P$ wants to monitor process $Q$ on node +$\sNid_Q$ \emph{both} nodes must be notified. The local node it notified +immediately, and a message is sent to to the remote node (rules +\textsc{Mon-Loc} and \textsc{Mon-Rem}). As with all messages, the +message to the remote node might be lost. When $P$ is disconnected from $Q$ it +is the responsibility of $P$'s local node $\sNid_P$ to notify $P$ +(\textsc{Mon-Dis}); when $Q$ terminates (\textsc{Mon-Ret}) or crashes +(\textsc{Mon-Throw}) it is the responsibility of the remote node $\sNid_Q$ to +notify $P$. + +\begin{figure} +\small +\begin{equation*} +\frac{ + \sNid_\mathit{fr} = \sNodeOf(\sPid_\mathit{fr}) +\qquad + \sNid_\mathit{to} = \sNodeOf(\sId_\mathit{to}) +\qquad + \sRef \text{ fresh} +}{ +\sSystem{\sCtxt{P}[ \sMonitor \; \sId_\mathit{to} ]_{\sPid_\mathit{fr}}} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sCtxt{P}[ \sReturn \; \sRef ]_{\sPid_\mathit{fr}}} + {\sQueue \sExtend{\sBlacklist} (\sNid_\mathit{to}, \sPid_\mathit{fr}, \sMonitor \; \sRef \; \sId_\mathit{to})} + {\sBlacklist} + {\sMonitors, (\sId_\mathit{to}, \sPid_\mathit{fr}, \sNid_\mathit{fr}, \sRef)} +} \textsc{Mon-Loc} +\end{equation*} +% +\begin{equation*} +\frac{ + \sPid_\mathit{fr} \notin \sSenders(\sQueue) +}{ + \sSystem{\sProcesses} + {\sQueue, (\sNid_\mathit{to}, \sPid_\mathit{fr}, \sMonitor \; \sRef \; \sId_\mathit{to}), \sQueue'} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sProcesses} + {\sQueue, \sQueue'} + {\sBlacklist} + {\sMonitors, (\sId_\mathit{to}, \sPid_\mathit{fr}, \sNid_\mathit{to}, \sRef)} +} \textsc{Mon-Rem} +\end{equation*} +% +\begin{equation*} +\frac{ + \sNid_\mathit{fr} = \sNodeOf(\sPid_\mathit{fr}) +\qquad + (\sId_\mathit{to}, \sPid_\mathit{fr}) \in \sBlacklist +}{ + \sSystem{\sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors, (\sId_\mathit{to}, \sPid_\mathit{fr}, \sNid_\mathit{fr}, \sRef)} +\rightarrow + \sSystem{\sProcesses} + {\sQueue, (\sPid_\mathit{fr}, \sId_\mathit{to}, \mathtt{discon} \; \sRef)} + {\sBlacklist} + {\sMonitors} +} \textsc{Mon-Dis} +\end{equation*} +% +\begin{equation*} +\frac{ + \sNodeOf(\sPid) = \sNid +}{ + \sSystem{{(\sReturn \; ())}_\sPid \sPar \sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors, (\sPid, \sPid', \sNid, \sRef)} +\rightarrow + \sSystem{{(\sReturn \; ())}_\sPid \sPar \sProcesses} + {\sQueue \sExtend{\sBlacklist} (\sPid', \sNid, \mathtt{died} \; \sRef)} + {\sBlacklist} + {\sMonitors} +} \textsc{Mon-Ret} +\end{equation*} +% +\begin{equation*} +\frac{ + \sNodeOf(\sPid) = \sNid +}{ + \sSystem{{(\sThrow{M})}_\sPid \sPar \sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors, (\sPid, \sPid', \sNid, \sRef)} +\rightarrow + \sSystem{{(\sThrow{M})}_\sPid \sPar \sProcesses} + {\sQueue \sExtend{\sBlacklist} (\sPid', \sNid, \mathtt{exc} \; \sRef \; M)} + {\sBlacklist} + {\sMonitors} +} \textsc{Mon-Throw} +\end{equation*} +% +\begin{equation*} +\frac{ + \sNodeOf(\sPid) = \sNid +\qquad + \sPid \notin \sProcesses +}{ + \sSystem{\sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors, (\sPid, \sPid', \sNid, \sRef)} +\rightarrow + \sSystem{\sProcesses} + {\sQueue \sExtend{\sBlacklist} (\sPid', \sNid, \mathtt{unknown} \; \sRef)} + {\sBlacklist} + {\sMonitors} +} \textsc{Mon-Unknown} +\end{equation*} +% +\caption{Monitoring} +\end{figure} + +\subsection{Process Termination} + +The rules for normal and abnormal process termination are defined in +Figure~\ref{fig:termination}. When a process crashes it dies +silently, unless monitors are setup. + +\begin{figure} +\small +% +\begin{equation*} +\frac{ + \sNodeOf(\sPid) = \sNid +\qquad + \nexists \; \sPid', \sRef \cdot (\sPid', \sPid, \sNid, \sRef) \in \sMonitors +}{ + \sSystem{{(\sReturn \; ())}_\sPid \sPar \sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors} +} \textsc{TermRet} +\end{equation*} +% +\begin{equation*} +\frac{ + \sNodeOf(\sPid) = \sNid +\qquad + \nexists \; \sPid', \sRef \cdot (\sPid', \sPid, \sNid, \sRef) \in \sMonitors +}{ + \sSystem{{(\sThrow{M})}_\sPid \sPar \sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors} +\rightarrow + \sSystem{\sProcesses} + {\sQueue} + {\sBlacklist} + {\sMonitors} +} \textsc{TermThrow} +\end{equation*} +% +\caption{\label{fig:termination}Process Termination} +\end{figure} + +\section{Open Issues} + +\subsection{Ordering of Monitor/Link Notifications} + +The semantics as described above, like the original Unified semantics, does not +guarantee that messages send from process $P$ to process $Q$ (\textsc{Send}) +are ordered with respect to the link or monitor notification sent when process +$P$ terminates normally or abnormally (\textsc{Mon-Ret}, \textsc{Mon-Throw}). This means that if process $P$ does + +\begin{lstlisting} +receiveWait [ + match $ \(Reply reply) -> ... + , match $ \(ProcessMonitorNotification ..) -> ... + ] +\end{lstlisting} + +and process $Q$ does + +\begin{lstlisting} +send pidA (Reply reply) +// terminate or indeed throw an exception +\end{lstlisting} + +then the semantics does not guarantee that the reply from process $Q$ will +arrive at process $P$ before the monitor notification; hence, this results in +an (artificial) race condition in process $P$. + +One possible solution is to regard such a link notification as a message from +process $Q$ to process $P$, which should be ordered along with the other +messages. + +\subsection{Ordering and Typed Channels} + +The situation is more tricky still than sketched above because we of typed +channels. The semantics does not provide ordering guarantees between messages +sent directly to the process and messages sent on typed channels. That means +that even if we consider a link or monitor notification as a message sent to +the process, to be ordered with other messages sent to that process, it is +still unordered with respect to messages sent on typed channels. This means +that we have similar race conditions\footnote{Even ignoring the fact that we +currently don't even provide a way to wait for a message on a typed channel +\emph{or} a direct message.} + +A possible solution is to insist that \emph{all} messages from process $P$ to +process $Q$ are ordered, no matter how they are sent. From a implementation +point of view, this would entail the use of a single ordered network connection +for all messages from $P$ to $Q$, rather than using an ordered connection per +typed channel plus one for direct messages. + +\bibliographystyle{apalike} +\bibliography{references} + +\end{document} diff --git a/semantics/Makefile b/semantics/Makefile new file mode 100644 index 00000000..1f553a28 --- /dev/null +++ b/semantics/Makefile @@ -0,0 +1,11 @@ +CloudHaskellSemantics.pdf: *.tex *.bib + pdflatex CloudHaskellSemantics + bibtex CloudHaskellSemantics + pdflatex CloudHaskellSemantics + pdflatex CloudHaskellSemantics + +.PHONY: clean +clean: + rm -f *.aux *.bbl *.blg *.log + +# vi:set noexpandtab: diff --git a/semantics/references.bib b/semantics/references.bib new file mode 100644 index 00000000..0ac0c680 --- /dev/null +++ b/semantics/references.bib @@ -0,0 +1,72 @@ +@inproceedings{cloudhaskell, + author = {Epstein, Jeff and Black, Andrew P. and Peyton-Jones, Simon}, + title = {Towards {H}askell in the cloud}, + booktitle = {Proceedings of the 4th ACM symposium on Haskell}, + series = {Haskell '11}, + year = {2011}, + isbn = {978-1-4503-0860-1}, + location = {Tokyo, Japan}, + pages = {118--129}, + numpages = {12}, + url = {http://doi.acm.org/10.1145/2034675.2034690}, + doi = {10.1145/2034675.2034690}, + acmid = {2034690}, + publisher = {ACM}, + address = {New York, NY, USA}, + keywords = {erlang, haskell, message-passing}, +} + +@inproceedings{unified, + author = {Svensson, Hans and Fredlund, Lars-{\AA}ke and Benac Earle, Clara}, + title = {A unified semantics for future {E}rlang}, + booktitle = {Proceedings of the 9th ACM SIGPLAN workshop on Erlang}, + series = {Erlang '10}, + year = {2010}, + isbn = {978-1-4503-0253-1}, + location = {Baltimore, Maryland, USA}, + pages = {23--32}, + numpages = {10}, + url = {http://doi.acm.org/10.1145/1863509.1863514}, + doi = {10.1145/1863509.1863514}, + acmid = {1863514}, + publisher = {ACM}, + address = {New York, NY, USA}, + keywords = {erlang, semantics}, +} + +@article{stm, + author = {Harris, Tim and Marlow, Simon and Jones, Simon Peyton and Herlihy, Maurice}, + title = {Composable memory transactions}, + journal = {Commun. ACM}, + issue_date = {August 2008}, + volume = {51}, + number = {8}, + month = aug, + year = {2008}, + issn = {0001-0782}, + pages = {91--100}, + numpages = {10}, + url = {http://doi.acm.org/10.1145/1378704.1378725}, + doi = {10.1145/1378704.1378725}, + acmid = {1378725}, + publisher = {ACM}, + address = {New York, NY, USA}, +} + +@inproceedings{erlang, + author = {Svensson, Hans and Fredlund, Lars-\AAke}, + title = {A more accurate semantics for distributed erlang}, + booktitle = {Proceedings of the 2007 SIGPLAN workshop on ERLANG Workshop}, + series = {ERLANG '07}, + year = {2007}, + isbn = {978-1-59593-675-2}, + location = {Freiburg, Germany}, + pages = {43--54}, + numpages = {12}, + url = {http://doi.acm.org/10.1145/1292520.1292528}, + doi = {10.1145/1292520.1292528}, + acmid = {1292528}, + publisher = {ACM}, + address = {New York, NY, USA}, + keywords = {distributed systems, erlang, model checking, semantics, verification}, +} diff --git a/static/tutorial/tutorial-client.hs b/static/tutorial/tutorial-client.hs new file mode 100644 index 00000000..b5677ae7 --- /dev/null +++ b/static/tutorial/tutorial-client.hs @@ -0,0 +1,24 @@ +import Network.Transport +import Network.Transport.TCP (createTransport, defaultTCPParameters) +import System.Environment +import Control.Monad +import Data.ByteString.Char8 + +main :: IO () +main = do + [host, port, serverAddr] <- getArgs + Right transport <- createTransport host port defaultTCPParameters + Right endpoint <- newEndPoint transport + + let addr = EndPointAddress (pack serverAddr) +-- Right conn <- connect endpoint addr ReliableOrdered defaultConnectHints + x <- connect endpoint addr ReliableOrdered defaultConnectHints + let conn = case x of + Right conn -> conn + Left err -> error$ "Error connecting: "++show err + send conn [pack "Hello world"] + close conn + + replicateM_ 3 $ receive endpoint >>= print + + closeTransport transport diff --git a/static/tutorial/tutorial-server.hs b/static/tutorial/tutorial-server.hs new file mode 100644 index 00000000..1b7e0652 --- /dev/null +++ b/static/tutorial/tutorial-server.hs @@ -0,0 +1,54 @@ +import Network.Transport +import Network.Transport.TCP (createTransport, defaultTCPParameters) +import Control.Concurrent +import Data.Map +import Control.Exception +import System.Environment + +-- | Server that echoes messages straight back to the origin endpoint. +echoServer :: EndPoint -> MVar () -> IO () +echoServer endpoint serverDone = go empty + where + go :: Map ConnectionId (MVar Connection) -> IO () + go cs = do + event <- receive endpoint + case event of + ConnectionOpened cid rel addr -> do + putStrLn$ " New connection: ID "++show cid++", reliability: "++show rel++", address: "++ show addr + connMVar <- newEmptyMVar + forkIO $ do + Right conn <- connect endpoint addr rel defaultConnectHints + putMVar connMVar conn + go (insert cid connMVar cs) + Received cid payload -> do + forkIO $ do + conn <- readMVar (cs ! cid) + send conn payload + return () + go cs + ConnectionClosed cid -> do + putStrLn$ " Closed connection: ID "++show cid + forkIO $ do + conn <- readMVar (cs ! cid) + close conn + go (delete cid cs) + EndPointClosed -> do + putStrLn "Echo server exiting" + putMVar serverDone () + +onCtrlC :: IO a -> IO () -> IO a +p `onCtrlC` q = catchJust isUserInterrupt p (const $ q >> p `onCtrlC` q) + where + isUserInterrupt :: AsyncException -> Maybe () + isUserInterrupt UserInterrupt = Just () + isUserInterrupt _ = Nothing + +main :: IO () +main = do + [host, port] <- getArgs + serverDone <- newEmptyMVar + Right transport <- createTransport host port defaultTCPParameters + Right endpoint <- newEndPoint transport + forkIO $ echoServer endpoint serverDone + putStrLn $ "Echo server started at " ++ show (address endpoint) + readMVar serverDone `onCtrlC` closeTransport transport diff --git a/tutorials/1.tutorial.md b/tutorials/tutorial1.md similarity index 99% rename from tutorials/1.tutorial.md rename to tutorials/tutorial1.md index 1c30b612..8230dc1c 100644 --- a/tutorials/1.tutorial.md +++ b/tutorials/tutorial1.md @@ -1,5 +1,5 @@ --- -layout: tutorial +layout: tutorial1 categories: tutorial title: Getting Started --- diff --git a/tutorials/2.nt_tutorial.md b/tutorials/tutorial2.md similarity index 99% rename from tutorials/2.nt_tutorial.md rename to tutorials/tutorial2.md index 68d1346a..d30ddafc 100644 --- a/tutorials/2.nt_tutorial.md +++ b/tutorials/tutorial2.md @@ -1,5 +1,5 @@ --- -layout: nt_tutorial +layout: tutorial2 categories: tutorial title: Programming with Network.Transport --- diff --git a/tutorials/3.managedprocess.md b/tutorials/tutorial3.md similarity index 99% rename from tutorials/3.managedprocess.md rename to tutorials/tutorial3.md index a1e90fa8..93826016 100644 --- a/tutorials/3.managedprocess.md +++ b/tutorials/tutorial3.md @@ -1,5 +1,5 @@ --- -layout: managedprocess +layout: tutorial3 categories: tutorial title: Managed Process Tutorial --- diff --git a/wiki/networktransport.md b/wiki/networktransport.md index 640abe75..ed70eaf0 100644 --- a/wiki/networktransport.md +++ b/wiki/networktransport.md @@ -105,6 +105,282 @@ Note however that the goal of `Network.Transport` is _not_ to provide a general If you are interested in helping out, please add a brief paragraph to [Applications and Other Protocols][7] so that we can coordinate the efforts. +-------- + +### The TCP Transport + +#### Overview + +When a TCP transport is created a new server is started which listens on a +given port number on the local host. In order to support multiple connections +the transport maintains a set of channels, one per connection, represented as a +pair of a `MVar [ByteString]` and a list of pairs `(ThreadId, Socket)` of +threads that are listening on this channel. A source end then corresponds to a +hostname (the hostname used by clients to identity the local host), port +number, and channel ID; a receive end simply corresponds to a channel ID. + +When `mkTransport` creates a new transport it spawns a thread that listens for +incoming connections, running `procConnections` (see below). The set of +channels (connections) associated with the transport is initialized to be +empty. + +`newConnectionWith` creates a new channel and add its to the transport channel +map (with an empty list of associated threads). + +To serialize the source end we encode the triple of the local host name, port +number, and channel ID, and to deserialize we just decode the same triple +(deserialize does not need any other properties of the TCP transport). + +To connect to the source end we create a new socket, connect to the server at +the IP address specified in the TCPConfig, and send the channel number over the +connection. Then to `closeSourceEnd` we simply close the socket, and to send a +bunch of byte strings we output them on the socket. + +To receive from the target end we just read from the channel associated with +the target end. To `closeTargetEnd` we find kill all threads associated with +the channel and close their sockets. + +When somebody connects to server (running `procConnections`), he first sends a +channel ID. `procConnections` then spawns a new thread running +`procMessages` which listens for bytestrings on the socket and output them on +the specified channel. The ID of this new thread (and the socket it uses) are +added to the channel map of the transport. + +`closeTransport` kills the server thread and all threads that were listening on +the channels associated with the transport, and closes all associated sockets. + +#### Improving Latency + +A series of benchmarks has shown that + +* The use of `-threaded` triples the latency. + +* Prepending a header to messages has a negligible effect on latency, even when + sending very small packets. However, the way that we turn the length from an + `Int32` to a `ByteString` _does_ have a significant impact; in particular, + using `Data.Serialize` is very slow (and using Blaze.ByteString not much + better). This is fast: + +{% highlight haskell %} +foreign import ccall unsafe "htonl" htonl :: CInt -> CInt + +encodeLength :: Int32 -> IO ByteString +encodeLength i32 = + BSI.create 4 $ \p -> + pokeByteOff p 0 (htonl (fromIntegral i32)) +{% endhighlight %} + +* We do not need to use `blaze-builder` or related; + `Network.Socket.Bytestring.sendMany` uses vectored I/O. On the client side + doing a single `recv` to try and read the message header and message, rather + one to read the header and one to read the payload improves latency, but only + by a tiny amount. + +* Indirection through an `MVar` or a `Chan` does not have an observable effect + on latency. + +* When two nodes _A_ and _B_ communicate, latency is worse when they + communicate over two pairs of sockets (used unidirectionally) rather than one + pair (used bidirectionally) by about 20%. This is not improved by using + `TCP_NODELAY`, and might be because [acknowledgements cannot piggyback with + payload][9] this way. It might thus be worthwhile to try and reuse TCP + connections (or use UDP). + +---- + +### Adding Support for Multicast to the Transport API + +Here we describe various design options for adding support for multicast +to the Transport API. + +#### Creating a new multicast group + +We can either have this as part of the transport + +{% highlight haskell %} + data Transport = Transport { + ... + , newMulticastGroup :: IO (Either Error MulticastGroup) + } + + data MulticastGroup = MulticastGroup { + ... + , multicastAddress :: MulticastAddress + , deleteMulticastGroup :: IO () + } +{% endhighlight %} + +or as part of an endpoint: + +{% highlight haskell %} + data Transport = Transport { + newEndPoint :: IO (Either Error EndPoint) + } + + data EndPoint = EndPoint { + ... + , newMulticastGroup :: IO (Either Error MulticastGroup) + } +{% endhighlight %} + +It should probably be part of the `Transport`, as there is no real connection +between an endpoint and the creation of the multigroup (however, see section +"Sending messages to a multicast group"). + +#### Subscribing to a multicast group + +This should be part of an endpoint; subscribing basically means that the +endpoint wants to receive events when multicast messages are sent. + +We could reify a subscription: + +{% highlight haskell %} + data EndPoint = EndPoint { + ... + , multicastSubscribe :: MulticastAddress -> IO MulticastSubscription + } + + data MulticastSubscription = MulticastSubscription { + ... + , multicastSubscriptionClose :: IO () + } +{% endhighlight %} + +but this suggests that one might have multiple subscriptions to the same group +which can be distinguished, which is misleading. Probably better to have: + +{% highlight haskell %} + data EndPoint = EndPoint { + multicastSubscribe :: MulticastAddress -> IO () + , multicastUnsubscribe :: MulticastAddress -> IO () + } +{% endhighlight %} + +#### Sending messages to a multicast group + +An important feature of the Transport API is that we are clear about which +operations are *lightweight* and which are not. For instance, creating new +endpoints is not lightweight, but opening new connections to endpoints is (as +light-weight as possible). + +Clearly the creation of a new multicast group is a heavyweight operation. It is +less evident however if we can support multiple lightweight "connections" to the +same multicast group, and if so, whether it is useful. + +If we decide that multiple lightweight connections to the multigroup is useful, +one option might be + +{% highlight haskell %} + data EndPoint = EndPoint { + ... + , connect :: Address -> Reliability -> IO (Either Error Connection) + , multicastConnect :: MulticastAddress -> IO (Either Error Connection) + } + + data Connection = Connection { + connectionId :: ConnectionId + , send :: [ByteString] -> IO () + , close :: IO () + , maxMsgSize :: Maybe Int + } + + data Event = + Receive ConnectionId [ByteString] + | ConnectionClosed ConnectionId + | ConnectionOpened ConnectionId ConnectionType Reliability Address +{% endhighlight %} + +The advantage of this approach is it's consistency with the rest of the +interface. The problem is that with multicast we cannot reliably send any +control messages, so we cannot make sure that the subscribers of the multicast +group will receive ConnectionOpened events when an endpoint creates a new +connection. Since we don't support these "connectionless connections" anywhere +else in the API this seems inconsistent with the rest of the design (this +implies that an "unreliable" Transport over UDP still needs to have reliable +control messages). (On the other hand, if we were going to support reliable +multicast protocols, then that would fit this design). + +If we don't want to support multiple lightweight connections to a multicast +group then a better design would be + +{% highlight haskell %} + data EndPoint = EndPoint { + , connect :: Address -> Reliability -> IO (Either Error Connection) + , multicastSend :: MulticastAddress -> [ByteString] -> IO () + } + + data Event = + ... + | MulticastReceive Address [ByteString] +{% endhighlight %} + +or alternatively + +{% highlight haskell %} + data EndPoint = EndPoint { + ... + , resolveMulticastGroup :: MulticastAddress -> IO (Either Error MulticastGroup) + } + + data MulticastGroup = MulticastGroup { + , ... + , send :: [ByteString] -> IO () + } +{% endhighlight %} + +If we do this however we need to make sure that newGroup is part an `EndPoint`, +not the `Transport`, otherwise `send` will not know the source of the message. +The version with `resolveMulticastGroup` has the additional benefit that in +"real" implementations we will probably need to allocate some resources before +we can send to the multicast group, and need to deallocate these resources at +some point too. + +### The current solution + +The above considerations lead to the following tentative proposal: + +{% highlight haskell %} + data Transport = Transport { + newEndPoint :: IO (Either Error EndPoint) + } + + data EndPoint = EndPoint { + receive :: IO Event + , address :: Address + , connect :: Address -> Reliability -> IO (Either Error Connection) + , newMulticastGroup :: IO (Either Error MulticastGroup) + , resolveMulticastGroup :: MulticastAddress -> IO (Either Error MulticastGroup) + } + + data Connection = Connection { + send :: [ByteString] -> IO () + , close :: IO () + } + + data Event = + Receive ConnectionId [ByteString] + | ConnectionClosed ConnectionId + | ConnectionOpened ConnectionId Reliability Address + | MulticastReceive MulticastAddress [ByteString] + + data MulticastGroup = MulticastGroup { + multicastAddress :: MulticastAddress + , deleteMulticastGroup :: IO () + , maxMsgSize :: Maybe Int + , multicastSend :: [ByteString] -> IO () + , multicastSubscribe :: IO () + , multicastUnsubscribe :: IO () + , multicastClose :: IO () + } +{% endhighlight %} + +where `multicastClose` indicates to the runtime that this endpoint no longer +wishes to send to this multicast group, and we can therefore deallocate the +resources we needed to send to the group (these resources can be allocated on +`resolveMulticastGroup` or on the first `multicastSend`; the advantage of the +latter is that is somebody resolves a group only to subscribe to it, not to +send to it, we don't allocate any unneeded resources). + [1]: http://www.olcf.ornl.gov/center-projects/common-communication-interface/ [2]: https://groups.google.com/forum/?fromgroups#!forum/parallel-haskell [3]: http://cloud-haskell.atlassian.net @@ -113,3 +389,4 @@ If you are interested in helping out, please add a brief paragraph to [6]: /wiki/newdesign.html [7]: /wiki/protocols.html [8]: https://cloud-haskell.atlassian.net/issues/?filter=10002 +[9]: http://lists.freebsd.org/pipermail/freebsd-hackers/2009-March/028006.html "2 uni-directional TCP connection good?" From ca63d57d675f1cb9ab8c15b48eec2ca750c3d21b Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:04:01 +0000 Subject: [PATCH 0955/2357] host tutorial code on the site for now --- tutorials/tutorial2.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tutorials/tutorial2.md b/tutorials/tutorial2.md index d30ddafc..5ae1ce84 100644 --- a/tutorials/tutorial2.md +++ b/tutorials/tutorial2.md @@ -9,9 +9,8 @@ title: Programming with Network.Transport This is a tutorial introduction to `Network.Transport`. To follow along, you should probably already be familiar with `Control.Concurrent`; in particular, the use of `fork` and `MVar`s. The code for the tutorial can -be downloaded as [tutorial-server.hs](https://github.com/haskell-distributed/distributed-process/blob/master/doc/tutorial/tutorial-server.hs) -and -[tutorial-client.hs](https://github.com/haskell-distributed/distributed-process/blob/master/doc/tutorial/tutorial-client.hs). +be downloaded as [tutorial-server.hs](/static/tutorial/tutorial-server.hs) +and [tutorial-client.hs](/static/tutorial/tutorial-client.hs). ------- From b867e2b5b479159fc49186f337c8130b2cc769fa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:18:56 +0000 Subject: [PATCH 0956/2357] add svg version of the network transport pic --- img/NetworkTCP.svg | 657 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 657 insertions(+) create mode 100644 img/NetworkTCP.svg diff --git a/img/NetworkTCP.svg b/img/NetworkTCP.svg new file mode 100644 index 00000000..da33d2fb --- /dev/null +++ b/img/NetworkTCP.svg @@ -0,0 +1,657 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + 198.51.100.1:1080 + 198.51.100.1:1081 + + 198.51.100.2:1080 + + 198.51.100.1:1080:0 + + 198.51.100.1:1081:0 + + 198.51.100.2:1080:0 + + 198.51.100.2:1080:1 + + + + + + + + + + + + + + + + + + + Endpoint + Transport + TCP connection(bi-directional) + + Transport connection(uni-directional) + Chan + + From 6bb5d4f5a897de292c710f0a8b55676d7b22660e Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:56:59 +0000 Subject: [PATCH 0957/2357] only build master+development on ci --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,7 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true +branches: + only: + - master + - development From 2e38188273a336e888038118f3124164d7c17f8f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:57:43 +0000 Subject: [PATCH 0958/2357] only build master+development on ci --- .travis.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index bad4a75d..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,14 @@ language: haskell script: "make ci" notifications: - irc: - channels: - - "irc.freenode.org#haskell-distributed" - use_notice: true - email: - recipients: - - cloud.haskell@gmail.com + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true +branches: + only: + - master + - development From 492bea3a1a08f737beb52d1bbe8077c04b0a5c43 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:58:13 +0000 Subject: [PATCH 0959/2357] only build master+development on ci --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,7 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true +branches: + only: + - master + - development From 35bfc019c524f0bc683c54a7a73421715e226ddf Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 22:59:23 +0000 Subject: [PATCH 0960/2357] only build master+development on ci --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,7 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true +branches: + only: + - master + - development From f77986b06a12581556bda0341da6dbde2b4cc237 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:00:22 +0000 Subject: [PATCH 0961/2357] only build master+development on ci --- .travis.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index bad4a75d..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,14 @@ language: haskell script: "make ci" notifications: - irc: - channels: - - "irc.freenode.org#haskell-distributed" - use_notice: true - email: - recipients: - - cloud.haskell@gmail.com + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true +branches: + only: + - master + - development From 629cc3bcb42acdf30c6a84d97e3319a6adefdd11 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:20:35 +0000 Subject: [PATCH 0962/2357] add travis config --- .travis.yml | 13 +++++++++++++ Makefile | 33 ++++++++++----------------------- REPOS | 4 ++++ 3 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 REPOS diff --git a/.travis.yml b/.travis.yml index 999bd37b..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,14 @@ language: haskell +script: "make ci" +notifications: + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true +branches: + only: + - master + - development diff --git a/Makefile b/Makefile index e30299a5..9756f8b0 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,10 @@ -## ---------------------------------------------------------------------------- -## -## Copyright (c) 2005 - 2012 Nebularis. -## -## Permission is hereby granted, free of charge, to any person obtaining a copy -## of this software and associated documentation files (the "Software"), deal -## in the Software without restriction, including without limitation the rights -## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -## copies of the Software, and to permit persons to whom the Software is -## furnished to do so, subject to the following conditions: -## -## The above copyright notice and this permission notice shall be included in -## all copies or substantial portions of the Software. -## -## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -## IN THE SOFTWARE. -## ---------------------------------------------------------------------------- - CONF=./dist/setup-config CABAL=distributed-process-platform.cabal BUILD_DEPENDS=$(CONF) $(CABAL) +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + .PHONY: all all: build @@ -39,9 +19,16 @@ build: configure .PHONY: configure configure: $(BUILD_DEPENDS) +.PHONY: ci +ci: $(REPOS) test + $(BUILD_DEPENDS): cabal configure --enable-tests +$(REPOS): + git clone $(BASE_GIT)/$@.git + cabal install ./$@ --force-reinstalls + .PHONY: clean clean: cabal clean diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..9f9c5707 --- /dev/null +++ b/REPOS @@ -0,0 +1,4 @@ +rank1dynamic +distributed-static +network-transport +network-transport-tcp From 307e858b22e727d9ccb9227ba32e4ea2cf385ad5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:20:35 +0000 Subject: [PATCH 0963/2357] add travis config --- .travis.yml | 13 +++++++++++++ Makefile | 33 ++++++++++----------------------- REPOS | 4 ++++ 3 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 REPOS diff --git a/.travis.yml b/.travis.yml index 999bd37b..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,14 @@ language: haskell +script: "make ci" +notifications: + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true +branches: + only: + - master + - development diff --git a/Makefile b/Makefile index e30299a5..9756f8b0 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,10 @@ -## ---------------------------------------------------------------------------- -## -## Copyright (c) 2005 - 2012 Nebularis. -## -## Permission is hereby granted, free of charge, to any person obtaining a copy -## of this software and associated documentation files (the "Software"), deal -## in the Software without restriction, including without limitation the rights -## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -## copies of the Software, and to permit persons to whom the Software is -## furnished to do so, subject to the following conditions: -## -## The above copyright notice and this permission notice shall be included in -## all copies or substantial portions of the Software. -## -## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -## IN THE SOFTWARE. -## ---------------------------------------------------------------------------- - CONF=./dist/setup-config CABAL=distributed-process-platform.cabal BUILD_DEPENDS=$(CONF) $(CABAL) +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + .PHONY: all all: build @@ -39,9 +19,16 @@ build: configure .PHONY: configure configure: $(BUILD_DEPENDS) +.PHONY: ci +ci: $(REPOS) test + $(BUILD_DEPENDS): cabal configure --enable-tests +$(REPOS): + git clone $(BASE_GIT)/$@.git + cabal install ./$@ --force-reinstalls + .PHONY: clean clean: cabal clean diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..9f9c5707 --- /dev/null +++ b/REPOS @@ -0,0 +1,4 @@ +rank1dynamic +distributed-static +network-transport +network-transport-tcp From f1cda9e24b3efc03af12b3081a61908e223b5ab0 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:20:35 +0000 Subject: [PATCH 0964/2357] add travis config --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.travis.yml b/.travis.yml index 999bd37b..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,14 @@ language: haskell +script: "make ci" +notifications: + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true +branches: + only: + - master + - development From bd60410675e88a3df72dee13e6a75369ab1ae7aa Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:20:35 +0000 Subject: [PATCH 0965/2357] add travis config --- .travis.yml | 13 +++++++++++++ Makefile | 33 ++++++++++----------------------- REPOS | 4 ++++ 3 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 REPOS diff --git a/.travis.yml b/.travis.yml index 999bd37b..af7ce1be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,14 @@ language: haskell +script: "make ci" +notifications: + email: + recipients: + - cloud.haskell@gmail.com + irc: + channels: + - "irc.freenode.org#haskell-distributed" + use_notice: true +branches: + only: + - master + - development diff --git a/Makefile b/Makefile index e30299a5..9756f8b0 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,10 @@ -## ---------------------------------------------------------------------------- -## -## Copyright (c) 2005 - 2012 Nebularis. -## -## Permission is hereby granted, free of charge, to any person obtaining a copy -## of this software and associated documentation files (the "Software"), deal -## in the Software without restriction, including without limitation the rights -## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -## copies of the Software, and to permit persons to whom the Software is -## furnished to do so, subject to the following conditions: -## -## The above copyright notice and this permission notice shall be included in -## all copies or substantial portions of the Software. -## -## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -## IN THE SOFTWARE. -## ---------------------------------------------------------------------------- - CONF=./dist/setup-config CABAL=distributed-process-platform.cabal BUILD_DEPENDS=$(CONF) $(CABAL) +BASE_GIT := git://github.com/haskell-distributed +REPOS=$(shell cat REPOS | sed '/^$$/d') + .PHONY: all all: build @@ -39,9 +19,16 @@ build: configure .PHONY: configure configure: $(BUILD_DEPENDS) +.PHONY: ci +ci: $(REPOS) test + $(BUILD_DEPENDS): cabal configure --enable-tests +$(REPOS): + git clone $(BASE_GIT)/$@.git + cabal install ./$@ --force-reinstalls + .PHONY: clean clean: cabal clean diff --git a/REPOS b/REPOS new file mode 100644 index 00000000..9f9c5707 --- /dev/null +++ b/REPOS @@ -0,0 +1,4 @@ +rank1dynamic +distributed-static +network-transport +network-transport-tcp From 0a214d2ffd95396c401f9db41e9944c0e6454e48 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:30:33 +0000 Subject: [PATCH 0966/2357] Include distributed-process in the list of CI dependencies [ci skip] --- README.md | 21 ++++++++++++++++++++- REPOS | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b259147..8adeea69 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Cloud Haskell Platform +### distributed-process-platform [![travis](https://secure.travis-ci.org/haskell-distributed/distributed-process-platform.png?branch=master,development)](http://travis-ci.org/haskell-distributed/distributed-process-platform) + + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. + +### License + +distributed-process is made available under a BSD-3 license. diff --git a/REPOS b/REPOS index 9f9c5707..f12e96fe 100644 --- a/REPOS +++ b/REPOS @@ -2,3 +2,4 @@ rank1dynamic distributed-static network-transport network-transport-tcp +distributed-process From 058214901bcbc9d9730007d7b29e1d901cbbc387 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:30:33 +0000 Subject: [PATCH 0967/2357] Include distributed-process in the list of CI dependencies [ci skip] --- README.md | 21 ++++++++++++++++++++- REPOS | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b259147..8adeea69 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Cloud Haskell Platform +### distributed-process-platform [![travis](https://secure.travis-ci.org/haskell-distributed/distributed-process-platform.png?branch=master,development)](http://travis-ci.org/haskell-distributed/distributed-process-platform) + + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. + +### License + +distributed-process is made available under a BSD-3 license. diff --git a/REPOS b/REPOS index 9f9c5707..f12e96fe 100644 --- a/REPOS +++ b/REPOS @@ -2,3 +2,4 @@ rank1dynamic distributed-static network-transport network-transport-tcp +distributed-process From 54db0c7ed8704b5e983c81e1f5c7d5f82131ec3d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:30:33 +0000 Subject: [PATCH 0968/2357] Include distributed-process in the list of CI dependencies [ci skip] --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b259147..8adeea69 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Cloud Haskell Platform +### distributed-process-platform [![travis](https://secure.travis-ci.org/haskell-distributed/distributed-process-platform.png?branch=master,development)](http://travis-ci.org/haskell-distributed/distributed-process-platform) + + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. + +### License + +distributed-process is made available under a BSD-3 license. From 2c7de40baae3cc12be33222c72eec089b3fc77b7 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:30:33 +0000 Subject: [PATCH 0969/2357] Include distributed-process in the list of CI dependencies [ci skip] --- README.md | 21 ++++++++++++++++++++- REPOS | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b259147..8adeea69 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Cloud Haskell Platform +### distributed-process-platform [![travis](https://secure.travis-ci.org/haskell-distributed/distributed-process-platform.png?branch=master,development)](http://travis-ci.org/haskell-distributed/distributed-process-platform) + + +This repository is part of Cloud Haskell. + +See http://haskell-distributed.github.com for documentation, user guides, +tutorials and assistance. + +### Getting Help / Raising Issues + +Please visit our [bug tracker](http://cloud-haskell.atlassian.net) to submit +issues. Anyone can browse, although you'll need to provide an email address +and create an account in order to submit new issues. + +If you'd like to talk to a human, please contact us at the parallel-haskell +mailing list in the first instance - parallel-haskell@googlegroups.com. + +### License + +distributed-process is made available under a BSD-3 license. diff --git a/REPOS b/REPOS index 9f9c5707..f12e96fe 100644 --- a/REPOS +++ b/REPOS @@ -2,3 +2,4 @@ rank1dynamic distributed-static network-transport network-transport-tcp +distributed-process From 51bee441408bde6000e203ae681e448d5e3c0d74 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:35:08 +0000 Subject: [PATCH 0970/2357] allow all branches of platform to build on ci --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index af7ce1be..439e6e0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,3 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true -branches: - only: - - master - - development From fd1737c6e857e641b51afedbe99e016eb6f35ba6 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:35:08 +0000 Subject: [PATCH 0971/2357] allow all branches of platform to build on ci --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index af7ce1be..439e6e0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,3 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true -branches: - only: - - master - - development From 0acd1a4e2dd308e468c891d6deaffcb6316a4d9c Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:35:08 +0000 Subject: [PATCH 0972/2357] allow all branches of platform to build on ci --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index af7ce1be..439e6e0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,3 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true -branches: - only: - - master - - development From 512451d0b69e04457d65fc036856f05747037a70 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:35:08 +0000 Subject: [PATCH 0973/2357] allow all branches of platform to build on ci --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index af7ce1be..439e6e0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,3 @@ notifications: channels: - "irc.freenode.org#haskell-distributed" use_notice: true -branches: - only: - - master - - development From 8e19918aec23fa087ab6db439f71cb3892c38ac5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:40:48 +0000 Subject: [PATCH 0974/2357] build with depends on ci --- .travis.yml | 1 + Makefile | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..34164cc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: haskell script: "make ci" +install: "make deps" notifications: email: recipients: diff --git a/Makefile b/Makefile index 9756f8b0..0948beec 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ ci: $(REPOS) test $(BUILD_DEPENDS): cabal configure --enable-tests +.PHONY: deps +deps: $(REPOS) + cabal install --enable-tests + $(REPOS): git clone $(BASE_GIT)/$@.git cabal install ./$@ --force-reinstalls From a86f81157fab0b8deeec8467a3ab30a5fb8832b9 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:40:48 +0000 Subject: [PATCH 0975/2357] build with depends on ci --- .travis.yml | 1 + Makefile | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..34164cc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: haskell script: "make ci" +install: "make deps" notifications: email: recipients: diff --git a/Makefile b/Makefile index 9756f8b0..0948beec 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ ci: $(REPOS) test $(BUILD_DEPENDS): cabal configure --enable-tests +.PHONY: deps +deps: $(REPOS) + cabal install --enable-tests + $(REPOS): git clone $(BASE_GIT)/$@.git cabal install ./$@ --force-reinstalls From 4ea0256b8d494962f3062698942abc3f60f4b8b4 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:40:48 +0000 Subject: [PATCH 0976/2357] build with depends on ci --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..34164cc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: haskell script: "make ci" +install: "make deps" notifications: email: recipients: From 1f43f2528ce59bf06f10bb5a0329971c879c82f5 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Mon, 4 Feb 2013 23:40:48 +0000 Subject: [PATCH 0977/2357] build with depends on ci --- .travis.yml | 1 + Makefile | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 439e6e0c..34164cc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: haskell script: "make ci" +install: "make deps" notifications: email: recipients: diff --git a/Makefile b/Makefile index 9756f8b0..0948beec 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ ci: $(REPOS) test $(BUILD_DEPENDS): cabal configure --enable-tests +.PHONY: deps +deps: $(REPOS) + cabal install --enable-tests + $(REPOS): git clone $(BASE_GIT)/$@.git cabal install ./$@ --force-reinstalls From e91304b5cdaa58a749238d62d42d74d99bbc2c8d Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 5 Feb 2013 10:34:19 +0000 Subject: [PATCH 0978/2357] fix broken link --- documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation.md b/documentation.md index c7138137..0d39db7e 100644 --- a/documentation.md +++ b/documentation.md @@ -549,4 +549,4 @@ TBC [19]: http://hackage.haskell.org/package/async [20]: /wiki/networktransport.html [21]: /static/doc/distributed-process-platform/Control-Distributed-Process-Platform-ManagedProcess.html -[22]: /tutorials/3.managedprocess.html +[22]: /tutorials/tutorial3.html From fcea64d8f0a8ae80b17a02da78d223e03d29ab90 Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 5 Feb 2013 13:14:21 +0000 Subject: [PATCH 0979/2357] fix footer bug tracking link --- _includes/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_includes/footer.html b/_includes/footer.html index 072d0822..e1630301 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -22,7 +22,7 @@

    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 literal 0 HcmV?d00001 diff --git a/img/back3.jpg b/img/back3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a7a3e61b105a77f3cbb5c71f4de5e2fe6c3d6364 GIT binary patch literal 219614 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 uHS(5gt5zF;@5pKa4oHoUFmZv%h<%9 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