on 791da98 help sign in
Changelog.markdowndeleted−4
since splitsnapshots
unifiedsplit
@@ -1,4 +0,0 @@
1−# v0.1 (2020-12-27)
2−
3−* initial release to support [Project:M36](https://github.com/agentm/project-m36)
4−
README.markdown+2−68
@@ -1,78 +1,12 @@
1# Curryer - Fast Haskell-to-Haskell RPC
1# Curryer - Fast Haskell-to-Haskell RPC
2
2
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.
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.
4
4
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.
5+Curryer is inspired by the now unmaintained [distributed-process](https://hackage.haskell.org/package/distributed-process) library.
6
6
7## Features
7## Features
8
8
9* blocking and non-blocking remote function calls
9* blocking and non-blocking remote function calls
10* asynchronous server-to-client callbacks (for server-initiated notifications)
10* asynchronous server-to-client callbacks (for server-initiated notifications)
11* timeouts
11* timeouts
12−* leverages [winery](https://hackage.haskell.org/package/winery) for high-performance serialization
13
12
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.shdeleted−1
@@ -1 +0,0 @@
1−cabal run perf -- +RTS -p && ~/.cabal/bin/ghc-prof-flamegraph < perf.prof > perf.html
bench/Basic.hs+13−43
@@ -9,70 +9,40 @@ import Codec.Winery
9import Control.Concurrent.Async
9import Control.Concurrent.Async
10import Control.Concurrent
10import Control.Concurrent
11import Control.Monad
11import Control.Monad
12−import qualified Data.ByteString as BS
13
12
14main :: IO ()
13main :: IO ()
15main = do
14main = do
16 --start server shared across benchmarks- gather stats on round-trip requests-responses
15 --start server shared across benchmarks- gather stats on round-trip requests-responses
17 portReadyMVar <- newEmptyMVar
16 portReadyMVar <- newEmptyMVar
18− server <- async (serve benchmarkServerRequestHandlers () localHostAddr 0 (Just portReadyMVar))
17+ server <- async (serve (pure Nothing) benchmarkServerMessageHandler localHostAddr 0 (Just portReadyMVar))
19 --wait for server to be ready
18 --wait for server to be ready
20 (SockAddrInet port _) <- takeMVar portReadyMVar
19 (SockAddrInet port _) <- takeMVar portReadyMVar
21− clientConn <- connect [] localHostAddr port
20+ clientConn <- connect (const (pure ())) localHostAddr port
22
21
23 _ <- syncWaitRequest clientConn 100
22 _ <- syncWaitRequest clientConn 100
24− let syncWait ms = nfIO (syncWaitRequest clientConn ms)
25− syncByteString bs = nfIO (syncByteStringRequest clientConn bs)
26− bs0 = BS.empty
27− bs100 = BS.pack (replicate 100 3)
28− bs1000 = BS.pack (replicate 1000 3)
29− bs1M = BS.pack (replicate 1000000 3)
30− defaultMain [bgroup "wait sync" [bench "0 ms" (syncWait 0),
23+ defaultMain [bgroup "wait sync" [bench "100 ms" (nfIO $ syncWaitRequest clientConn 100)]]
31− bench "10 ms" (syncWait 10),
32− 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)
37− ]]
38−
39− --TODO: bench for > PIPE_BUF bytes
40
24
25+ putStrLn "benchmark complete"
41 close clientConn
26 close clientConn
42 cancel server
27 cancel server
43
28
44−data WaitMillisecondsReq = WaitMillisecondsReq Int --ask the server to respond in X milliseconds
29+data BenchRequest = WaitMillisecondsReq Int
45− deriving (Generic, Show)
46− deriving Serialise via WineryVariant WaitMillisecondsReq
47−
48−data WaitByteStringReq = WaitByteStringReq BS.ByteString
49 deriving (Generic, Show)
30 deriving (Generic, Show)
50− deriving Serialise via WineryVariant WaitByteStringReq
31+ deriving Serialise via WineryVariant BenchRequest
51
32
52−data WaitMillisecondsResp = WaitMillisecondsResp
33+data BenchResponse = WaitMillisecondsResp
53 deriving (Generic, Show, Eq)
34 deriving (Generic, Show, Eq)
54− deriving Serialise via WineryVariant WaitMillisecondsResp
35+ deriving Serialise via WineryVariant BenchResponse
55
36
56−benchmarkServerRequestHandlers :: RequestHandlers a
37+benchmarkServerMessageHandler :: BenchRequest -> IO (HandlerResponse BenchResponse)
57−benchmarkServerRequestHandlers =
38+benchmarkServerMessageHandler (WaitMillisecondsReq ms) = do
58− [RequestHandler $ \_ (WaitMillisecondsReq ms) -> do
59− threadDelay (1000 * ms)
39+ threadDelay (1000 * ms)
60− pure WaitMillisecondsResp,
40+ pure (HandlerResponse WaitMillisecondsResp)
61− RequestHandler $ \_ (WaitByteStringReq bs) ->
62− pure bs
63− ]
64
41
65--perform 10000 small synchronous requests
42--perform 10000 small synchronous requests
66−syncWaitRequest :: Connection -> Int -> IO Int
43+syncWaitRequest :: Connection BenchResponse -> Int -> IO Int
67syncWaitRequest conn ms = do
44syncWaitRequest conn ms = do
68 ret <- call conn (WaitMillisecondsReq ms)
45 ret <- call conn (WaitMillisecondsReq ms)
69 when (ret /= Right WaitMillisecondsResp) (error "ret failed")
46 when (ret /= Right WaitMillisecondsResp) (error "ret failed")
70 pure ms
47 pure ms
71
48
72−syncByteStringRequest :: Connection -> BS.ByteString -> IO BS.ByteString
73−syncByteStringRequest conn bs = do
74− ret <- call conn (WaitByteStringReq bs)
75− case ret of
76− Right bs' -> pure bs'
77− Left err -> error (show err)
78−
cabal.project+1−1
@@ -3,4 +3,4 @@ packages: .
3source-repository-package
3source-repository-package
4 type: git
4 type: git
5 location: https://github.com/composewell/streamly.git
5 location: https://github.com/composewell/streamly.git
6− tag: 4e48b52e8709390a47e99c107ebed2d1c9420076
6+ tag: e218fb9d8f4263fd69d32a2ff318bac0e179c642
curryer-rpc.cabal → curryer.cabalrenamed+6−24
@@ -1,4 +1,4 @@
1−Name: curryer-rpc
1+Name: curryer
2Version: 0.1
2Version: 0.1
3License: PublicDomain
3License: PublicDomain
4Build-Type: Simple
4Build-Type: Simple
@@ -11,14 +11,14 @@ Maintainer: agentm@themactionfaction.com
11Cabal-Version: >= 1.10
11Cabal-Version: >= 1.10
12Synopsis: Fast, Haskell RPC
12Synopsis: Fast, Haskell RPC
13Description: Haskell-to-Haskell RPC using Winery serialization.
13Description: Haskell-to-Haskell RPC using Winery serialization.
14−Extra-Source-Files: Changelog.markdown README.markdown cabal.project
14+Extra-Source-Files: Changelog.markdown README.markdown
15
15
16Source-Repository head
16Source-Repository head
17 Type: git
17 Type: git
18 location: https://github.com/agentm/curryer
18 location: https://github.com/agentm/curryer
19
19
20Library
20Library
21− Build-Depends: base >= 4.12 && < 4.15
21+ Build-Depends: base >= 4.8
22 , winery
22 , winery
23 , bytestring
23 , bytestring
24 , streamly >= 0.7.2
24 , streamly >= 0.7.2
@@ -31,9 +31,6 @@ Library
31 , containers
31 , containers
32 , stm-containers
32 , stm-containers
33 , hashable
33 , hashable
34− , time
35− , network-byte-order
36− , stm
37 Hs-Source-Dirs: ./src
34 Hs-Source-Dirs: ./src
38 Default-Language: Haskell2010
35 Default-Language: Haskell2010
39 ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports
36 ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports
@@ -51,33 +48,18 @@ Test-Suite test
51 build-depends: tasty
48 build-depends: tasty
52 , tasty-hunit
49 , tasty-hunit
53 , base
50 , base
54− , curryer-rpc
51+ , curryer
55 , winery
52 , winery
56 , network
53 , network
57 , async
54 , async
58− , stm
59 other-modules: Curryer.Test.Basic
55 other-modules: Curryer.Test.Basic
60
56
61Benchmark perf
57Benchmark perf
62 Default-Language: Haskell2010
58 Default-Language: Haskell2010
63 Default-Extensions: OverloadedStrings
59 Default-Extensions: OverloadedStrings
64− Build-Depends: base, criterion, curryer-rpc, network, winery, async, bytestring
60+ Build-Depends: base, criterion, curryer, network, winery, async
65−
66 Main-Is: Basic.hs
61 Main-Is: Basic.hs
67 Type: exitcode-stdio-1.0
62 Type: exitcode-stdio-1.0
68 GHC-Options: -Wall -threaded -rtsopts
63 GHC-Options: -Wall -threaded -rtsopts
69− -- -fprof-auto
70 HS-Source-Dirs: ./bench
64 HS-Source-Dirs: ./bench
71−
65+
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.hsdeleted−39
@@ -1,39 +0,0 @@
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.hsdeleted−30
@@ -1,30 +0,0 @@
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+49−122
@@ -1,4 +1,3 @@
1−{-# LANGUAGE RankNTypes, ScopedTypeVariables, GADTs #-}
2module Network.RPC.Curryer.Client where
1module Network.RPC.Curryer.Client where
3import Network.RPC.Curryer.Server
2import Network.RPC.Curryer.Server
4import Network.Socket as Socket
3import Network.Socket as Socket
@@ -9,139 +8,67 @@ import qualified Data.UUID.V4 as UUIDBase
9import qualified StmContainers.Map as STMMap
8import qualified StmContainers.Map as STMMap
10import Control.Concurrent.MVar
9import Control.Concurrent.MVar
11import GHC.Conc
10import GHC.Conc
12−import Data.Time.Clock
13−import System.Timeout
14−import Control.Monad
15
11
16−type SyncMap = STMMap.Map UUID (MVar (Either ConnectionError BinaryMessage), UTCTime)
12+type SyncMap a = STMMap.Map UUID (MVar a)
13+-- the request map holds
14+data Connection a = Connection Socket (Async ()) (SyncMap a)
17
15
18−-- | Represents a remote connection to server.
16+connect :: (Serialise msg) =>
19−data Connection = Connection { _conn_sockLock :: Locking Socket,
17+ AsyncMessageHandler msg ->
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 ->
18+ HostAddr ->
34− PortNumber ->
19+ PortNumber ->
35− IO Connection
20+ IO (Connection msg)
36−connect asyncHandlers hostAddr portNum = do
21+connect notificationCallback hostAddr portNum = do
37 sock <- TCP.connect hostAddr portNum
22 sock <- TCP.connect hostAddr portNum
38 syncmap <- STMMap.newIO
23 syncmap <- STMMap.newIO
39− asyncThread <- async (clientAsync sock syncmap asyncHandlers)
24+ asyncThread <- async (clientAsync sock syncmap notificationCallback)
40− sockLock <- newLock sock
41− pure (Connection {
25+ pure (Connection sock asyncThread syncmap)
42− _conn_sockLock = sockLock,
43− _conn_asyncThread = asyncThread,
44− _conn_syncmap = syncmap
45− })
46
26
47−-- | Close the connection and release all connection resources.
48−close :: Connection -> IO ()
27+close :: Connection a -> IO ()
49−close conn = do
28+close (Connection sock asyncThread _) = do
50− withLock (_conn_sockLock conn) $ \sock ->
51− Socket.close sock
29+ Socket.close sock
52− cancel (_conn_asyncThread conn)
30+ cancel asyncThread
53
31
54−-- | async thread for handling client-side incoming messages- dispatch to proper waiting thread or asynchronous notifications handler
32+-- async thread for handling client-side incoming messages- dispatch to proper waiting thread or handler asynchronous notifications
55−clientAsync ::
33+clientAsync :: Serialise b =>
56 Socket ->
34 Socket ->
57− SyncMap ->
35+ SyncMap b ->
58− ClientAsyncRequestHandlers ->
36+ AsyncMessageHandler b ->
59 IO ()
37 IO ()
60−clientAsync sock syncmap asyncHandlers = do
38+clientAsync sock syncmap asyncHandler = do
61− lsock <- newLock sock
39+ -- ping proper thread to continue
40+ let responseHandler responseMsg = do
41+ putStrLn "client-side message handler"
42+ case responseMsg of
43+ Response requestId val -> do
44+ varval <- atomically $ do
45+ v <- STMMap.lookup requestId syncmap
46+ STMMap.delete requestId syncmap
47+ pure v
48+ case varval of
49+ Nothing -> error "dumped unrequested response"
50+ Just mVar -> putMVar mVar val
51+ AsyncRequest _ asyncMsg ->
52+ asyncHandler asyncMsg
53+ ResponseExpectedRequest _ _ -> error "dumped response expected request"
54+ ExceptionResponse _ -> error "TODO Exception"
62− drainSocketMessages sock (clientEnvelopeHandler asyncHandlers lsock syncmap)
55+ drainSocketMessages sock responseHandler
63
56
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)
103−call = callTimeout Nothing
104−
105−-- | Send a request to the remote server and returns a response but with the possibility of a timeout after n microseconds.
57+-- | Send a request to the remote server and returns a response.
106−callTimeout :: (Serialise request, Serialise response) => Maybe Int -> Connection -> request -> IO (Either ConnectionError response)
58+call :: (Serialise request, Serialise response) => Connection response -> request -> IO (Either ConnectionError response)
107−callTimeout mTimeout conn msg = do
59+call (Connection sock _ mVarMap) msg = do
108− requestID <- UUID <$> UUIDBase.nextRandom
60+ requestID <- UUID <$> UUIDBase.nextRandom
109− let mVarMap = _conn_syncmap conn
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
117 -- setup mvar to wait for response
61 -- setup mvar to wait for response
118 responseMVar <- newEmptyMVar
62 responseMVar <- newEmptyMVar
119− now <- getCurrentTime
120− atomically $ STMMap.insert (responseMVar, now) requestID mVarMap
63+ atomically $ STMMap.insert responseMVar requestID mVarMap
121− sendEnvelope envelope (_conn_sockLock conn)
64+ sendMessage (ResponseExpectedRequest requestID msg) sock
122− let timeoutMicroseconds =
123− case mTimeout of
124− Just timeout' -> timeout' + 100 --add 100 ms to account for unknown network latency
125− Nothing -> -1
126− mResponse <- timeout timeoutMicroseconds (takeMVar responseMVar)
65+ response <- takeMVar responseMVar
127 atomically $ STMMap.delete requestID mVarMap
66 atomically $ STMMap.delete requestID mVarMap
128− case mResponse of
67+ pure (Right response)
129− --timeout
130− Nothing ->
131− pure (Left TimeoutError)
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)
139
68
140−-- | Call a remote function but do not expect a response from the server.
141−asyncCall :: Serialise request => Connection -> request -> IO (Either ConnectionError ())
69+asyncCall :: (Serialise request, Serialise response) => Connection response -> request -> IO (Either ConnectionError ())
142−asyncCall conn msg = do
70+asyncCall (Connection sock _ _) msg = do
143 requestID <- UUID <$> UUIDBase.nextRandom
71 requestID <- UUID <$> UUIDBase.nextRandom
144− let envelope = Envelope fprint (RequestMessage 0) requestID (serialise msg)
72+ sendMessage (AsyncRequest requestID msg) sock
145− fprint = fingerprint msg
146− sendEnvelope envelope (_conn_sockLock conn)
147 pure (Right ())
73 pure (Right ())
74+
src/Network/RPC/Curryer/Server.hs+138−263
@@ -1,20 +1,18 @@
1−{-# LANGUAGE DerivingVia, DeriveGeneric, RankNTypes, ScopedTypeVariables, MultiParamTypeClasses, OverloadedStrings, GeneralizedNewtypeDeriving, CPP, ExistentialQuantification, StandaloneDeriving, GADTs, RecordWildCards #-}
1+{-# LANGUAGE DerivingVia, DeriveGeneric, RankNTypes, ScopedTypeVariables, MultiParamTypeClasses, OverloadedStrings, GeneralizedNewtypeDeriving #-}
2−{-# OPTIONS_GHC -fno-warn-orphans #-}
3{- HLINT ignore "Use lambda-case" -}
2{- HLINT ignore "Use lambda-case" -}
4module Network.RPC.Curryer.Server where
3module Network.RPC.Curryer.Server where
4+import Streamly
5import qualified Streamly.Prelude as S
5import qualified Streamly.Prelude as S
6import Streamly.Network.Socket
6import Streamly.Network.Socket
7−import Streamly.Internal.Network.Socket (handleWithM)
7+import Streamly.Internal.Network.Socket (handleWithM, writeChunk)
8import Network.Socket as Socket
8import Network.Socket as Socket
9import Network.Socket.ByteString as Socket
9import Network.Socket.ByteString as Socket
10−import Streamly.Internal.Data.Parser as P hiding (concatMap)
10+import Streamly.Internal.Data.Parser.ParserD as PD
11import Codec.Winery
11import Codec.Winery
12import Codec.Winery.Internal (varInt, decodeVarInt, getBytes)
12import Codec.Winery.Internal (varInt, decodeVarInt, getBytes)
13import Codec.Winery.Class (mkExtractor)
13import Codec.Winery.Class (mkExtractor)
14import GHC.Generics
14import GHC.Generics
15−import GHC.Fingerprint
16−import Data.Typeable
17−import Control.Concurrent.MVar (MVar, newMVar, withMVar)
15+import Control.Concurrent.MVar (MVar)
18import Control.Exception
16import Control.Exception
19import Data.Function ((&))
17import Data.Function ((&))
20import Data.Word
18import Data.Word
@@ -23,103 +21,53 @@ import qualified Data.ByteString.Lazy as BSL
23import qualified Data.ByteString.FastBuilder as BB
21import qualified Data.ByteString.FastBuilder as BB
24import Streamly.Data.Fold as FL
22import Streamly.Data.Fold as FL
25import qualified Streamly.Internal.Data.Stream.IsStream as S
23import qualified Streamly.Internal.Data.Stream.IsStream as S
24+import Data.Foldable
25+import Data.Bits
26import qualified Data.Binary as B
26import qualified Data.Binary as B
27import qualified Data.UUID as UUIDBase
27import qualified Data.UUID as UUIDBase
28import qualified Data.UUID.V4 as UUIDBase
28import qualified Data.UUID.V4 as UUIDBase
29−import Control.Monad
29+import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as SA
30−import Data.Functor
31−
32import qualified Network.RPC.Curryer.StreamlyAdditions as SA
30import qualified Network.RPC.Curryer.StreamlyAdditions as SA
33−--import Control.Monad
31+import Control.Monad
34import Data.Hashable
32import Data.Hashable
35−import System.Timeout
36−import qualified Network.ByteOrder as BO
37−
38
33
39-- for toArrayS conversion
34-- for toArrayS conversion
40import qualified Data.ByteString.Internal as BSI
35import qualified Data.ByteString.Internal as BSI
41−import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as Arr
36+import Foreign.ForeignPtr (plusForeignPtr)
42import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
37import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
43−import GHC.Ptr (minusPtr)
38+import GHC.Ptr (plusPtr)
44
39
45−-- define CURRYER_SHOW_BYTES 1
46−#if CURRYER_SHOW_BYTES == 1
47import Debug.Trace
40import Debug.Trace
48−#endif
49−
50−traceBytes :: Applicative f => String -> BS.ByteString -> f ()
51−#if CURRYER_SHOW_BYTES == 1
52−traceBytes msg bs = traceShowM (msg, BS.length bs, bs)
53−#else
54−traceBytes _ _ = pure ()
55−#endif
56−
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
41
71−type Timeout = Word32
42+data ClientHelloMessage = ClientHelloMessage Int Int
43+ deriving Generic
44+ deriving Serialise via WineryVariant ClientHelloMessage
45+
46+data ServerHelloMessage = ServerHelloMessage { serverServices :: [String],
47+ serverInfo :: String
48+ }
49+ deriving Generic
50+ deriving Serialise via WineryRecord ServerHelloMessage
51+
52+--request-response token
53+--client-side timeout
54+--server-to-client and client-to-server async request (no response requested)
55+
56+data Message a = Response UUID a
57+ | AsyncRequest UUID a
58+ | ResponseExpectedRequest UUID a
59+ -- | Services
60+ | ExceptionResponse String
61+ deriving (Generic, Show)
62+ deriving Serialise via WineryVariant (Message a)
63+
64+-- | The response type
65+data HandlerResponse a = NoResponse
66+ | HandlerResponse a
67+ | HandlerException a
68+ deriving Generic
69+ deriving Serialise via WineryVariant (HandlerResponse a)
72
70
73−type BinaryMessage = BS.ByteString
74−
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)
84−
85−type TimeoutMicroseconds = Int
86−
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−
123--avoid orphan instance
71--avoid orphan instance
124newtype UUID = UUID { _unUUID :: UUIDBase.UUID }
72newtype UUID = UUID { _unUUID :: UUIDBase.UUID }
125 deriving (Show, Eq, B.Binary, Hashable)
73 deriving (Show, Eq, B.Binary, Hashable)
@@ -138,18 +86,12 @@ instance Serialise UUID where
138 x -> error $ "invalid schema element " <> show x
86 x -> error $ "invalid schema element " <> show x
139 decodeCurrent = B.decode . BSL.fromStrict <$> (decodeVarInt >>= getBytes)
87 decodeCurrent = B.decode . BSL.fromStrict <$> (decodeVarInt >>= getBytes)
140
88
141−-- | Errors from remote calls.
89+
142data ConnectionError = CodecError String -- show of WineryException from exception initiator which cannot otherwise be transmitted over a line due to dependencies on TypeReps
90data ConnectionError = CodecError String -- show of WineryException from exception initiator which cannot otherwise be transmitted over a line due to dependencies on TypeReps
143 | TimeoutError
91 | TimeoutError
144− | ExceptionError String
145 deriving (Generic, Show, Eq)
92 deriving (Generic, Show, Eq)
146 deriving Serialise via WineryVariant ConnectionError
93 deriving Serialise via WineryVariant ConnectionError
147
94
148−data TimeoutException = TimeoutException
149− deriving Show
150−
151−instance Exception TimeoutException
152−
153type HostAddr = (Word8, Word8, Word8, Word8)
95type HostAddr = (Word8, Word8, Word8, Word8)
154
96
155allHostAddrs,localHostAddr :: HostAddr
97allHostAddrs,localHostAddr :: HostAddr
@@ -157,186 +99,119 @@ allHostAddrs = (0,0,0,0)
157localHostAddr = (127,0,0,1)
99localHostAddr = (127,0,0,1)
158
100
159-- Each message is length-prefixed by a 32-bit unsigned length.
101-- Each message is length-prefixed by a 32-bit unsigned length.
160−envelopeP :: Parser IO Word8 Envelope
102+messageBoundaryP :: Parser IO Word8 BS.ByteString
161−envelopeP = do
103+messageBoundaryP = do
162− let msgTypeP = (P.satisfy (== 0) *>
104+ let x = FL.toList
163− (RequestMessage . fromIntegral <$> word32P)) `P.alt`
105+ w4x8 <- PD.take 4 x
164− (P.satisfy (== 1) $> ResponseMessage) `P.alt`
106+ traceShowM ("w4x8", w4x8)
165− (P.satisfy (== 2) $> TimeoutResponseMessage) `P.alt`
107+ let c = fromIntegral (fromOctets w4x8)
166− (P.satisfy (== 3) $> ExceptionResponseMessage)
108+ traceShowM ("c", c)
167− lenPrefixedByteStringP = do
109+ vals <- PD.take c x
168− c <- fromIntegral <$> word32P
110+ let bytes = BS.pack vals
169− --streamly can't handle takeEQ 0, so add special handling
111+ traceShowM ("parsedBytes", c, BS.length bytes, bytes)
170− if c == 0 then
112+ pure bytes
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
198− let s = FL.toList
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
219
113
220type NewConnectionHandler msg = IO (Maybe msg)
114type NewConnectionHandler msg = IO (Maybe msg)
221
115
222−type NewMessageHandler req resp = req -> IO resp
116+type NewMessageHandler msg resp = msg -> IO (HandlerResponse resp)
223−
117+
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.
118+serve :: (Show msg, Serialise msg, Serialise resp) =>
225−serve ::
119+ NewConnectionHandler resp ->
226− RequestHandlers s->
120+ NewMessageHandler msg resp ->
227− s ->
228 HostAddr ->
121 HostAddr ->
229 PortNumber ->
122 PortNumber ->
230 Maybe (MVar SockAddr) ->
123 Maybe (MVar SockAddr) ->
231 IO Bool
124 IO Bool
232−serve userMsgHandlers serverState hostaddr port mSockLock = do
125+serve connhandler msghandler hostaddr port mSockLock = do
233− let
234− handleSock sock = do
126+ let handleSock sock = do
235− lockingSocket <- newLock sock
127+ putStrLn "handleSock"
236− drainSocketMessages sock (serverEnvelopeHandler lockingSocket userMsgHandlers serverState)
128+ let messageHandler msg = do
237−
129+ putStrLn $ "GOT MSG " ++ show msg
130+ case msg of
131+ Response{} -> putStrLn "client sent response"
132+ ResponseExpectedRequest requestID val -> do
133+ putStrLn "server received ResponseExpectedRequest"
134+ resp <- msghandler val
135+ case resp of
136+ HandlerResponse responseVal -> sendMessage (Response requestID responseVal) sock
137+ NoResponse -> error "attempt to return non-response to expected response message"
138+ HandlerException _ -> error "TODO HandlerException"
139+ AsyncRequest _ val -> do
140+ putStrLn "AsyncReq"
141+ void $ msghandler val
142+ --no response necessaryxs
143+ ExceptionResponse{} -> putStrLn "client sent exception response"
144+ -- allow the server to send an async welcome message to the new client, if necessary
145+ mResp <- connhandler
146+ case mResp of
147+ Nothing -> pure ()
148+ Just resp -> do
149+ requestID <- UUID <$> UUIDBase.nextRandom
150+ sendMessage (AsyncRequest requestID resp) sock
151+ drainSocketMessages sock messageHandler
238− S.serially (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & S.parallely . S.mapM (handleWithM handleSock) & S.drain
152+ serially (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & serially . S.mapM (handleWithM handleSock) & S.drain
239 pure True
153 pure True
240
154
241−openEnvelope :: forall s. (Serialise s, Typeable s) => Envelope -> Maybe s
155+--add callback to allow for responses via socket
242−openEnvelope (Envelope eprint _ _ bytes) =
156+type MessageHandler a = Message a -> IO ()
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
157
250−matchEnvelope :: forall a b s. (Serialise a, Serialise b, Typeable b) =>
158+type AsyncMessageHandler a = a -> IO ()
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
159
259−-- | Called by `serve` to process incoming envelope requests. Never returns, so use `async` to spin it off on another thread.
160+drainSocketMessages :: Serialise msg => Socket -> MessageHandler msg -> IO ()
260−serverEnvelopeHandler ::
161+drainSocketMessages sock msgHandler = do
261− Locking Socket
162+ let sockStream = S.unfold readWithBufferOf (1024 * 4, sock)
262− -> RequestHandlers s
163+ handler bs = do
263− -> s
164+ case deserialise bs of
264− -> Envelope
165+ Left err ->
265− -> IO ()
166+ print err
266−serverEnvelopeHandler _ _ _ (Envelope _ TimeoutResponseMessage _ _) = pure ()
167+ Right val ->
267−serverEnvelopeHandler _ _ _ (Envelope _ ExceptionResponseMessage _ _) = pure ()
168+ msgHandler val --add response function
268−serverEnvelopeHandler _ _ _ (Envelope _ ResponseMessage _ _) = pure ()
169+ S.drain $ serially $ S.parseManyD messageBoundaryP sockStream & S.mapM handler
269−serverEnvelopeHandler sockLock msgHandlers serverState envelope@(Envelope _ (RequestMessage timeoutms) msgId _) = do
170+
270− --find first matching handler
171+fromOctets :: [Word8] -> Word32
271− let runTimeout :: IO b -> IO (Maybe b)
172+fromOctets = foldl' accum 0
272− runTimeout m =
173+ where
273− if timeoutms == 0 then
174+ accum a o = (a `shiftL` 8) .|. fromIntegral o
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
175
176+octets :: Word32 -> [Word8]
177+octets w =
178+ [ fromIntegral (w `shiftR` 24)
179+ , fromIntegral (w `shiftR` 16)
180+ , fromIntegral (w `shiftR` 8)
181+ , fromIntegral w
182+ ]
314
183
315−type EnvelopeHandler = Envelope -> IO ()
184+--send length-tagged bytestring, perhaps should be in network byte order?
185+sendMessage :: Serialise a => a -> Socket -> IO ()
186+sendMessage msg socket' = do
187+ let byteArray = toArraySlow fullbytes
188+ dbgByteString = BS.pack (SA.toList byteArray)
189+ msgbytes = serialise msg
190+ fullbytes = lenbytes <> msgbytes
191+ len = BS.length msgbytes
192+ lenbytes = BS.pack (octets (fromIntegral len))
193+ --when (dbgByteString /= msgbytes) (error "mismatch!")
194+
195+ byteCount <- Socket.send socket' fullbytes
196+ when (byteCount /= BS.length fullbytes) (error "bytes sent mismatch")
197+ traceShowM ("sent bytes:", byteCount, fullbytes)
316
198
317−drainSocketMessages :: Socket -> EnvelopeHandler -> IO ()
318−drainSocketMessages sock envelopeHandler = do
319− let sockStream = S.unfold readWithBufferOf (1024 * 4, sock)
320− S.drain $ S.serially $ S.parseMany envelopeP sockStream & S.mapM envelopeHandler
321
199
322−--send length-tagged bytestring, perhaps should be in network byte order?
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
200
333−fingerprint :: Typeable a => a -> Fingerprint
201+--from streamly-bytestring
334−fingerprint = typeRepFingerprint . typeOf
202+-- | Convert a 'ByteString' to an array of 'Word8'. This function unwraps the
203+-- 'ByteString' and wraps it with 'Array' constructors and hence the operation
204+-- is performed in constant time.
205+{-# INLINE toArrayS #-}
206+toArrayS :: BS.ByteString -> SA.Array Word8
207+--slow path
208+--toArrayS = SA.fromList . BS.unpack
209+toArrayS (BSI.PS fp off len) = SA.Array nfp endPtr
210+ where
211+ nfp = fp `plusForeignPtr` off
212+ endPtr = unsafeForeignPtrToPtr nfp `plusPtr` len `plusPtr` 1 -- ? why +1?
335
213
336−fromArray :: Arr.Array Word8 -> BSI.ByteString
214+toArraySlow :: BS.ByteString -> SA.Array Word8
337−fromArray Arr.Array {..}
215+toArraySlow bs = traceShow ("arr", SA.length arr) arr
338− | aLen == 0 = mempty
339− | otherwise = BSI.PS aStart 0 aLen
340 where
216 where
341− aStartPtr = unsafeForeignPtrToPtr aStart
217+ arr = SA.fromList (BS.unpack bs)
342− aLen = aEnd `minusPtr` aStartPtr
test/Curryer/Test/Basic.hs+53−173
@@ -1,4 +1,4 @@
1−{-# LANGUAGE DerivingVia, DeriveGeneric, TypeApplications, ExistentialQuantification #-}
1+{-# LANGUAGE DerivingVia, DeriveGeneric, TypeApplications #-}
2module Curryer.Test.Basic where
2module Curryer.Test.Basic where
3import Test.Tasty
3import Test.Tasty
4import Test.Tasty.HUnit
4import Test.Tasty.HUnit
@@ -8,221 +8,101 @@ import Control.Concurrent.MVar
8import Network.Socket (SockAddr(..))
8import Network.Socket (SockAddr(..))
9import Control.Concurrent.Async
9import Control.Concurrent.Async
10import Control.Monad
10import Control.Monad
11−import Control.Concurrent
12−import Control.Concurrent.STM
13−import Control.Exception
14−import Data.List
15
11
16import Network.RPC.Curryer.Server
12import Network.RPC.Curryer.Server
17import Network.RPC.Curryer.Client
13import Network.RPC.Curryer.Client
18
14
19−-- TODO: add test for nested calls
20−
21testTree :: TestTree
15testTree :: TestTree
22−testTree = testGroup "basic" [testCase "simple" testSimpleCall
16+testTree = testGroup "basic" [testCase "simple" testSimpleCall,
23− ,testCase "client async" testAsyncServerCall
17+ testCase "client async" testAsyncServerCall,
24− ,testCase "server async" testAsyncClientCall
18+ 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
30 ]
19 ]
31
20
32−
21+--client-to-server request
33−data AddTwoNumbersReq = AddTwoNumbersReq Int Int
22+data TestRequest = AddTwoNumbersReq Int Int
23+ | TestAsyncReq String
24+ | TestCallMeBackReq String
34 deriving (Generic, Show)
25 deriving (Generic, Show)
35− deriving Serialise via WineryVariant AddTwoNumbersReq
26+ deriving Serialise via WineryVariant TestRequest
36
27
37−data TestCallMeBackReq = TestCallMeBackReq String
28+--server responds to client
29+data TestResponse = AddTwoNumbersResp Int
30+ | AsyncHello String
31+ | TestCallMeBackResp String
38 deriving (Generic, Show, Eq)
32 deriving (Generic, Show, Eq)
39− deriving Serialise via WineryVariant TestCallMeBackReq
33+ deriving Serialise via WineryVariant TestResponse
40−
34+
41−data TestAsyncReq = TestAsyncReq String
35+testServerMessageHandler :: Maybe (MVar String) -> TestRequest -> IO (HandlerResponse TestResponse)
42− deriving (Generic, Show)
36+testServerMessageHandler mAsyncMVar msg = do
43− deriving Serialise via WineryVariant TestAsyncReq
37+ --print msg
44−
38+ case msg of
45−data DelayMicrosecondsReq = DelayMicrosecondsReq Int
39+ --round-trip request to add two Ints
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)
40+ AddTwoNumbersReq x y -> pure (HandlerResponse (AddTwoNumbersResp (x+y)))
41+ --respond to a request for an async callback
73− , RequestHandler $ \_ (TestCallMeBackReq s) ->
42+ TestCallMeBackReq s -> do
74− case mAsyncMVar of
43+ case mAsyncMVar of
75− Nothing -> pure ()
44+ Nothing -> pure ()
76− Just mvar -> putMVar mvar s
45+ Just mvar -> putMVar mvar s
77− , AsyncRequestHandler $ \_ (TestAsyncReq v) ->
46+ pure NoResponse
78− maybe (pure ()) (\mvar -> putMVar mvar v) mAsyncMVar
47+ TestAsyncReq _ -> pure NoResponse
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
85− threadDelay ms
86− pure ()
87− , RequestHandler $ \_ ThrowServerSideExceptionReq -> do
88− _ <- error "test server exception"
89− pure ()
90− ]
91
48
92-- test a simple client-to-server round-trip function execution
49-- test a simple client-to-server round-trip function execution
93testSimpleCall :: Assertion
50testSimpleCall :: Assertion
94testSimpleCall = do
51testSimpleCall = do
95 readyVar <- newEmptyMVar
52 readyVar <- newEmptyMVar
96
53
97− server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
54+ server <- async (serve (pure Nothing) (testServerMessageHandler Nothing) localHostAddr 0 (Just readyVar))
98 --wait for server to be ready
55 --wait for server to be ready
99 (SockAddrInet port _) <- takeMVar readyVar
56 (SockAddrInet port _) <- takeMVar readyVar
57+ let clientHandler :: AsyncMessageHandler TestResponse
58+ clientHandler _ = error "async handler called"
59+ mkConn :: IO (Connection TestResponse)
100− conn <- connect [] localHostAddr port
60+ mkConn = connect clientHandler localHostAddr port
61+ conn <- mkConn
62+ let c :: IO (Either ConnectionError TestResponse)
63+ c = call conn (AddTwoNumbersReq 1 1)
101 replicateM_ 5 $ do --make five AddTwo calls to shake out parallelism bugs
64 replicateM_ 5 $ do --make five AddTwo calls to shake out parallelism bugs
102− x <- call conn (AddTwoNumbersReq 1 1)
65+ x <- c
103− assertEqual "server request+response" (Right (2 :: Int)) x
66+ assertEqual "server request+response" (Right (AddTwoNumbersResp 2)) x
104 close conn
67 close conn
105 cancel server
68 cancel server
106
69
107−
108--test that the client can proces a server-initiated asynchronous callback from the server
70--test that the client can proces a server-initiated asynchronous callback from the server
109testAsyncServerCall :: Assertion
71testAsyncServerCall :: Assertion
110testAsyncServerCall = do
72testAsyncServerCall = do
111 portReadyVar <- newEmptyMVar
73 portReadyVar <- newEmptyMVar
112 receivedAsyncMessageVar <- newEmptyMVar
74 receivedAsyncMessageVar <- newEmptyMVar
113− let clientAsyncHandlers =
75+
114− [ClientAsyncRequestHandler (\(AsyncHelloReq s) ->
76+ let testServerNewConnHandler :: NewConnectionHandler TestResponse
115− putMVar receivedAsyncMessageVar s)]
77+ testServerNewConnHandler = do
78+ pure (Just (AsyncHello "welcome"))
79+
116− server <- async (serve (testServerRequestHandlers (Just receivedAsyncMessageVar)) emptyServerState localHostAddr 0 (Just portReadyVar))
80+ server <- async (serve testServerNewConnHandler (testServerMessageHandler Nothing) localHostAddr 0 (Just portReadyVar))
117 (SockAddrInet port _) <- takeMVar portReadyVar
81 (SockAddrInet port _) <- takeMVar portReadyVar
82+ let clientHandler :: AsyncMessageHandler TestResponse
83+ clientHandler msg = do
84+ case msg of
85+ AsyncHello str -> putMVar receivedAsyncMessageVar str
86+ x -> error ("unexpected message " <> show x)
118− conn <- connect clientAsyncHandlers localHostAddr port
87+ conn <- connect @TestResponse clientHandler localHostAddr port
119− Right () <- asyncCall conn (AsyncHelloReq "welcome")
120 asyncMessage <- takeMVar receivedAsyncMessageVar
88 asyncMessage <- takeMVar receivedAsyncMessageVar
121 assertEqual "async message" "welcome" asyncMessage
89 assertEqual "async message" "welcome" asyncMessage
122 close conn
90 close conn
123 cancel server
91 cancel server
124
92
125−
126−emptyServerState :: ()
127−emptyServerState = ()
128−
129--test that the client can make a non-blocking call
93--test that the client can make a non-blocking call
130testAsyncClientCall :: Assertion
94testAsyncClientCall :: Assertion
131testAsyncClientCall = do
95testAsyncClientCall = do
132 portReadyVar <- newEmptyMVar
96 portReadyVar <- newEmptyMVar
133 receivedAsyncMessageVar <- newEmptyMVar
97 receivedAsyncMessageVar <- newEmptyMVar
134
98
135− server <- async (serve (testServerRequestHandlers (Just receivedAsyncMessageVar)) emptyServerState localHostAddr 0 (Just portReadyVar))
99+ server <- async (serve (pure Nothing) (testServerMessageHandler (Just receivedAsyncMessageVar)) localHostAddr 0 (Just portReadyVar))
136 (SockAddrInet port _) <- takeMVar portReadyVar
100 (SockAddrInet port _) <- takeMVar portReadyVar
137−
101+
138− conn <- connect [] localHostAddr port
102+ conn <- connect @TestResponse (\_ -> pure ()) localHostAddr port
139 --send an async message, wait for an async response to confirm receipt
103 --send an async message, wait for an async response to confirm receipt
140 Right () <- asyncCall conn (TestCallMeBackReq "hi server")
104 Right () <- asyncCall conn (TestCallMeBackReq "hi server")
141 asyncMessage <- takeMVar receivedAsyncMessageVar
105 asyncMessage <- takeMVar receivedAsyncMessageVar
142 assertEqual "async message" "hi server" asyncMessage
106 assertEqual "async message" "hi server" asyncMessage
143 close conn
107 close conn
144 cancel server
108 cancel server
145−
146−testSyncClientCallTimeout :: Assertion
147−testSyncClientCallTimeout = do
148− readyVar <- newEmptyMVar
149−
150− server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
151− --wait for server to be ready
152− (SockAddrInet port _) <- takeMVar readyVar
153− conn <- connect [] localHostAddr port
154− x <- callTimeout @_ @Int (Just 500) conn (DelayMicrosecondsReq 1000)
155− assertEqual "client sync timeout" (Left TimeoutError) x
156− close conn
157− cancel server
158−
159−testSyncException :: Assertion
160−testSyncException = do
161− readyVar <- newEmptyMVar
162−
163− server <- async (serve (testServerRequestHandlers Nothing) emptyServerState localHostAddr 0 (Just readyVar))
164− (SockAddrInet port _) <- takeMVar readyVar
165−
166− conn <- connect [] localHostAddr port
167− ret <- call conn ThrowServerSideExceptionReq
168− case ret of
169− Left (ExceptionError actualExc) ->
170− assertBool "server-side exception" ("test server exception" `isPrefixOf` actualExc)
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
226− close conn
227− cancel server
228−