on master help sign in
implement asynchronous server-to-client notifications

hlint passes

commitf1fd065a2bc54714de69ef531071d229b21e12eb authorAgentM <agentm@themactionfaction.com> dateSun 27 Sep 2020, 20:02 (6 years ago) parent6c4a479
6 files changed+346−51
curryer.cabal+24−1 src/Network/RPC/Curryer/Client.hs+51−15 src/Network/RPC/Curryer/Server.hs+112−35 src/Network/RPC/Curryer/StreamlyAdditions.hs+66 test/Curryer/Test/Basic.hs+84 test/Driver.hs+9
curryer.cabal+24−1
@@ -25,8 +25,31 @@ Library
2525 , network
2626 , exceptions
2727 , async
28+ , uuid
29+ , fast-builder
30+ , binary
31+ , containers
32+ , stm-containers
33+ , hashable
2834 Hs-Source-Dirs: ./src
35+ Default-Language: Haskell2010
36+ ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports
2937 Exposed-Modules:
3038 Network.RPC.Curryer.Server
3139 Network.RPC.Curryer.Client
32−
40+ Network.RPC.Curryer.StreamlyAdditions
41+
42+Test-Suite test
43+ type: exitcode-stdio-1.0
44+ main-is: Driver.hs
45+ hs-source-dirs: test
46+ default-language: Haskell2010
47+ ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports
48+ build-depends: tasty
49+ , tasty-hunit
50+ , base
51+ , curryer
52+ , winery
53+ , network
54+ , async
55+ other-modules: Curryer.Test.Basic
src/Network/RPC/Curryer/Client.hs+51−15
@@ -1,29 +1,65 @@
11module Network.RPC.Curryer.Client where
2−import Streamly.Prelude
3−import Network.RPC.Vintner.Server
4−import Streamly.Network.Socket
2+import Network.RPC.Curryer.Server
53import Network.Socket as Socket
64import qualified Streamly.Network.Inet.TCP as TCP
75import Codec.Winery
86import Control.Concurrent.Async
7+import qualified Data.UUID.V4 as UUIDBase
8+import qualified StmContainers.Map as STMMap
9+import Control.Concurrent.MVar
10+import GHC.Conc
911
10−-- | client-side functions
11−data Connection = Connection Socket (Async ())
12+type SyncMap a = STMMap.Map UUID (MVar a)
13+-- the request map holds
14+data Connection a = Connection Socket (Async ()) (SyncMap a)
1215
13−connect :: Serialise a => MessageHandler a -> HostAddr -> PortNumber -> IO Connection
16+connect :: (Serialise msg) =>
17+ AsyncMessageHandler msg ->
18+ HostAddr ->
19+ PortNumber ->
20+ IO (Connection msg)
1421connect notificationCallback hostAddr portNum = do
1522 sock <- TCP.connect hostAddr portNum
16− asyncThread <- async (clientAsync sock notificationCallback)
17− pure (Connection sock asyncThread)
23+ syncmap <- STMMap.newIO
24+ asyncThread <- async (clientAsync sock syncmap notificationCallback)
25+ pure (Connection sock asyncThread syncmap)
1826
19−close :: Connection -> IO ()
20−close (Connection sock asyncThread) = do
27+close :: Connection a -> IO ()
28+close (Connection sock asyncThread _) = do
2129 Socket.close sock
2230 cancel asyncThread
2331
24−-- async thread for handling incoming messages
25−clientAsync :: Serialise a => Socket -> MessageHandler a -> IO ()
26−clientAsync sock msgHandler = do
27− drainSocketMessages sock msgHandler
28−
32+-- async thread for handling client-side incoming messages- dispatch to proper waiting thread or handler asynchronous notifications
33+clientAsync :: Serialise b =>
34+ Socket ->
35+ SyncMap b ->
36+ AsyncMessageHandler b ->
37+ IO ()
38+clientAsync sock syncmap asyncHandler = do
39+ -- ping proper thread to continue
40+ let responseHandler responseMsg = do
41+ putStrLn "clientAsync handler"
42+ case responseMsg of
43+ Response requestId val -> do
44+ mMVar <- atomically $ STMMap.lookup requestId syncmap
45+ case mMVar of
46+ Nothing -> error "dumped unrequested response"
47+ Just mVar -> putMVar mVar val
48+ AsyncRequest asyncMsg ->
49+ asyncHandler asyncMsg
50+ ResponseExpectedRequest _ _ -> error "dumped response expected request"
51+ ExceptionResponse _ -> error "TODO Exception"
52+ drainSocketMessages sock responseHandler
2953
54+-- | Send a request to the remote server and returns a response.
55+call :: (Serialise request, Serialise response) => Connection response -> request -> IO (Either ConnectionError response)
56+call (Connection sock _ mVarMap) msg = do
57+ requestID <- UUID <$> UUIDBase.nextRandom
58+ -- setup mvar to wait for response
59+ responseMVar <- newEmptyMVar
60+ atomically $ STMMap.insert responseMVar requestID mVarMap
61+ sendMessage (ResponseExpectedRequest requestID msg) sock
62+ response <- takeMVar responseMVar
63+ atomically $ STMMap.delete requestID mVarMap
64+ pure (Right response)
65+
src/Network/RPC/Curryer/Server.hs+112−35
@@ -1,32 +1,41 @@
1−{-# LANGUAGE StandaloneDeriving, DerivingVia, DeriveGeneric, RankNTypes, TypeApplications, ScopedTypeVariables, MultiParamTypeClasses #-}
1+{-# LANGUAGE DerivingVia, DeriveGeneric, RankNTypes, ScopedTypeVariables, MultiParamTypeClasses, OverloadedStrings, GeneralizedNewtypeDeriving #-}
2+{-# HLINT ignore "Use lambda-case" #-}
23module Network.RPC.Curryer.Server where
34import Streamly
45import qualified Streamly.Prelude as S
5−import qualified Streamly.Network.Inet.TCP as TCP
66import Streamly.Network.Socket
77import Streamly.Internal.Network.Socket (handleWithM, writeChunk)
88import Network.Socket as Socket
99import Streamly.Internal.Data.Parser.ParserD as PD
1010import Codec.Winery
11+import Codec.Winery.Internal (varInt, decodeVarInt, getBytes)
12+import Codec.Winery.Class (mkExtractor)
1113import GHC.Generics
12−import Control.Concurrent.MVar (putMVar, MVar)
14+import Control.Concurrent.MVar (MVar)
1315import Control.Exception
1416import Data.Function ((&))
1517import Data.Word
16−import Control.Monad.Catch
1718import qualified Data.ByteString as BS
19+import qualified Data.ByteString.Lazy as BSL
20+import qualified Data.ByteString.FastBuilder as BB
1821import Streamly.Data.Fold as FL
1922import qualified Streamly.Internal.Data.Stream.IsStream as S
2023import Data.Foldable
21−import Data.Word
2224import Data.Bits
25+import qualified Data.Binary as B
26+import qualified Data.UUID as UUIDBase
2327import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as SA
28+import qualified Network.RPC.Curryer.StreamlyAdditions as SA
29+import Control.Monad
30+import Data.Hashable
2431
2532-- for toArrayS conversion
2633import qualified Data.ByteString.Internal as BSI
2734import Foreign.ForeignPtr (plusForeignPtr)
2835import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
29−import GHC.Ptr (minusPtr, plusPtr)
36+import GHC.Ptr (plusPtr)
37+
38+import Debug.Trace
3039
3140data ClientHelloMessage = ClientHelloMessage Int Int
3241 deriving Generic
@@ -42,59 +51,119 @@ data ServerHelloMessage = ServerHelloMessage { serverServices :: [String],
4251--client-side timeout
4352--server-to-client and client-to-server async request (no response requested)
4453
45−data Response a = Response a
54+data Message a = Response UUID a
55+ | AsyncRequest a
56+ | ResponseExpectedRequest UUID a
57+ -- | Services
4658 | ExceptionResponse String
47− deriving Generic
48− deriving Serialise via WineryVariant (Response a)
49−
50−data Request a = Request a
51− | Services
52− deriving Generic
53− deriving Serialise via WineryVariant (Request a)
54−
55−class Event a where
56− dispatchAsyncEvent :: a -> IO ()
57− dispatchEvent :: a -> IO (Response b)
59+ deriving (Generic, Show)
60+ deriving Serialise via WineryVariant (Message a)
61+
62+-- | The response type
63+data HandlerResponse a = NoResponse
64+ | HandlerResponse a
65+ | HandlerException a
66+ deriving Generic
67+ deriving Serialise via WineryVariant (HandlerResponse a)
68+
69+--avoid orphan instance
70+newtype UUID = UUID { _unUUID :: UUIDBase.UUID }
71+ deriving (Show, Eq, B.Binary, Hashable)
72+
73+instance Serialise UUID where
74+ schemaGen _ = pure (STag (TagStr "Data.UUID") SBytes)
75+ toBuilder uuid = let bytes = BSL.toStrict (B.encode uuid) in
76+ varInt (BS.length bytes) <> BB.byteString bytes
77+ {-# INLINE toBuilder #-}
78+ extractor = mkExtractor $
79+ \schema' -> case schema' of
80+ STag (TagStr "Data.UUID") SBytes ->
81+ pure $ \term -> case term of
82+ TBytes bs -> B.decode (BSL.fromStrict bs)
83+ term' -> throw (InvalidTerm term')
84+ x -> error $ "invalid schema element " <> show x
85+ decodeCurrent = B.decode . BSL.fromStrict <$> (decodeVarInt >>= getBytes)
86+
87+
88+data ConnectionError = CodecError String -- show of WineryException from exception initiator which cannot otherwise be transmitted over a line due to dependencies on TypeReps
89+ | TimeoutError
90+ deriving (Generic, Show, Eq)
91+ deriving Serialise via WineryVariant ConnectionError
5892
5993type HostAddr = (Word8, Word8, Word8, Word8)
6094
95+allHostAddrs,localHostAddr :: HostAddr
6196allHostAddrs = (0,0,0,0)
6297localHostAddr = (127,0,0,1)
6398
6499-- Each message is length-prefixed by a 32-bit unsigned length.
65100messageBoundaryP :: Parser IO Word8 BS.ByteString
66101messageBoundaryP = do
102+ --traceShowM "message!"
67103 w4x8 <- PD.take 4 FL.toList
104+ --traceShowM ("w4x8",w4x8)
68105 let c = fromIntegral (fromOctets w4x8)
106+ --traceShowM ("c", c)
69107 vals <- PD.take c FL.toList
70− pure (BS.pack vals)
108+ let bytes = BS.pack vals
109+ traceShowM ("vals", BS.length bytes, bytes)
110+ pure bytes
71111
72−serve :: Serialise msg =>
73− (msg -> Socket -> IO ()) ->
112+type NewConnectionHandler msg = IO (Maybe msg)
113+
114+type NewMessageHandler msg resp = msg -> IO (HandlerResponse resp)
115+
116+serve :: (Show msg, Serialise msg, Serialise resp) =>
117+ NewConnectionHandler resp ->
118+ NewMessageHandler msg resp ->
74119 HostAddr ->
75120 PortNumber ->
76− Maybe (MVar AddrInfo) ->
121+ Maybe (MVar SockAddr) ->
77122 IO Bool
78−serve handler hostaddr port mAddressMVar = do
123+serve connhandler msghandler hostaddr port mSockLock = do
79124 let handleSock sock = do
80− let sockStream :: SerialT IO Word8
81− sockStream = S.unfold readWithBufferOf (1024 * 4, sock)
82− messageHandler bs = case deserialise bs of
83− Left err -> error (show err) --FIXME
84− Right val -> handler val sock
125+ putStrLn "handleSock"
126+ let messageHandler msg = do
127+ putStrLn $ "GOT MSG " ++ show msg
128+ case msg of
129+ Response{} -> putStrLn "client sent response"
130+ ResponseExpectedRequest requestID val -> do
131+ putStrLn "ResponseExp"
132+ resp <- msghandler val
133+ case resp of
134+ HandlerResponse responseVal -> sendMessage (Response requestID responseVal) sock
135+ NoResponse -> error "attempt to return non-response to expected response message"
136+ HandlerException _ -> error "TODO HandlerException"
137+ AsyncRequest val -> do
138+ putStrLn "AsyncReq"
139+ void $ msghandler val
140+ --no response necessaryxs
141+ ExceptionResponse{} -> putStrLn "client sent exception response"
142+ -- allow the server to send an async welcome message to the new client, if necessary
143+ mResp <- connhandler
144+ case mResp of
145+ Nothing -> pure ()
146+ Just resp -> sendMessage (AsyncRequest resp) sock
85147 drainSocketMessages sock messageHandler
86− serially (S.unfold TCP.acceptOnAddr (hostaddr, port)) & parallely . S.mapM (handleWithM handleSock) & S.drain
148+ serially (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & parallely . S.mapM (handleWithM handleSock) & S.drain
87149 pure True
88150
89−type MessageHandler a = a -> IO ()
151+--add callback to allow for responses via socket
152+type MessageHandler a = Message a -> IO ()
153+
154+type AsyncMessageHandler a = a -> IO ()
90155
91156drainSocketMessages :: Serialise msg => Socket -> MessageHandler msg -> IO ()
92157drainSocketMessages sock msgHandler = do
93158 let sockStream = S.unfold readWithBufferOf (1024 * 4, sock)
94− handler bs =
159+ handler bs = do
160+ --print ("drain handler", bs)
95161 case deserialise bs of
96− Left err -> error (show err) --FIXME
97− Right val -> msgHandler val --add response function
162+ Left err ->
163+ print err
164+ Right val -> do
165+ --putStrLn "deserialized value for msgHandler"
166+ msgHandler val --add response function
98167 S.drain $ S.parseManyD messageBoundaryP sockStream & S.mapM handler
99168
100169fromOctets :: [Word8] -> Word32
@@ -110,8 +179,16 @@ octets w =
110179 , fromIntegral w
111180 ]
112181
182+--send length-tagged bytestring, perhaps should be in network byte order?
113183sendMessage :: Serialise a => a -> Socket -> IO ()
114−sendMessage msg socket = writeChunk socket (toArrayS (serialise msg))
184+sendMessage msg socket' = do
185+ putStrLn ("sending bytes: " ++ show (blen <> bytes))
186+ writeChunk socket' (toArrayS (blen <> bytes))
187+ where
188+ bytes = serialise msg
189+ len = BS.length bytes
190+ blen = BS.pack (octets (fromIntegral len))
191+
115192
116193--from streamly-bytestring
117194-- | Convert a 'ByteString' to an array of 'Word8'. This function unwraps the
@@ -124,5 +201,5 @@ toArrayS :: BS.ByteString -> SA.Array Word8
124201toArrayS (BSI.PS fp off len) = SA.Array nfp endPtr
125202 where
126203 nfp = fp `plusForeignPtr` off
127− endPtr = unsafeForeignPtrToPtr nfp `plusPtr` len
204+ endPtr = unsafeForeignPtrToPtr nfp `plusPtr` len `plusPtr` 1 -- ? why +1?
128205
src/Network/RPC/Curryer/StreamlyAdditions.hsnew file+66
@@ -0,0 +1,66 @@
1+module Network.RPC.Curryer.StreamlyAdditions where
2+import Control.Monad.IO.Class
3+import Network.Socket (Socket, PortNumber, SocketOption, SockAddr(..), maxListenQueue, Family(..), SocketType(..), defaultProtocol, tupleToHostAddress, withSocketsDo, socket, setSocketOption, bind, getSocketName)
4+import qualified Network.Socket as Net
5+import Control.Exception (onException)
6+import Control.Concurrent.MVar
7+import Data.Word
8+import qualified Streamly.Internal.Data.Unfold as UF
9+import Streamly.Network.Socket hiding (accept)
10+import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
11+import Streamly.Internal.Data.Unfold.Types (Unfold(..))
12+
13+acceptOnAddrWith
14+ :: MonadIO m
15+ => [(SocketOption, Int)]
16+ -> Maybe (MVar SockAddr)
17+ -> Unfold m ((Word8, Word8, Word8, Word8), PortNumber) Socket
18+acceptOnAddrWith opts mSockLock = UF.lmap f (accept mSockLock)
19+ where
20+ f (addr, port) =
21+ (maxListenQueue
22+ , SockSpec
23+ { sockFamily = AF_INET
24+ , sockType = Stream
25+ , sockProto = defaultProtocol -- TCP
26+ , sockOpts = opts
27+ }
28+ , SockAddrInet port (tupleToHostAddress addr)
29+ )
30+
31+accept :: MonadIO m => Maybe (MVar SockAddr) -> Unfold m (Int, SockSpec, SockAddr) Socket
32+accept mSockLock = UF.map fst (listenTuples mSockLock)
33+
34+initListener :: Int -> SockSpec -> SockAddr -> IO Socket
35+initListener listenQLen sockSpec addr =
36+ withSocketsDo $ do
37+ sock <- socket (sockFamily sockSpec) (sockType sockSpec) (sockProto sockSpec)
38+ use sock `onException` Net.close sock
39+ return sock
40+
41+ where
42+
43+ use sock = do
44+ mapM_ (uncurry (setSocketOption sock)) (sockOpts sockSpec)
45+ bind sock addr
46+ Net.listen sock listenQLen
47+
48+listenTuples :: MonadIO m
49+ => Maybe (MVar SockAddr)
50+ -> Unfold m (Int, SockSpec, SockAddr) (Socket, SockAddr)
51+listenTuples mSockLock = Unfold step inject
52+ where
53+ inject (listenQLen, spec, addr) =
54+ liftIO $ do
55+ sock <- initListener listenQLen spec addr
56+ sockAddr <- getSocketName sock
57+ case mSockLock of
58+ Just mvar ->
59+ putMVar mvar sockAddr
60+ Nothing -> pure ()
61+ pure sock
62+
63+ step listener = do
64+ r <- liftIO (Net.accept listener `onException` Net.close listener)
65+ return $ D.Yield r listener
66+
test/Curryer/Test/Basic.hsnew file+84
@@ -0,0 +1,84 @@
1+{-# LANGUAGE DerivingVia, DeriveGeneric, TypeApplications #-}
2+module Curryer.Test.Basic where
3+import Test.Tasty
4+import Test.Tasty.HUnit
5+import Codec.Winery
6+import GHC.Generics
7+import Control.Concurrent.MVar
8+import Network.Socket (SockAddr(..))
9+import Control.Concurrent.Async
10+
11+import Network.RPC.Curryer.Server
12+import Network.RPC.Curryer.Client
13+
14+testTree :: TestTree
15+testTree = testGroup "basic" [--testCase "simple" testSimpleCall,
16+ testCase "client async" testAsyncServerCall
17+ ]
18+
19+--client-to-server request
20+data TestRequest = AddTwoNumbersReq Int Int
21+ deriving (Generic, Show)
22+ deriving Serialise via WineryVariant TestRequest
23+
24+--server responds to client
25+data TestResponse = AddTwoNumbersResp Int
26+ | AsyncHello String
27+ deriving (Generic, Show, Eq)
28+ deriving Serialise via WineryVariant TestResponse
29+
30+testServerMessageHandler :: TestRequest -> IO (HandlerResponse TestResponse)
31+testServerMessageHandler msg = do
32+ --print msg
33+ case msg of
34+ AddTwoNumbersReq x y -> pure (HandlerResponse (AddTwoNumbersResp (x+y)))
35+
36+-- test a simple client-to-server round-trip function execution
37+testSimpleCall :: Assertion
38+testSimpleCall = do
39+ readyVar <- newEmptyMVar
40+
41+ server <- async (serve (pure Nothing) testServerMessageHandler localHostAddr 0 (Just readyVar))
42+ --wait for server to be ready
43+-- putStrLn "waiting for server"
44+ sock@(SockAddrInet port _) <- takeMVar readyVar
45+-- putStrLn $ "server ready: " ++ show sock
46+ let clientHandler :: AsyncMessageHandler TestResponse
47+ clientHandler _ = error "async handler called"
48+ mkConn :: IO (Connection TestResponse)
49+ mkConn = connect clientHandler localHostAddr port
50+ conn <- mkConn
51+-- putStrLn "connect complete"
52+ let c :: IO (Either ConnectionError TestResponse)
53+ c = call conn (AddTwoNumbersReq 1 1)
54+ x <- c
55+-- putStrLn "call complete"
56+ assertEqual "server request+response" (Right (AddTwoNumbersResp 2)) x
57+-- print x
58+ close conn
59+
60+--test that the client can proces a server-initiated asynchronous callback from the server
61+testAsyncServerCall :: Assertion
62+testAsyncServerCall = do
63+ portReadyVar <- newEmptyMVar
64+ receivedAsyncMessageVar <- newEmptyMVar
65+
66+ let testServerNewConnHandler :: NewConnectionHandler TestResponse
67+ testServerNewConnHandler = do
68+ pure (Just (AsyncHello "welcome"))
69+
70+ server <- async (serve testServerNewConnHandler testServerMessageHandler localHostAddr 0 (Just portReadyVar))
71+ (SockAddrInet port _) <- takeMVar portReadyVar
72+ let clientHandler :: AsyncMessageHandler TestResponse
73+ clientHandler msg = do
74+ case msg of
75+ AsyncHello str -> putMVar receivedAsyncMessageVar str
76+ x -> error ("unexpected message " <> show x)
77+ conn <- connect @TestResponse clientHandler localHostAddr port
78+ asyncMessage <- takeMVar receivedAsyncMessageVar
79+ assertEqual "async message" "welcome" asyncMessage
80+ close conn
81+
82+--test that the client can make a non-blocking call
83+--testAsyncClientCall :: Assertion
84+
test/Driver.hsnew file+9
@@ -0,0 +1,9 @@
1+import Test.Tasty
2+import Curryer.Test.Basic as Basic
3+
4+main :: IO ()
5+main = defaultMain curryerTestTree
6+
7+curryerTestTree :: TestTree
8+curryerTestTree = testGroup "curryer" [Basic.testTree]
9+