on master help sign in
10 files changed+684−310
since splitsnapshots
unifiedsplit
Changelog.markdown+4 README.markdown+69−3 bench/Basic.hs+25−21 cabal.project+1−1 curryer.cabal → curryer-rpc.cabal+21−7 examples/SimpleKeyValueClient.hs+39 examples/SimpleKeyValueServer.hs+30 src/Network/RPC/Curryer/Client.hs+98−66 src/Network/RPC/Curryer/Server.hs+249−134 test/Curryer/Test/Basic.hs+148−78
Changelog.markdownnew file+4
@@ -0,0 +1,4 @@
1+# v0.1 (2020-12-27)
2+
3+* initial release to support [Project:M36](https://github.com/agentm/project-m36)
4+
README.markdown+69−3
@@ -1,12 +1,78 @@
11# Curryer - Fast Haskell-to-Haskell RPC
22
3−Curryer (pun intended) is a fast, Haskell-exclusive RPC (remote procedure call) library. By using the latest Haskell serialization and streaming libraries, curryer aims to the fastest and easiest means of communicating between Haskell-based processes.
3+Curryer (pun intended) is a fast, Haskell-exclusive RPC (remote procedure call) library. By using the latest Haskell serialization and streaming libraries, curryer aims to be the fastest and easiest means of communicating between Haskell-based processes.
44
5−Curryer is inspired by the now unmaintained [distributed-process](https://hackage.haskell.org/package/distributed-process) library.
5+Curryer is inspired by the now unmaintained [distributed-process](https://hackage.haskell.org/package/distributed-process) library, but is lighter-weight and uses a higher-performance serialization package.
66
77## Features
88
99* blocking and non-blocking remote function calls
1010* asynchronous server-to-client callbacks (for server-initiated notifications)
1111* timeouts
12−* utilizes versionable shared ADT instead of shared function `StaticPtr`s
12+* leverages [winery](https://hackage.haskell.org/package/winery) for high-performance serialization
13+
14+## Requirements
15+
16+* GHC 8.6+
17+
18+## Code Example
19+
20+[Server](https://github.com/agentm/curryer/examples/SimpleKeyValueServer.hs):
21+
22+```haskell
23+data SetKey = SetKey String String
24+ deriving (Generic, Show)
25+ deriving Serialise via WineryVariant SetKey
26+
27+data GetKey = GetKey String
28+ deriving (Generic, Show)
29+ deriving Serialise via WineryVariant GetKey
30+
31+main :: IO ()
32+main = do
33+ kvmap <- M.newIO
34+ void $ serve kvRequestHandlers kvmap localHostAddr 8765 Nothing
35+
36+kvRequestHandlers :: RequestHandlers (M.Map String String)
37+kvRequestHandlers = [ RequestHandler $ \state (SetKey k v) ->
38+ atomically $ M.insert v k (connectionServerState state)
39+ , RequestHandler $ \state (GetKey k) ->
40+ atomically $ M.lookup k (connectionServerState state)
41+ ]
42+```
43+
44+[Client](https://github.com/agentm/curryer/examples/SimpleKeyValueClient.hs):
45+
46+```haskell
47+data SetKey = SetKey String String
48+ deriving (Generic, Show)
49+ deriving Serialise via WineryVariant SetKey
50+
51+data GetKey = GetKey String
52+ deriving (Generic, Show)
53+ deriving Serialise via WineryVariant GetKey
54+
55+data CommandOptions = Get {name :: String}
56+ | Set {name :: String, value :: String}
57+ deriving (Generic, Show)
58+
59+instance ParseRecord CommandOptions
60+
61+main :: IO ()
62+main = do
63+ opts <- getRecord "SimpleKeyValueClient"
64+ conn <- connect [] localHostAddr 8765
65+ case opts of
66+ Get k -> do
67+ eRet <- call conn (GetKey k)
68+ case eRet of
69+ Left err -> error (show err)
70+ Right (Just val) -> putStrLn val
71+ Right Nothing -> error "no such key"
72+ Set k v -> do
73+ eRet <- call conn (SetKey k v)
74+ case eRet of
75+ Left err -> error (show err)
76+ Right () -> pure ()
77+
78+```
bench/Basic.hs+25−21
@@ -15,10 +15,10 @@ main :: IO ()
1515main = do
1616 --start server shared across benchmarks- gather stats on round-trip requests-responses
1717 portReadyMVar <- newEmptyMVar
18− server <- async (serve (pure Nothing) benchmarkServerMessageHandler localHostAddr 0 (Just portReadyMVar))
18+ server <- async (serve benchmarkServerRequestHandlers () localHostAddr 0 (Just portReadyMVar))
1919 --wait for server to be ready
2020 (SockAddrInet port _) <- takeMVar portReadyMVar
21− clientConn <- connect (const (pure ())) localHostAddr port
21+ clientConn <- connect [] localHostAddr port
2222
2323 _ <- syncWaitRequest clientConn 100
2424 let syncWait ms = nfIO (syncWaitRequest clientConn ms)
@@ -30,10 +30,10 @@ main = do
3030 defaultMain [bgroup "wait sync" [bench "0 ms" (syncWait 0),
3131 bench "10 ms" (syncWait 10),
3232 bench "100 ms" (syncWait 100)],
33− bgroup "bytestring roundtrip" [bench "0 bytes" (syncByteString bs0),
34− bench "100 bytes" (syncByteString bs100),
35− bench "1000 bytes" (syncByteString bs1000),
36− bench "1000000 bytes" (syncByteString bs1M)
33+ bgroup "bytestring roundtrip" [bench "0 bytes" (syncByteString bs0)
34+ ,bench "100 bytes" (syncByteString bs100)
35+ ,bench "1000 bytes" (syncByteString bs1000)
36+ ,bench "1000000 bytes" (syncByteString bs1M)
3737 ]]
3838
3939 --TODO: bench for > PIPE_BUF bytes
@@ -41,34 +41,38 @@ main = do
4141 close clientConn
4242 cancel server
4343
44−data BenchRequest = WaitMillisecondsReq Int --ask the server to respond in X milliseconds
45− | WaitByteStringReq BS.ByteString --ask the server to round trip arbitrary bytes
44+data WaitMillisecondsReq = WaitMillisecondsReq Int --ask the server to respond in X milliseconds
4645 deriving (Generic, Show)
47− deriving Serialise via WineryVariant BenchRequest
46+ deriving Serialise via WineryVariant WaitMillisecondsReq
4847
49−data BenchResponse = WaitMillisecondsResp
50− | WaitByteStringResp BS.ByteString
48+data WaitByteStringReq = WaitByteStringReq BS.ByteString
49+ deriving (Generic, Show)
50+ deriving Serialise via WineryVariant WaitByteStringReq
51+
52+data WaitMillisecondsResp = WaitMillisecondsResp
5153 deriving (Generic, Show, Eq)
52− deriving Serialise via WineryVariant BenchResponse
54+ deriving Serialise via WineryVariant WaitMillisecondsResp
5355
54−benchmarkServerMessageHandler :: BenchRequest -> IO (HandlerResponse BenchResponse)
55−benchmarkServerMessageHandler (WaitMillisecondsReq ms) = do
56− threadDelay (1000 * ms)
57− pure (HandlerResponse WaitMillisecondsResp)
58−benchmarkServerMessageHandler (WaitByteStringReq bs) = pure (HandlerResponse (WaitByteStringResp bs))
56+benchmarkServerRequestHandlers :: RequestHandlers a
57+benchmarkServerRequestHandlers =
58+ [RequestHandler $ \_ (WaitMillisecondsReq ms) -> do
59+ threadDelay (1000 * ms)
60+ pure WaitMillisecondsResp,
61+ RequestHandler $ \_ (WaitByteStringReq bs) ->
62+ pure bs
63+ ]
5964
6065--perform 10000 small synchronous requests
61−syncWaitRequest :: Connection BenchResponse -> Int -> IO Int
66+syncWaitRequest :: Connection -> Int -> IO Int
6267syncWaitRequest conn ms = do
6368 ret <- call conn (WaitMillisecondsReq ms)
6469 when (ret /= Right WaitMillisecondsResp) (error "ret failed")
6570 pure ms
6671
67−syncByteStringRequest :: Connection BenchResponse -> BS.ByteString -> IO BS.ByteString
72+syncByteStringRequest :: Connection -> BS.ByteString -> IO BS.ByteString
6873syncByteStringRequest conn bs = do
6974 ret <- call conn (WaitByteStringReq bs)
7075 case ret of
71− Right (WaitByteStringResp bs') -> pure bs'
72− Right _ -> error "invalid resp"
76+ Right bs' -> pure bs'
7377 Left err -> error (show err)
7478
cabal.project+1−1
@@ -3,4 +3,4 @@ packages: .
33source-repository-package
44 type: git
55 location: https://github.com/composewell/streamly.git
6− tag: e218fb9d8f4263fd69d32a2ff318bac0e179c642
6+ tag: 4e48b52e8709390a47e99c107ebed2d1c9420076
curryer.cabal → curryer-rpc.cabalrenamed+21−7
@@ -1,4 +1,4 @@
1−Name: curryer
1+Name: curryer-rpc
22Version: 0.1
33License: PublicDomain
44Build-Type: Simple
@@ -11,14 +11,14 @@ Maintainer: agentm@themactionfaction.com
1111Cabal-Version: >= 1.10
1212Synopsis: Fast, Haskell RPC
1313Description: Haskell-to-Haskell RPC using Winery serialization.
14−Extra-Source-Files: Changelog.markdown README.markdown
14+Extra-Source-Files: Changelog.markdown README.markdown cabal.project
1515
1616Source-Repository head
1717 Type: git
1818 location: https://github.com/agentm/curryer
1919
2020Library
21− Build-Depends: base >= 4.8
21+ Build-Depends: base >= 4.12 && < 4.15
2222 , winery
2323 , bytestring
2424 , streamly >= 0.7.2
@@ -33,6 +33,7 @@ Library
3333 , hashable
3434 , time
3535 , network-byte-order
36+ , stm
3637 Hs-Source-Dirs: ./src
3738 Default-Language: Haskell2010
3839 ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports
@@ -50,20 +51,33 @@ Test-Suite test
5051 build-depends: tasty
5152 , tasty-hunit
5253 , base
53− , curryer
54+ , curryer-rpc
5455 , winery
5556 , network
5657 , async
58+ , stm
5759 other-modules: Curryer.Test.Basic
5860
5961Benchmark perf
6062 Default-Language: Haskell2010
6163 Default-Extensions: OverloadedStrings
62− Build-Depends: base, criterion, curryer, network, winery, async, bytestring
64+ Build-Depends: base, criterion, curryer-rpc, network, winery, async, bytestring
6365
6466 Main-Is: Basic.hs
6567 Type: exitcode-stdio-1.0
66− GHC-Options: -Wall -threaded -rtsopts -fprof-auto
68+ GHC-Options: -Wall -threaded -rtsopts
69+ -- -fprof-auto
6770 HS-Source-Dirs: ./bench
6871
69−
72+Executable SimpleKeyValueServer
73+ Build-Depends: stm-containers, stm, base, curryer-rpc, winery
74+ Main-Is: examples/SimpleKeyValueServer.hs
75+ GHC-Options: -Wall -threaded
76+ Default-Language: Haskell2010
77+
78+Executable SimpleKeyValueClient
79+ Build-Depends: stm, base, curryer-rpc, winery, optparse-generic
80+ Main-Is: examples/SimpleKeyValueClient.hs
81+ GHC-Options: -Wall -threaded
82+ Default-Language: Haskell2010
83+
examples/SimpleKeyValueClient.hsnew file+39
@@ -0,0 +1,39 @@
1+{-# LANGUAGE DerivingVia, DeriveGeneric, OverloadedStrings #-}
2+import GHC.Generics
3+import Network.RPC.Curryer.Client
4+import Network.RPC.Curryer.Server (localHostAddr)
5+import Options.Generic
6+import Codec.Winery
7+
8+data SetKey = SetKey String String
9+ deriving (Generic, Show)
10+ deriving Serialise via WineryVariant SetKey
11+
12+data GetKey = GetKey String
13+ deriving (Generic, Show)
14+ deriving Serialise via WineryVariant GetKey
15+
16+data CommandOptions = Get {name :: String}
17+ | Set {name :: String, value :: String}
18+ deriving (Generic, Show)
19+
20+instance ParseRecord CommandOptions
21+
22+main :: IO ()
23+main = do
24+ opts <- getRecord "SimpleKeyValueClient"
25+ -- connect to the remote server (in this case on the localhost address)
26+ conn <- connect [] localHostAddr 8765
27+ case opts of
28+ Get k -> do
29+ --call the remote function and validate the result
30+ eRet <- call conn (GetKey k)
31+ case eRet of
32+ Left err -> error (show err)
33+ Right (Just val) -> putStrLn val
34+ Right Nothing -> error "no such key"
35+ Set k v -> do
36+ eRet <- call conn (SetKey k v)
37+ case eRet of
38+ Left err -> error (show err)
39+ Right () -> pure ()
examples/SimpleKeyValueServer.hsnew file+30
@@ -0,0 +1,30 @@
1+{-# LANGUAGE DerivingVia, DeriveGeneric #-}
2+import Network.RPC.Curryer.Server
3+import Control.Concurrent.STM
4+import StmContainers.Map as M
5+import GHC.Generics
6+import Codec.Winery
7+import Data.Functor
8+
9+-- create data structures to represent remote function calls
10+data SetKey = SetKey String String
11+ deriving (Generic, Show)
12+ deriving Serialise via WineryVariant SetKey
13+
14+data GetKey = GetKey String
15+ deriving (Generic, Show)
16+ deriving Serialise via WineryVariant GetKey
17+
18+--call the `serve` function to handle incoming requests
19+main :: IO ()
20+main = do
21+ kvmap <- M.newIO
22+ void $ serve kvRequestHandlers kvmap localHostAddr 8765 Nothing
23+
24+-- setup incoming request handlers to operate on the server's state
25+kvRequestHandlers :: RequestHandlers (M.Map String String)
26+kvRequestHandlers = [ RequestHandler $ \state (SetKey k v) ->
27+ atomically $ M.insert v k (connectionServerState state)
28+ , RequestHandler $ \state (GetKey k) ->
29+ atomically $ M.lookup k (connectionServerState state)
30+ ]
src/Network/RPC/Curryer/Client.hs+98−66
@@ -1,4 +1,4 @@
1−{-# LANGUAGE TypeApplications, RankNTypes, ScopedTypeVariables #-}
1+{-# LANGUAGE RankNTypes, ScopedTypeVariables, GADTs #-}
22module Network.RPC.Curryer.Client where
33import Network.RPC.Curryer.Server
44import Network.Socket as Socket
@@ -11,90 +11,114 @@ import Control.Concurrent.MVar
1111import GHC.Conc
1212import Data.Time.Clock
1313import System.Timeout
14−import Data.Proxy
14+import Control.Monad
1515
16−type SyncMap a = STMMap.Map UUID (MVar (Either ConnectionError a), UTCTime)
17−-- the request map holds
18−data Connection a = Connection { _conn_sock :: Socket,
19− _conn_asyncThread :: Async (),
20− _conn_syncmap :: SyncMap a
21− }
16+type SyncMap = STMMap.Map UUID (MVar (Either ConnectionError BinaryMessage), UTCTime)
2217
23−connect :: forall clientmsg. (Serialise clientmsg) =>
24− AsyncMessageHandler clientmsg ->
25− HostAddr ->
26− PortNumber ->
27− IO (Connection clientmsg)
28−connect notificationCallback hostAddr portNum = do
18+-- | Represents a remote connection to server.
19+data Connection = Connection { _conn_sockLock :: Locking Socket,
20+ _conn_asyncThread :: Async (),
21+ _conn_syncmap :: SyncMap
22+ }
23+
24+-- | Function handlers run on the client, triggered by the server- useful for asynchronous callbacks.
25+data ClientAsyncRequestHandler where
26+ ClientAsyncRequestHandler :: forall a. Serialise a => (a -> IO ()) -> ClientAsyncRequestHandler
27+
28+type ClientAsyncRequestHandlers = [ClientAsyncRequestHandler]
29+
30+-- | Connects to a remote server with specific async callbacks registered.
31+connect ::
32+ ClientAsyncRequestHandlers ->
33+ HostAddr ->
34+ PortNumber ->
35+ IO Connection
36+connect asyncHandlers hostAddr portNum = do
2937 sock <- TCP.connect hostAddr portNum
3038 syncmap <- STMMap.newIO
31− let decoder :: Decoder (Message clientmsg)
32− decoder = case getDecoder (schema (Proxy @(Message clientmsg))) of
33− Left err -> error (show err)
34− Right dec -> dec
35− asyncThread <- async (clientAsync sock syncmap decoder notificationCallback)
39+ asyncThread <- async (clientAsync sock syncmap asyncHandlers)
40+ sockLock <- newLock sock
3641 pure (Connection {
37− _conn_sock = sock,
42+ _conn_sockLock = sockLock,
3843 _conn_asyncThread = asyncThread,
3944 _conn_syncmap = syncmap
4045 })
4146
42−close :: Connection a -> IO ()
47+-- | Close the connection and release all connection resources.
48+close :: Connection -> IO ()
4349close conn = do
44− Socket.close (_conn_sock conn)
50+ withLock (_conn_sockLock conn) $ \sock ->
51+ Socket.close sock
4552 cancel (_conn_asyncThread conn)
4653
47−-- async thread for handling client-side incoming messages- dispatch to proper waiting thread or handler asynchronous notifications
48−clientAsync :: forall b. Serialise b =>
54+-- | async thread for handling client-side incoming messages- dispatch to proper waiting thread or asynchronous notifications handler
55+clientAsync ::
4956 Socket ->
50− SyncMap b ->
51− Decoder (Message b) ->
52− AsyncMessageHandler b ->
57+ SyncMap ->
58+ ClientAsyncRequestHandlers ->
5359 IO ()
54−clientAsync sock syncmap decoder asyncHandler = do
55− -- ping proper thread to continue
56− let findRequest requestId =
57− atomically $ do
58− val' <- STMMap.lookup requestId syncmap
59− STMMap.delete requestId syncmap
60− pure val'
61−
62− let responseHandler responseMsg = do
63− --putStrLn "client-side message handler"
64− case responseMsg of
65− Response requestId val -> do
66− varval <- findRequest requestId
67− case varval of
68− Nothing -> error "dumped unrequested response"
69− Just (mVar,_) -> putMVar mVar (Right val)
70− AsyncRequest _ asyncMsg ->
71− asyncHandler asyncMsg
72− ResponseExpectedRequest{} -> error "dumped response expected request"
73− ExceptionResponse requestId excString -> do
74− varval <- findRequest requestId
75− case varval of
76− Nothing -> error "dumped unrequested exception response"
77− Just (mVar,_) -> putMVar mVar (Left (ExceptionError excString))
78− TimedOutResponse requestId -> do
79− varval <- findRequest requestId
80− case varval of
81− Nothing -> error "dumped unrequested timeout response"
82− Just (mVar,_) -> putMVar mVar (Left TimeoutError)
83− drainSocketMessages sock decoder responseHandler
60+clientAsync sock syncmap asyncHandlers = do
61+ lsock <- newLock sock
62+ drainSocketMessages sock (clientEnvelopeHandler asyncHandlers lsock syncmap)
8463
85−call :: (Serialise request, Serialise response) => Connection response -> request -> IO (Either ConnectionError response)
64+consumeResponse :: UUID -> STMMap.Map UUID (MVar a, b) -> a -> IO ()
65+consumeResponse msgId syncMap val = do
66+ match <- atomically $ do
67+ val' <- STMMap.lookup msgId syncMap
68+ STMMap.delete msgId syncMap
69+ pure val'
70+ case match of
71+ Nothing -> pure () -- drop message
72+ Just (mVar, _) -> putMVar mVar val
73+
74+-- | handles envelope responses from server- timeout from ths server is ignored, but perhaps that's proper for trusted servers- the server expects the client to process all async requests
75+clientEnvelopeHandler ::
76+ ClientAsyncRequestHandlers
77+ -> Locking Socket
78+ -> SyncMap
79+ -> Envelope
80+ -> IO ()
81+clientEnvelopeHandler handlers _ _ envelope@(Envelope _ (RequestMessage _) _ _) = do
82+ --should this run off on another green thread?
83+ let firstMatcher Nothing (ClientAsyncRequestHandler (dispatchf :: a -> IO ())) = do
84+ case openEnvelope envelope of
85+ Nothing -> pure Nothing
86+ Just decoded -> do
87+ dispatchf decoded
88+ pure (Just ())
89+ firstMatcher acc _ = pure acc
90+ foldM_ firstMatcher Nothing handlers
91+clientEnvelopeHandler _ _ syncMap (Envelope _ ResponseMessage msgId binaryMessage) =
92+ consumeResponse msgId syncMap (Right binaryMessage)
93+clientEnvelopeHandler _ _ syncMap (Envelope _ TimeoutResponseMessage msgId _) =
94+ consumeResponse msgId syncMap (Left TimeoutError)
95+clientEnvelopeHandler _ _ syncMap (Envelope _ ExceptionResponseMessage msgId excPayload) =
96+ case deserialise excPayload of
97+ Left err -> error ("failed to deserialise exception string" <> show err)
98+ Right excStr ->
99+ consumeResponse msgId syncMap (Left (ExceptionError excStr))
100+
101+-- | Basic remote function call via data type and return value.
102+call :: (Serialise request, Serialise response) => Connection -> request -> IO (Either ConnectionError response)
86103call = callTimeout Nothing
87104
88−-- | Send a request to the remote server and returns a response.
89−callTimeout :: (Serialise request, Serialise response) => Maybe Int -> Connection response -> request -> IO (Either ConnectionError response)
105+-- | Send a request to the remote server and returns a response but with the possibility of a timeout after n microseconds.
106+callTimeout :: (Serialise request, Serialise response) => Maybe Int -> Connection -> request -> IO (Either ConnectionError response)
90107callTimeout mTimeout conn msg = do
108+ requestID <- UUID <$> UUIDBase.nextRandom
91109 let mVarMap = _conn_syncmap conn
92− requestID <- UUID <$> UUIDBase.nextRandom
110+ timeoutms = case mTimeout of
111+ Nothing -> 0
112+ Just tm | tm < 0 -> 0
113+ Just tm -> fromIntegral tm
114+
115+ envelope = Envelope fprint (RequestMessage timeoutms) requestID (serialise msg)
116+ fprint = fingerprint msg
93117 -- setup mvar to wait for response
94118 responseMVar <- newEmptyMVar
95119 now <- getCurrentTime
96120 atomically $ STMMap.insert (responseMVar, now) requestID mVarMap
97− sendMessage (ResponseExpectedRequest requestID mTimeout msg) (_conn_sock conn)
121+ sendEnvelope envelope (_conn_sockLock conn)
98122 let timeoutMicroseconds =
99123 case mTimeout of
100124 Just timeout' -> timeout' + 100 --add 100 ms to account for unknown network latency
@@ -105,11 +129,19 @@ callTimeout mTimeout conn msg = do
105129 --timeout
106130 Nothing ->
107131 pure (Left TimeoutError)
108− Just res -> pure res
132+ Just (Left exc) ->
133+ pure (Left exc)
134+ Just (Right binmsg) ->
135+ --TODO use decoder instead
136+ case deserialise binmsg of
137+ Left err -> error ("deserialise client error " <> show err)
138+ Right m -> pure (Right m)
109139
110−asyncCall :: (Serialise request, Serialise response) => Connection response -> request -> IO (Either ConnectionError ())
140+-- | Call a remote function but do not expect a response from the server.
141+asyncCall :: Serialise request => Connection -> request -> IO (Either ConnectionError ())
111142asyncCall conn msg = do
112143 requestID <- UUID <$> UUIDBase.nextRandom
113− sendMessage (AsyncRequest requestID msg) (_conn_sock conn)
144+ let envelope = Envelope fprint (RequestMessage 0) requestID (serialise msg)
145+ fprint = fingerprint msg
146+ sendEnvelope envelope (_conn_sockLock conn)
114147 pure (Right ())
115−
src/Network/RPC/Curryer/Server.hs+249−134
@@ -1,18 +1,20 @@
1−{-# LANGUAGE DerivingVia, DeriveGeneric, RankNTypes, ScopedTypeVariables, MultiParamTypeClasses, OverloadedStrings, GeneralizedNewtypeDeriving, TypeApplications, CPP #-}
1+{-# LANGUAGE DerivingVia, DeriveGeneric, RankNTypes, ScopedTypeVariables, MultiParamTypeClasses, OverloadedStrings, GeneralizedNewtypeDeriving, CPP, ExistentialQuantification, StandaloneDeriving, GADTs, RecordWildCards #-}
2+{-# OPTIONS_GHC -fno-warn-orphans #-}
23{- HLINT ignore "Use lambda-case" -}
34module Network.RPC.Curryer.Server where
4−import Streamly
55import qualified Streamly.Prelude as S
66import Streamly.Network.Socket
77import Streamly.Internal.Network.Socket (handleWithM)
88import Network.Socket as Socket
99import Network.Socket.ByteString as Socket
10−import Streamly.Internal.Data.Parser as P
10+import Streamly.Internal.Data.Parser as P hiding (concatMap)
1111import Codec.Winery
1212import Codec.Winery.Internal (varInt, decodeVarInt, getBytes)
1313import Codec.Winery.Class (mkExtractor)
1414import GHC.Generics
15−import Control.Concurrent.MVar (MVar)
15+import GHC.Fingerprint
16+import Data.Typeable
17+import Control.Concurrent.MVar (MVar, newMVar, withMVar)
1618import Control.Exception
1719import Data.Function ((&))
1820import Data.Word
@@ -24,21 +26,23 @@ import qualified Streamly.Internal.Data.Stream.IsStream as S
2426import qualified Data.Binary as B
2527import qualified Data.UUID as UUIDBase
2628import qualified Data.UUID.V4 as UUIDBase
29+import Control.Monad
30+import Data.Functor
2731
2832import qualified Network.RPC.Curryer.StreamlyAdditions as SA
29−import Control.Monad
33+--import Control.Monad
3034import Data.Hashable
3135import System.Timeout
3236import qualified Network.ByteOrder as BO
33−import Data.Proxy
37+
3438
3539-- for toArrayS conversion
36−{-import qualified Data.ByteString.Internal as BSI
37−import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as SA
38−import Foreign.ForeignPtr (plusForeignPtr)
40+import qualified Data.ByteString.Internal as BSI
41+import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as Arr
3942import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
40−import GHC.Ptr (plusPtr)
41−-}
43+import GHC.Ptr (minusPtr)
44+
45+-- define CURRYER_SHOW_BYTES 1
4246#if CURRYER_SHOW_BYTES == 1
4347import Debug.Trace
4448#endif
@@ -50,23 +54,72 @@ traceBytes msg bs = traceShowM (msg, BS.length bs, bs)
5054traceBytes _ _ = pure ()
5155#endif
5256
53−type Timeout = Int
57+data Locking a = Locking (MVar ()) a
58+
59+newLock :: a -> IO (Locking a)
60+newLock x = do
61+ lock <- newMVar ()
62+ pure (Locking lock x)
63+
64+withLock :: Locking a -> (a -> IO b) -> IO b
65+withLock (Locking mvar v) m =
66+ withMVar mvar $ \_ -> m v
67+
68+lockless :: Locking a -> a
69+lockless (Locking _ a) = a
70+
71+type Timeout = Word32
72+
73+type BinaryMessage = BS.ByteString
5474
55−data Message a = Response UUID a
56− | AsyncRequest UUID a
57− | ResponseExpectedRequest UUID (Maybe Timeout) a
58− | TimedOutResponse UUID
59− | ExceptionResponse UUID String
60− deriving (Generic, Show)
61− deriving Serialise via WineryVariant (Message a)
75+--includes the fingerprint of the incoming data type (held in the BinaryMessage) to determine how to dispatch the message.
76+--add another envelope type for unencoded binary messages for any easy optimization for in-process communication
77+data Envelope = Envelope {
78+ envFingerprint :: !Fingerprint,
79+ envMessageType :: !MessageType,
80+ envMsgId :: !UUID,
81+ envPayload :: !BinaryMessage
82+ }
83+ deriving (Generic, Show)
6284
63−-- | The response type
64−data HandlerResponse a = NoResponse
65− | HandlerResponse a
66− | HandlerException String
67− deriving Generic
68− deriving Serialise via WineryVariant (HandlerResponse a)
85+type TimeoutMicroseconds = Int
6986
87+deriving instance Generic Fingerprint
88+deriving via WineryVariant Fingerprint instance Serialise Fingerprint
89+
90+-- | Internal type used to mark envelope types.
91+data MessageType = RequestMessage TimeoutMicroseconds
92+ | ResponseMessage
93+ | TimeoutResponseMessage
94+ | ExceptionResponseMessage
95+ deriving (Generic, Show)
96+ deriving Serialise via WineryVariant MessageType
97+
98+-- | A list of `RequestHandler`s.
99+type RequestHandlers serverState = [RequestHandler serverState]
100+
101+-- | Data types for server-side request handlers, in synchronous (client waits for return value) and asynchronous (client does not wait for return value) forms.
102+data RequestHandler serverState where
103+ -- | create a request handler with a response
104+ RequestHandler :: forall a b serverState. (Serialise a, Serialise b) => (ConnectionState serverState -> a -> IO b) -> RequestHandler serverState
105+ -- | create an asynchronous request handler where the client does not expect nor await a response
106+ AsyncRequestHandler :: forall a serverState. Serialise a => (ConnectionState serverState -> a -> IO ()) -> RequestHandler serverState
107+
108+-- | Server state sent in via `serve` and passed to `RequestHandler`s.
109+data ConnectionState a = ConnectionState {
110+ connectionServerState :: a,
111+ connectionSocket :: Locking Socket
112+ }
113+
114+-- | Used by server-side request handlers to send additional messages to the client. This is useful for sending asynchronous responses to the client outside of the normal request-response flow. The locking socket can be found in the ConnectionState when a request handler is called.
115+sendMessage :: Serialise a => Locking Socket -> a -> IO ()
116+sendMessage lockSock msg = do
117+ requestID <- UUID <$> UUIDBase.nextRandom
118+ let env =
119+ Envelope (fingerprint msg) (RequestMessage timeout') requestID (serialise msg)
120+ timeout' = 0
121+ sendEnvelope env lockSock
122+
70123--avoid orphan instance
71124newtype UUID = UUID { _unUUID :: UUIDBase.UUID }
72125 deriving (Show, Eq, B.Binary, Hashable)
@@ -85,13 +138,18 @@ instance Serialise UUID where
85138 x -> error $ "invalid schema element " <> show x
86139 decodeCurrent = B.decode . BSL.fromStrict <$> (decodeVarInt >>= getBytes)
87140
88−
141+-- | Errors from remote calls.
89142data ConnectionError = CodecError String -- show of WineryException from exception initiator which cannot otherwise be transmitted over a line due to dependencies on TypeReps
90143 | TimeoutError
91144 | ExceptionError String
92145 deriving (Generic, Show, Eq)
93146 deriving Serialise via WineryVariant ConnectionError
94147
148+data TimeoutException = TimeoutException
149+ deriving Show
150+
151+instance Exception TimeoutException
152+
95153type HostAddr = (Word8, Word8, Word8, Word8)
96154
97155allHostAddrs,localHostAddr :: HostAddr
@@ -99,129 +157,186 @@ allHostAddrs = (0,0,0,0)
99157localHostAddr = (127,0,0,1)
100158
101159-- Each message is length-prefixed by a 32-bit unsigned length.
102−messageBoundaryP :: Parser IO Word8 BS.ByteString
103−messageBoundaryP = do
160+envelopeP :: Parser IO Word8 Envelope
161+envelopeP = do
162+ let msgTypeP = (P.satisfy (== 0) *>
163+ (RequestMessage . fromIntegral <$> word32P)) `P.alt`
164+ (P.satisfy (== 1) $> ResponseMessage) `P.alt`
165+ (P.satisfy (== 2) $> TimeoutResponseMessage) `P.alt`
166+ (P.satisfy (== 3) $> ExceptionResponseMessage)
167+ lenPrefixedByteStringP = do
168+ c <- fromIntegral <$> word32P
169+ --streamly can't handle takeEQ 0, so add special handling
170+ if c == 0 then
171+ pure BS.empty
172+ else
173+ fromArray <$> P.takeEQ c (Arr.writeN c)
174+ Envelope <$> fingerprintP <*> msgTypeP <*> uuidP <*> lenPrefixedByteStringP
175+
176+encodeEnvelope :: Envelope -> BS.ByteString
177+encodeEnvelope (Envelope (Fingerprint fp1 fp2) msgType msgId bs) =
178+ fingerprintBs <> msgTypeBs <> msgIdBs <> lenPrefixedBs
179+ where
180+ fingerprintBs = BO.bytestring64 fp1 <> BO.bytestring64 fp2
181+ msgTypeBs = case msgType of
182+ RequestMessage timeoutms -> BS.singleton 0 <> BO.bytestring32 (fromIntegral timeoutms)
183+ ResponseMessage -> BS.singleton 1
184+ TimeoutResponseMessage -> BS.singleton 2
185+ ExceptionResponseMessage -> BS.singleton 3
186+ msgIdBs =
187+ case UUIDBase.toWords (_unUUID msgId) of
188+ (u1, u2, u3, u4) -> foldr ((<>) . BO.bytestring32) BS.empty [u1, u2, u3, u4]
189+ msgLen = fromIntegral (BS.length bs)
190+ lenPrefixedBs = BO.bytestring32 msgLen <> bs
191+
192+fingerprintP :: Parser IO Word8 Fingerprint
193+fingerprintP =
194+ Fingerprint <$> word64P <*> word64P
195+
196+word64P :: Parser IO Word8 Word64
197+word64P = do
104198 let s = FL.toList
105− w4x8 <- P.take 4 s
106− --traceShowM ("w4x8"::String, w4x8)
107− let c = fromIntegral (BO.word32 (BS.pack w4x8))
108− --traceShowM ("c"::String, c)
109− vals <- P.take c s
110− let bytes = BS.pack vals
111− --traceShowM ("parsedBytes"::String, c, BS.length bytes, bytes)
112− pure bytes
199+ b <- P.takeEQ 8 s
200+ pure (BO.word64 (BS.pack b))
201+
202+--parse a 32-bit integer from network byte order
203+word32P :: Parser IO Word8 Word32
204+word32P = do
205+ let s = FL.toList
206+ w4x8 <- P.takeEQ 4 s
207+ pure (BO.word32 (BS.pack w4x8))
208+
209+-- uuid is encode as 4 32-bit words because of its convenient 32-bit tuple encoding
210+uuidP :: Parser IO Word8 UUID
211+uuidP = do
212+ u1 <- word32P
213+ u2 <- word32P
214+ u3 <- word32P
215+ --u4 <- word32P
216+ --pure (UUID (UUIDBase.fromWords u1 u2 u3 u4))-}
217+ --(UUID . UUIDBase.fromWords) <$> word32P <*> word32P <*> word32P <*> word32P
218+ UUID . UUIDBase.fromWords u1 u2 u3 <$> word32P
113219
114220type NewConnectionHandler msg = IO (Maybe msg)
115221
116−type NewMessageHandler req resp = req -> IO (HandlerResponse resp)
117−
118−serve :: forall req resp. (Show req, Serialise req, Serialise resp) =>
119− NewConnectionHandler resp ->
120− NewMessageHandler req resp ->
222+type NewMessageHandler req resp = req -> IO resp
223+
224+-- | Listen for new connections and handle requests which are passed the server state 's'. The MVar SockAddr can be be optionally used to know when the server is ready for processing requests.
225+serve ::
226+ RequestHandlers s->
227+ s ->
121228 HostAddr ->
122229 PortNumber ->
123230 Maybe (MVar SockAddr) ->
124231 IO Bool
125−serve connhandler userMsgHandler hostaddr port mSockLock = do
232+serve userMsgHandlers serverState hostaddr port mSockLock = do
126233 let
127− decoder :: NewMessageHandler req resp -> Decoder (Message req)
128− decoder _ =
129− case getDecoder (schema (Proxy @(Message req))) of
130− Left exc -> error (show exc)
131− Right dec -> dec
132234 handleSock sock = do
133− mResp <- connhandler
134− case mResp of
135− Nothing -> pure ()
136− Just resp -> do
137− requestID <- UUID <$> UUIDBase.nextRandom
138− sendMessage (AsyncRequest requestID resp) sock
139− drainSocketMessages sock (decoder userMsgHandler) (serverMessageHandler sock userMsgHandler)
140− serially (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & parallely . S.mapM (handleWithM handleSock) & S.drain
235+ lockingSocket <- newLock sock
236+ drainSocketMessages sock (serverEnvelopeHandler lockingSocket userMsgHandlers serverState)
237+
238+ S.serially (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & S.parallely . S.mapM (handleWithM handleSock) & S.drain
141239 pure True
142240
143−serverMessageHandler :: forall req resp. (Show req,
144− Serialise req,
145− Serialise resp)
146− => Socket
147− -> NewMessageHandler req resp
148− -> Message req
241+openEnvelope :: forall s. (Serialise s, Typeable s) => Envelope -> Maybe s
242+openEnvelope (Envelope eprint _ _ bytes) =
243+ if eprint == fingerprint (undefined :: s) then
244+ case deserialise bytes of
245+ Left err -> error (show err)
246+ Right v -> Just v
247+ else
248+ Nothing
249+
250+matchEnvelope :: forall a b s. (Serialise a, Serialise b, Typeable b) =>
251+ Envelope ->
252+ (ConnectionState s -> a -> IO b) ->
253+ Maybe (ConnectionState s -> a -> IO b, a)
254+matchEnvelope envelope dispatchf =
255+ case openEnvelope envelope :: Maybe a of
256+ Nothing -> Nothing
257+ Just decoded -> Just (dispatchf, decoded)
258+
259+-- | Called by `serve` to process incoming envelope requests. Never returns, so use `async` to spin it off on another thread.
260+serverEnvelopeHandler ::
261+ Locking Socket
262+ -> RequestHandlers s
263+ -> s
264+ -> Envelope
149265 -> IO ()
150−serverMessageHandler sock requestMessageHandler msg = do
151− --putStrLn $ "GOT MSG " ++ show msg
152− let --runTimeout :: IO (HandlerResponse resp) -> IO (Maybe (HandlerResponse resp))
153− runTimeout mTimeout m = case mTimeout of
154− Nothing -> Just <$> m
155− Just timeoutMicroseconds -> timeout timeoutMicroseconds m
156− case msg of
157− Response{} -> putStrLn "client sent response"
158− ResponseExpectedRequest requestID mTimeout val -> do
159− let normalResponder = do
160− resp <- runTimeout mTimeout (requestMessageHandler val)
161− case resp of
162− Just (HandlerResponse responseVal) -> sendMessage (Response requestID responseVal) sock
163− Just NoResponse -> error "attempt to return non-response to expected response message"
164− Just (HandlerException exc) -> sendMessage (ExceptionResponse @(Message resp) requestID exc) sock
165− Nothing ->
166− sendMessage (TimedOutResponse @(Message resp) requestID) sock
167− excHandler :: SomeException -> IO ()
168− excHandler e = do
169− --send exception to client
170− sendMessage (ExceptionResponse @(Message resp) requestID (show e)) sock
171− throwIO e
172− catch normalResponder excHandler
173− AsyncRequest _ val -> do
174− void $ requestMessageHandler val
175− --no response necessary
176− ExceptionResponse{} -> putStrLn "client sent exception response"
177− TimedOutResponse{} -> putStrLn "client sent timed out response"
178−
179−--add callback to allow for responses via socket
180−type MessageHandler a = Message a -> IO ()
181−
182−type AsyncMessageHandler a = a -> IO ()
183−
184−
185−
186−drainSocketMessages :: Serialise msg => Socket -> Decoder (Message msg) -> MessageHandler msg -> IO ()
187−drainSocketMessages sock decoder msgHandler = do
266+serverEnvelopeHandler _ _ _ (Envelope _ TimeoutResponseMessage _ _) = pure ()
267+serverEnvelopeHandler _ _ _ (Envelope _ ExceptionResponseMessage _ _) = pure ()
268+serverEnvelopeHandler _ _ _ (Envelope _ ResponseMessage _ _) = pure ()
269+serverEnvelopeHandler sockLock msgHandlers serverState envelope@(Envelope _ (RequestMessage timeoutms) msgId _) = do
270+ --find first matching handler
271+ let runTimeout :: IO b -> IO (Maybe b)
272+ runTimeout m =
273+ if timeoutms == 0 then
274+ (Just <$> m) `catch` timeoutExcHandler
275+ else
276+ (timeout (fromIntegral timeoutms) m) `catch` timeoutExcHandler
277+ --allow server-side function to throw TimeoutError which is caught here and becomes TimeoutError value
278+ timeoutExcHandler :: TimeoutException -> IO (Maybe b)
279+ timeoutExcHandler _ = pure Nothing
280+
281+ sState = ConnectionState {
282+ connectionServerState = serverState,
283+ connectionSocket = sockLock
284+ }
285+
286+ firstMatcher (RequestHandler msghandler) Nothing =
287+ case matchEnvelope envelope msghandler of
288+ Nothing -> pure Nothing
289+ Just (dispatchf, decoded) -> do
290+ --TODO add exception handling
291+ mResponse <- runTimeout (dispatchf sState decoded)
292+ let envelopeResponse =
293+ case mResponse of
294+ Just response ->
295+ Envelope (fingerprint response) ResponseMessage msgId (serialise response)
296+ Nothing ->
297+ Envelope (fingerprint TimeoutError) TimeoutResponseMessage msgId BS.empty
298+ sendEnvelope envelopeResponse sockLock
299+ pure (Just ())
300+ firstMatcher (AsyncRequestHandler msghandler) Nothing =
301+ case matchEnvelope envelope msghandler of
302+ Nothing -> pure Nothing
303+ Just (dispatchf, decoded) -> do
304+ _ <- dispatchf sState decoded
305+ pure (Just ())
306+ firstMatcher _ acc = pure acc
307+ eExc <- try $ foldM_ (flip firstMatcher) Nothing msgHandlers :: IO (Either SomeException ())
308+ case eExc of
309+ Left exc ->
310+ let env = Envelope (fingerprint (show exc)) ExceptionResponseMessage msgId (serialise (show exc)) in
311+ sendEnvelope env sockLock
312+ Right () -> pure ()
313+
314+
315+type EnvelopeHandler = Envelope -> IO ()
316+
317+drainSocketMessages :: Socket -> EnvelopeHandler -> IO ()
318+drainSocketMessages sock envelopeHandler = do
188319 let sockStream = S.unfold readWithBufferOf (1024 * 4, sock)
189− handler bs = do
190− let decoded = evalDecoder decoder bs
191− msgHandler decoded
192− S.drain $ serially $ S.parseMany messageBoundaryP sockStream & S.mapM handler
320+ S.drain $ S.serially $ S.parseMany envelopeP sockStream & S.mapM envelopeHandler
193321
194322--send length-tagged bytestring, perhaps should be in network byte order?
195−sendMessage :: Serialise a => Message a -> Socket -> IO ()
196−sendMessage msg socket' = do
197− let
198− msgbytes = serialiseOnly msg
199− fullbytes = lenbytes <> msgbytes
200− len = BS.length msgbytes
201− lenbytes = BO.bytestring32 (fromIntegral len)
202−
203− byteCount <- Socket.send socket' fullbytes
204− when (byteCount /= BS.length fullbytes) (error "bytes sent mismatch")
205− traceBytes "sent bytes" fullbytes
206−
207−
208−
209−--from streamly-bytestring
210−-- | Convert a 'ByteString' to an array of 'Word8'. This function unwraps the
211−-- 'ByteString' and wraps it with 'Array' constructors and hence the operation
212−-- is performed in constant time.
213−{-
214−{-# INLINE toArrayS #-}
215−toArrayS :: BS.ByteString -> SA.Array Word8
216−--slow path
217−--toArrayS = SA.fromList . BS.unpack
218−toArrayS (BSI.PS fp off len) = SA.Array nfp endPtr
219− where
220− nfp = fp `plusForeignPtr` off
221− endPtr = unsafeForeignPtrToPtr nfp `plusPtr` len `plusPtr` 1 -- ? why +1?
323+sendEnvelope :: Envelope -> Locking Socket -> IO ()
324+sendEnvelope envelope sockLock = do
325+ let envelopebytes = encodeEnvelope envelope
326+ fullbytes = envelopebytes
327+ --Socket.sendAll syscalls send() on a loop until all the bytes are sent, so we need socket locking here to account for serialized messages of size > PIPE_BUF
328+ withLock sockLock $ \socket' ->
329+ Socket.sendAll socket' fullbytes
330+ --traceShowM ("sendEnvelope", envelope)
331+ traceBytes "sendEnvelope" fullbytes
332+
333+fingerprint :: Typeable a => a -> Fingerprint
334+fingerprint = typeRepFingerprint . typeOf
222335
223−toArraySlow :: BS.ByteString -> SA.Array Word8
224−toArraySlow bs = arr
336+fromArray :: Arr.Array Word8 -> BSI.ByteString
337+fromArray Arr.Array {..}
338+ | aLen == 0 = mempty
339+ | otherwise = BSI.PS aStart 0 aLen
225340 where
226− arr = SA.fromList (BS.unpack bs)
227−-}
341+ aStartPtr = unsafeForeignPtrToPtr aStart
342+ aLen = aEnd `minusPtr` aStartPtr
test/Curryer/Test/Basic.hs+148−78
@@ -1,4 +1,4 @@
1−{-# LANGUAGE DerivingVia, DeriveGeneric, TypeApplications #-}
1+{-# LANGUAGE DerivingVia, DeriveGeneric, TypeApplications, ExistentialQuantification #-}
22module Curryer.Test.Basic where
33import Test.Tasty
44import Test.Tasty.HUnit
@@ -9,108 +9,133 @@ import Network.Socket (SockAddr(..))
99import Control.Concurrent.Async
1010import Control.Monad
1111import Control.Concurrent
12+import Control.Concurrent.STM
13+import Control.Exception
1214import Data.List
1315
1416import Network.RPC.Curryer.Server
1517import Network.RPC.Curryer.Client
1618
19+-- TODO: add test for nested calls
20+
1721testTree :: TestTree
18−testTree = testGroup "basic" [testCase "simple" testSimpleCall,
19− testCase "client async" testAsyncServerCall,
20− testCase "server async" testAsyncClientCall,
21− testCase "client sync timeout" testSyncClientCallTimeout,
22− testCase "server-side exception" testSyncException
22+testTree = testGroup "basic" [testCase "simple" testSimpleCall
23+ ,testCase "client async" testAsyncServerCall
24+ ,testCase "server async" testAsyncClientCall
25+ ,testCase "client sync timeout" testSyncClientCallTimeout
26+ ,testCase "server-side exception" testSyncException
27+ ,testCase "multi-threaded client" testMultithreadedClient
28+ ,testCase "server state" testServerState
29+ ,testCase "request handler throws timeout" testRequestHandlerThrowTimeout
2330 ]
2431
25−--client-to-server request
26−data TestRequest = AddTwoNumbersReq Int Int
27− | TestAsyncReq String
28− | TestCallMeBackReq String
29− | DelayMicrosecondsReq Int
30− | ThrowServerSideExceptionReq
32+
33+data AddTwoNumbersReq = AddTwoNumbersReq Int Int
3134 deriving (Generic, Show)
32− deriving Serialise via WineryVariant TestRequest
35+ deriving Serialise via WineryVariant AddTwoNumbersReq
3336
34−--server responds to client
35−data TestResponse = AddTwoNumbersResp Int
36− | AsyncHello String
37− | TestCallMeBackResp String
38− | DelayMicrosecondsResp
37+data TestCallMeBackReq = TestCallMeBackReq String
3938 deriving (Generic, Show, Eq)
40− deriving Serialise via WineryVariant TestResponse
41−
42−testServerMessageHandler :: Maybe (MVar String) -> TestRequest -> IO (HandlerResponse TestResponse)
43−testServerMessageHandler mAsyncMVar msg = do
44− --print msg
45− case msg of
46− --round-trip request to add two Ints
47− AddTwoNumbersReq x y -> pure (HandlerResponse (AddTwoNumbersResp (x+y)))
48− --respond to a request for an async callback
49− TestCallMeBackReq s -> do
50− case mAsyncMVar of
51− Nothing -> pure ()
52− Just mvar -> putMVar mvar s
53− pure NoResponse
54− TestAsyncReq _ -> pure NoResponse
55− DelayMicrosecondsReq ms -> do
39+ deriving Serialise via WineryVariant TestCallMeBackReq
40+
41+data TestAsyncReq = TestAsyncReq String
42+ deriving (Generic, Show)
43+ deriving Serialise via WineryVariant TestAsyncReq
44+
45+data DelayMicrosecondsReq = DelayMicrosecondsReq Int
46+ deriving (Generic, Show)
47+ deriving Serialise via WineryVariant DelayMicrosecondsReq
48+
49+data RoundtripStringReq = RoundtripStringReq String
50+ deriving (Generic, Show)
51+ deriving Serialise via WineryVariant RoundtripStringReq
52+
53+data ThrowServerSideExceptionReq = ThrowServerSideExceptionReq
54+ deriving (Generic, Show)
55+ deriving Serialise via WineryVariant ThrowServerSideExceptionReq
56+
57+data ChangeServerState = ChangeServerState
58+ deriving (Generic, Show)
59+ deriving Serialise via WineryVariant ChangeServerState
60+
61+--used to server -> client async request
62+data AsyncHelloReq = AsyncHelloReq String
63+ deriving (Generic, Show)
64+ deriving Serialise via WineryVariant AsyncHelloReq
65+
66+data ThrowTimeout = ThrowTimeout
67+ deriving (Generic, Show)
68+ deriving Serialise via WineryVariant ThrowTimeout
69+
70+testServerRequestHandlers :: Maybe (MVar String) -> RequestHandlers ()
71+testServerRequestHandlers mAsyncMVar =
72+ [ RequestHandler $ \_ (AddTwoNumbersReq x y) -> pure (x + y)
73+ , RequestHandler $ \_ (TestCallMeBackReq s) ->
74+ case mAsyncMVar of
75+ Nothing -> pure ()
76+ Just mvar -> putMVar mvar s
77+ , AsyncRequestHandler $ \_ (TestAsyncReq v) ->
78+ maybe (pure ()) (\mvar -> putMVar mvar v) mAsyncMVar
79+ -- an async hello to the server generates an async hello to the client
80+ , AsyncRequestHandler $ \sState (AsyncHelloReq s) -> do
81+ sendMessage (connectionSocket sState) (AsyncHelloReq s)
82+
83+ , RequestHandler $ \_ (RoundtripStringReq s) -> pure s
84+ , RequestHandler $ \_ (DelayMicrosecondsReq ms) -> do
5685 threadDelay ms
57− pure (HandlerResponse DelayMicrosecondsResp)
58− ThrowServerSideExceptionReq -> error "test server exception"
86+ pure ()
87+ , RequestHandler $ \_ ThrowServerSideExceptionReq -> do
88+ _ <- error "test server exception"
89+ pure ()
90+ ]
5991
6092-- test a simple client-to-server round-trip function execution
6193testSimpleCall :: Assertion
6294testSimpleCall = do
6395 readyVar <- newEmptyMVar
6496
65− server <- async (serve (pure Nothing) (testServerMessageHandler Nothing) localHostAddr 0 (Just readyVar))
97+ server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
6698 --wait for server to be ready
6799 (SockAddrInet port _) <- takeMVar readyVar
68− let clientHandler :: AsyncMessageHandler TestResponse
69− clientHandler _ = error "async handler called"
70− mkConn :: IO (Connection TestResponse)
71− mkConn = connect clientHandler localHostAddr port
72− conn <- mkConn
73− let c :: IO (Either ConnectionError TestResponse)
74− c = call conn (AddTwoNumbersReq 1 1)
100+ conn <- connect [] localHostAddr port
75101 replicateM_ 5 $ do --make five AddTwo calls to shake out parallelism bugs
76− x <- c
77− assertEqual "server request+response" (Right (AddTwoNumbersResp 2)) x
102+ x <- call conn (AddTwoNumbersReq 1 1)
103+ assertEqual "server request+response" (Right (2 :: Int)) x
78104 close conn
79105 cancel server
80106
107+
81108--test that the client can proces a server-initiated asynchronous callback from the server
82109testAsyncServerCall :: Assertion
83110testAsyncServerCall = do
84111 portReadyVar <- newEmptyMVar
85112 receivedAsyncMessageVar <- newEmptyMVar
86−
87− let testServerNewConnHandler :: NewConnectionHandler TestResponse
88− testServerNewConnHandler = do
89− pure (Just (AsyncHello "welcome"))
90−
91− server <- async (serve testServerNewConnHandler (testServerMessageHandler Nothing) localHostAddr 0 (Just portReadyVar))
113+ let clientAsyncHandlers =
114+ [ClientAsyncRequestHandler (\(AsyncHelloReq s) ->
115+ putMVar receivedAsyncMessageVar s)]
116+ server <- async (serve (testServerRequestHandlers (Just receivedAsyncMessageVar)) emptyServerState localHostAddr 0 (Just portReadyVar))
92117 (SockAddrInet port _) <- takeMVar portReadyVar
93− let clientHandler :: AsyncMessageHandler TestResponse
94− clientHandler msg = do
95− case msg of
96− AsyncHello str -> putMVar receivedAsyncMessageVar str
97− x -> error ("unexpected message " <> show x)
98− conn <- connect @TestResponse clientHandler localHostAddr port
118+ conn <- connect clientAsyncHandlers localHostAddr port
119+ Right () <- asyncCall conn (AsyncHelloReq "welcome")
99120 asyncMessage <- takeMVar receivedAsyncMessageVar
100121 assertEqual "async message" "welcome" asyncMessage
101122 close conn
102123 cancel server
103124
125+
126+emptyServerState :: ()
127+emptyServerState = ()
128+
104129--test that the client can make a non-blocking call
105130testAsyncClientCall :: Assertion
106131testAsyncClientCall = do
107132 portReadyVar <- newEmptyMVar
108133 receivedAsyncMessageVar <- newEmptyMVar
109134
110− server <- async (serve (pure Nothing) (testServerMessageHandler (Just receivedAsyncMessageVar)) localHostAddr 0 (Just portReadyVar))
135+ server <- async (serve (testServerRequestHandlers (Just receivedAsyncMessageVar)) emptyServerState localHostAddr 0 (Just portReadyVar))
111136 (SockAddrInet port _) <- takeMVar portReadyVar
112−
113− conn <- connect @TestResponse (\_ -> pure ()) localHostAddr port
137+
138+ conn <- connect [] localHostAddr port
114139 --send an async message, wait for an async response to confirm receipt
115140 Right () <- asyncCall conn (TestCallMeBackReq "hi server")
116141 asyncMessage <- takeMVar receivedAsyncMessageVar
@@ -122,37 +147,82 @@ testSyncClientCallTimeout :: Assertion
122147testSyncClientCallTimeout = do
123148 readyVar <- newEmptyMVar
124149
125− server <- async (serve (pure Nothing) (testServerMessageHandler Nothing) localHostAddr 0 (Just readyVar))
150+ server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
126151 --wait for server to be ready
127152 (SockAddrInet port _) <- takeMVar readyVar
128− let clientHandler :: AsyncMessageHandler TestResponse
129− clientHandler _ = error "async handler called"
130− mkConn :: IO (Connection TestResponse)
131− mkConn = connect clientHandler localHostAddr port
132− conn <- mkConn
133− let c :: IO (Either ConnectionError TestResponse)
134− c = callTimeout (Just 500) conn (DelayMicrosecondsReq 1000)
135− x <- c
153+ conn <- connect [] localHostAddr port
154+ x <- callTimeout @_ @Int (Just 500) conn (DelayMicrosecondsReq 1000)
136155 assertEqual "client sync timeout" (Left TimeoutError) x
137156 close conn
138157 cancel server
139−
158+
140159testSyncException :: Assertion
141160testSyncException = do
142161 readyVar <- newEmptyMVar
143162
144− server <- async (serve (pure Nothing) (testServerMessageHandler Nothing) localHostAddr 0 (Just readyVar))
163+ server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
145164 (SockAddrInet port _) <- takeMVar readyVar
146− let clientHandler :: AsyncMessageHandler TestResponse
147− clientHandler _ = error "async handler called"
148− mkConn :: IO (Connection TestResponse)
149− mkConn = connect clientHandler localHostAddr port
150− conn <- mkConn
165+
166+ conn <- connect [] localHostAddr port
151167 ret <- call conn ThrowServerSideExceptionReq
152168 case ret of
153169 Left (ExceptionError actualExc) ->
154170 assertBool "server-side exception" ("test server exception" `isPrefixOf` actualExc)
155− Right _ -> assertFailure "missed exception"
171+ Left otherErr ->
172+ assertFailure ("server-side error " <> show otherErr)
173+ Right () -> assertFailure "missed exception"
174+ close conn
175+ cancel server
176+
177+--throw large messages (> PIPE_BUF) at the server from multiple client connections to exercise the socket lock
178+testMultithreadedClient :: Assertion
179+testMultithreadedClient = do
180+ readyVar <- newEmptyMVar
181+
182+ server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
183+ (SockAddrInet port _) <- takeMVar readyVar
184+ conn <- connect [] localHostAddr port
185+ let bigString = replicate (1024 * 1000) 'x'
186+ replicateM_ 10 $ do
187+ ret <- call conn (RoundtripStringReq bigString)
188+ assertEqual "big string multithread" (Right bigString) ret
189+ --putStrLn "plus one"
190+ close conn
191+ cancel server
192+
193+testServerState :: Assertion
194+testServerState = do
195+ readyVar <- newEmptyMVar
196+
197+ let serverHandlers = [RequestHandler (\sState ChangeServerState ->
198+ atomically $
199+ modifyTVar (connectionServerState sState) (+ 1))
200+ ]
201+ serverState <- newTVarIO @Int 1
202+ server <- async (serve serverHandlers serverState localHostAddr 0 (Just readyVar))
203+ (SockAddrInet port _) <- takeMVar readyVar
204+ conn <- connect [] localHostAddr port
205+ ret <- call conn ChangeServerState
206+ assertEqual "server ret" (Right ()) ret
207+ serverState' <- readTVarIO serverState
208+ assertEqual "server state" 2 serverState'
209+ close conn
210+ cancel server
211+
212+--test that the request handler can throw a TimeoutError exception which is converted into a TimeoutError response
213+testRequestHandlerThrowTimeout :: Assertion
214+testRequestHandlerThrowTimeout = do
215+ readyVar <- newEmptyMVar
216+
217+ let serverHandlers = [RequestHandler (\_ ThrowTimeout ->
218+ throw TimeoutException >> pure (1 :: Int)
219+ )
220+ ]
221+ server <- async (serve serverHandlers () localHostAddr 0 (Just readyVar))
222+ (SockAddrInet port _) <- takeMVar readyVar
223+ conn <- connect [] localHostAddr port
224+ ret <- call @_ @Int conn ThrowTimeout
225+ assertEqual "handler timeout exception" (Left TimeoutError) ret
156226 close conn
157227 cancel server
158228