curryer-rpc 0.1 → 0.2
raw patch · 6 files changed
+100/−58 lines, 6 filesdep ~streamly
Dependency ranges changed: streamly
Files
- Changelog.markdown +4/−0
- cabal.project +0/−6
- curryer-rpc.cabal +4/−4
- src/Network/RPC/Curryer/Client.hs +6/−7
- src/Network/RPC/Curryer/Server.hs +82/−40
- src/Network/RPC/Curryer/StreamlyAdditions.hs +4/−1
Changelog.markdown view
@@ -1,3 +1,7 @@+# v0.2 (2021-12-13)++* update for streamly 0.8.0 (we are no longer pegged to a pre-release streamly)+ # v0.1 (2020-12-27) * initial release to support [Project:M36](https://github.com/agentm/project-m36)
− cabal.project
@@ -1,6 +0,0 @@-packages: .--source-repository-package- type: git- location: https://github.com/composewell/streamly.git- tag: ac3af8749194f1788704dda8667d0b3807075cc2
curryer-rpc.cabal view
@@ -1,5 +1,5 @@ Name: curryer-rpc-Version: 0.1+Version: 0.2 License: PublicDomain Build-Type: Simple Homepage: https://github.com/agentm/curryer@@ -11,7 +11,7 @@ Cabal-Version: >= 1.10 Synopsis: Fast, Haskell RPC Description: Haskell-to-Haskell RPC using Winery serialization.-Extra-Source-Files: Changelog.markdown README.markdown cabal.project+Extra-Source-Files: Changelog.markdown README.markdown Source-Repository head Type: git@@ -21,7 +21,7 @@ Build-Depends: base >= 4.12 && < 4.15 , winery , bytestring- , streamly >= 0.7.2+ , streamly >= 0.8.0 , network , exceptions , async@@ -36,7 +36,7 @@ , stm Hs-Source-Dirs: ./src Default-Language: Haskell2010- ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports + ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports Exposed-Modules: Network.RPC.Curryer.Server Network.RPC.Curryer.Client
src/Network/RPC/Curryer/Client.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RankNTypes, ScopedTypeVariables, GADTs #-}+{-# LANGUAGE RankNTypes, ScopedTypeVariables, GADTs, TypeApplications #-} module Network.RPC.Curryer.Client where import Network.RPC.Curryer.Server import Network.Socket as Socket@@ -93,7 +93,7 @@ clientEnvelopeHandler _ _ syncMap (Envelope _ TimeoutResponseMessage msgId _) = consumeResponse msgId syncMap (Left TimeoutError) clientEnvelopeHandler _ _ syncMap (Envelope _ ExceptionResponseMessage msgId excPayload) = - case deserialise excPayload of+ case msgDeserialise excPayload of Left err -> error ("failed to deserialise exception string" <> show err) Right excStr -> consumeResponse msgId syncMap (Left (ExceptionError excStr))@@ -112,7 +112,7 @@ Just tm | tm < 0 -> 0 Just tm -> fromIntegral tm - envelope = Envelope fprint (RequestMessage timeoutms) requestID (serialise msg)+ envelope = Envelope fprint (RequestMessage timeoutms) requestID (msgSerialise msg) fprint = fingerprint msg -- setup mvar to wait for response responseMVar <- newEmptyMVar@@ -132,16 +132,15 @@ Just (Left exc) -> pure (Left exc) Just (Right binmsg) ->- --TODO use decoder instead- case deserialise binmsg of+ case msgDeserialise binmsg of Left err -> error ("deserialise client error " <> show err)- Right m -> pure (Right m)+ Right v -> pure (Right v) -- | Call a remote function but do not expect a response from the server. asyncCall :: Serialise request => Connection -> request -> IO (Either ConnectionError ()) asyncCall conn msg = do requestID <- UUID <$> UUIDBase.nextRandom- let envelope = Envelope fprint (RequestMessage 0) requestID (serialise msg)+ let envelope = Envelope fprint (RequestMessage 0) requestID (msgSerialise msg) fprint = fingerprint msg sendEnvelope envelope (_conn_sockLock conn) pure (Right ())
src/Network/RPC/Curryer/Server.hs view
@@ -3,14 +3,12 @@ {- HLINT ignore "Use lambda-case" -} module Network.RPC.Curryer.Server where import qualified Streamly.Prelude as S-import Streamly.Network.Socket-import Streamly.Internal.Network.Socket (handleWithM)+import Streamly.Network.Socket as SSock import Network.Socket as Socket import Network.Socket.ByteString as Socket import Streamly.Internal.Data.Parser as P hiding (concatMap) import Codec.Winery import Codec.Winery.Internal (varInt, decodeVarInt, getBytes)-import Codec.Winery.Class (mkExtractor) import GHC.Generics import GHC.Fingerprint import Data.Typeable@@ -21,28 +19,30 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.FastBuilder as BB-import Streamly.Data.Fold as FL-import qualified Streamly.Internal.Data.Stream.IsStream as S+import Streamly.Data.Fold as FL hiding (foldr)+import qualified Streamly.Internal.Data.Stream.IsStream as P import qualified Data.Binary as B import qualified Data.UUID as UUIDBase import qualified Data.UUID.V4 as UUIDBase import Control.Monad import Data.Functor+import Control.Applicative import qualified Network.RPC.Curryer.StreamlyAdditions as SA---import Control.Monad import Data.Hashable import System.Timeout import qualified Network.ByteOrder as BO + -- for toArrayS conversion-{-import qualified Data.ByteString.Internal as BSI-import qualified Streamly.Internal.Data.Array.Storable.Foreign.Types as SA-import Foreign.ForeignPtr (plusForeignPtr)-import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)-import GHC.Ptr (plusPtr)--}--- define CURRYER_SHOW_BYTES 1+import qualified Data.ByteString.Internal as BSI+import qualified Streamly.Internal.Data.Array.Foreign.Type as Arr+import qualified Streamly.Internal.Data.Array.Foreign.Mut.Type as ArrT+import GHC.ForeignPtr (ForeignPtr(ForeignPtr))+import GHC.Ptr (minusPtr, Ptr(..))++--define CURRYER_SHOW_BYTES 1+ #if CURRYER_SHOW_BYTES == 1 import Debug.Trace #endif@@ -54,6 +54,13 @@ traceBytes _ _ = pure () #endif +-- a level of indirection to be able to switch between serialising with and without the winery schema+msgSerialise :: Serialise a => a -> BS.ByteString+msgSerialise = serialiseOnly++msgDeserialise :: forall s. Serialise s => BS.ByteString -> Either WineryException s+msgDeserialise = deserialiseOnly+ data Locking a = Locking (MVar ()) a newLock :: a -> IO (Locking a)@@ -116,7 +123,7 @@ sendMessage lockSock msg = do requestID <- UUID <$> UUIDBase.nextRandom let env =- Envelope (fingerprint msg) (RequestMessage timeout') requestID (serialise msg)+ Envelope (fingerprint msg) (RequestMessage timeout') requestID (msgSerialise msg) timeout' = 0 sendEnvelope env lockSock @@ -156,28 +163,38 @@ allHostAddrs = (0,0,0,0) localHostAddr = (127,0,0,1) +msgTypeP :: Parser IO Word8 MessageType+msgTypeP = (P.satisfy (== 0) *>+ (RequestMessage . fromIntegral <$> word32P)) <|>+ (P.satisfy (== 1) $> ResponseMessage) <|>+ (P.satisfy (== 2) $> TimeoutResponseMessage) <|>+ (P.satisfy (== 3) $> ExceptionResponseMessage)+ -- Each message is length-prefixed by a 32-bit unsigned length. envelopeP :: Parser IO Word8 Envelope envelopeP = do- let s = FL.toList- msgTypeP = (P.satisfy (== 0) *>- (RequestMessage . fromIntegral <$> word32P)) `P.alt`- (P.satisfy (== 1) $> ResponseMessage) `P.alt`- (P.satisfy (== 2) $> TimeoutResponseMessage) `P.alt`- (P.satisfy (== 3) $> ExceptionResponseMessage)- lenPrefixedByteStringP = do+ let lenPrefixedByteStringP = do c <- fromIntegral <$> word32P --streamly can't handle takeEQ 0, so add special handling+-- traceShowM ("envelopeP payload byteCount"::String, c) if c == 0 then pure BS.empty else- BS.pack <$> P.takeEQ c s+ fromArray <$> P.takeEQ c (Arr.writeN c) Envelope <$> fingerprintP <*> msgTypeP <*> uuidP <*> lenPrefixedByteStringP +--overhead is fingerprint (16 bytes), msgType (1+4 optional bytes for request message), msgId (4 bytes), uuid (16 bytes) = 41 bytes per request message, 37 bytes for all others encodeEnvelope :: Envelope -> BS.ByteString encodeEnvelope (Envelope (Fingerprint fp1 fp2) msgType msgId bs) =- fingerprintBs <> msgTypeBs <> msgIdBs <> lenPrefixedBs+{- traceShow ("encodeEnvelope"::String,+ ("fingerprint len"::String, BS.length fingerprintBs),+ ("msgtype length"::String,BS.length msgTypeBs),+ ("id len"::String, BS.length msgIdBs),+ ("payload len"::String, payloadLen),+ ("complete len"::String, BS.length completeMessage)) $-}+ completeMessage where+ completeMessage = fingerprintBs <> msgTypeBs <> msgIdBs <> lenPrefixedBs fingerprintBs = BO.bytestring64 fp1 <> BO.bytestring64 fp2 msgTypeBs = case msgType of RequestMessage timeoutms -> BS.singleton 0 <> BO.bytestring32 (fromIntegral timeoutms)@@ -187,8 +204,10 @@ msgIdBs = case UUIDBase.toWords (_unUUID msgId) of (u1, u2, u3, u4) -> foldr ((<>) . BO.bytestring32) BS.empty [u1, u2, u3, u4]- msgLen = fromIntegral (BS.length bs)- lenPrefixedBs = BO.bytestring32 msgLen <> bs+ lenPrefixedBs = BO.bytestring32 payloadLen <> bs+ payloadLen = fromIntegral (BS.length bs)+ + fingerprintP :: Parser IO Word8 Fingerprint fingerprintP =@@ -204,7 +223,8 @@ word32P :: Parser IO Word8 Word32 word32P = do let s = FL.toList- w4x8 <- P.takeEQ 4 s + w4x8 <- P.takeEQ 4 s+-- traceShowM ("w4x8"::String, BO.word32 (BS.pack w4x8)) pure (BO.word32 (BS.pack w4x8)) -- uuid is encode as 4 32-bit words because of its convenient 32-bit tuple encoding@@ -235,19 +255,26 @@ handleSock sock = do lockingSocket <- newLock sock drainSocketMessages sock (serverEnvelopeHandler lockingSocket userMsgHandlers serverState)- - S.serially (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & S.parallely . S.mapM (handleWithM handleSock) & S.drain++ S.fromSerial (S.unfold (SA.acceptOnAddrWith [(ReuseAddr,1)] mSockLock) (hostaddr, port)) & S.fromParallel . S.mapM (forSocketM handleSock) & S.drain pure True openEnvelope :: forall s. (Serialise s, Typeable s) => Envelope -> Maybe s openEnvelope (Envelope eprint _ _ bytes) = if eprint == fingerprint (undefined :: s) then- case deserialise bytes of- Left err -> error (show err)- Right v -> Just v+ case msgDeserialise bytes of+ Left _e -> {-traceShow ("openEnv error"::String, e) $-} Nothing+ Right decoded -> Just decoded else Nothing +--use winery to decode only the data structure and skip the schema+deserialiseOnly :: forall s. Serialise s => BS.ByteString -> Either WineryException s+deserialiseOnly bytes = do+ dec <- getDecoder (schema (Proxy :: Proxy s))+ pure (evalDecoder dec bytes)++ matchEnvelope :: forall a b s. (Serialise a, Serialise b, Typeable b) => Envelope -> (ConnectionState s -> a -> IO b) ->@@ -293,7 +320,7 @@ let envelopeResponse = case mResponse of Just response ->- Envelope (fingerprint response) ResponseMessage msgId (serialise response)+ Envelope (fingerprint response) ResponseMessage msgId (msgSerialise response) Nothing -> Envelope (fingerprint TimeoutError) TimeoutResponseMessage msgId BS.empty sendEnvelope envelopeResponse sockLock@@ -308,7 +335,7 @@ eExc <- try $ foldM_ (flip firstMatcher) Nothing msgHandlers :: IO (Either SomeException ()) case eExc of Left exc ->- let env = Envelope (fingerprint (show exc)) ExceptionResponseMessage msgId (serialise (show exc)) in+ let env = Envelope (fingerprint (show exc)) ExceptionResponseMessage msgId (msgSerialise (show exc)) in sendEnvelope env sockLock Right () -> pure () @@ -317,20 +344,35 @@ drainSocketMessages :: Socket -> EnvelopeHandler -> IO () drainSocketMessages sock envelopeHandler = do- let sockStream = S.unfold readWithBufferOf (1024 * 4, sock)- S.drain $ S.serially $ S.parseMany envelopeP sockStream & S.mapM envelopeHandler+ S.unfold SSock.read sock+ & P.parseMany envelopeP+ & S.mapM envelopeHandler+ & S.fromAsync+ & S.drain --send length-tagged bytestring, perhaps should be in network byte order? sendEnvelope :: Envelope -> Locking Socket -> IO () sendEnvelope envelope sockLock = do let envelopebytes = encodeEnvelope envelope- fullbytes = envelopebytes --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- withLock sockLock $ \socket' ->- Socket.sendAll socket' fullbytes- --traceShowM ("sendEnvelope", envelope)- traceBytes "sendEnvelope" fullbytes+ withLock sockLock $ \socket' -> do+ {-traceShowM ("sendEnvelope"::String,+ ("type"::String, envMessageType envelope),+ socket', ("env len"::String, BS.length envelopebytes),+ "payloadbytes"::String, envPayload envelope)-}+ Socket.sendAll socket' envelopebytes+-- traceBytes "sendEnvelope" envelopebytes fingerprint :: Typeable a => a -> Fingerprint fingerprint = typeRepFingerprint . typeOf +fromArray :: Arr.Array Word8 -> BSI.ByteString+fromArray arr + | aLen == 0 = mempty+ | otherwise = {-traceShow ("bsi len"::String, aLen, Arr.byteLength arr) $-} BSI.PS aStartFPtr 0 aLen+ where+ aStart = Arr.arrStart arr+ aEnd = Arr.aEnd arr+ aStartFPtr = case Arr.arrStart arr of+ Ptr addr -> ForeignPtr addr (ArrT.arrayToFptrContents (Arr.arrContents arr))+ aLen = aEnd `minusPtr` aStart
src/Network/RPC/Curryer/StreamlyAdditions.hs view
@@ -3,12 +3,13 @@ import Network.Socket (Socket, PortNumber, SocketOption, SockAddr(..), maxListenQueue, Family(..), SocketType(..), defaultProtocol, tupleToHostAddress, withSocketsDo, socket, setSocketOption, bind, getSocketName) import qualified Network.Socket as Net import Control.Exception (onException)+import Control.Monad.Catch (finally) import Control.Concurrent.MVar import Data.Word import qualified Streamly.Internal.Data.Unfold as UF import Streamly.Network.Socket hiding (accept) import qualified Streamly.Internal.Data.Stream.StreamD.Type as D-import Streamly.Internal.Data.Unfold.Types (Unfold(..))+import Streamly.Internal.Data.Unfold.Type (Unfold(..)) acceptOnAddrWith :: MonadIO m@@ -64,3 +65,5 @@ r <- liftIO (Net.accept listener `onException` Net.close listener) return $ D.Yield r listener +handleWithM :: (Socket -> IO ()) -> Socket -> IO ()+handleWithM f sk = finally (f sk) (Net.close sk)