mirror of
https://github.com/unclechu/gRPC-haskell.git
synced 2024-11-30 06:59:43 +01:00
Server-side call cancellation (#26)
* begin module with LowLevel tests of Op stuff, cancel functions, refactor Ops a little. * refactor op tests, add server-side cancellation test * add cancel function for unregistered calls, move some unregistered stuff * remove unnecessary threadDelay * update test description * fix init metadata api for registered server calls * pass call into handler for cancelling, add cancel test
This commit is contained in:
parent
58661adf8b
commit
1907fa66c4
12 changed files with 223 additions and 94 deletions
|
@ -8,13 +8,14 @@ import Control.Monad (forever)
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import Network.GRPC.LowLevel
|
import Network.GRPC.LowLevel
|
||||||
import qualified Network.GRPC.LowLevel.Server.Unregistered as U
|
import qualified Network.GRPC.LowLevel.Server.Unregistered as U
|
||||||
|
import qualified Network.GRPC.LowLevel.Call.Unregistered as U
|
||||||
|
|
||||||
serverMeta :: MetadataMap
|
serverMeta :: MetadataMap
|
||||||
serverMeta = [("test_meta", "test_meta_value")]
|
serverMeta = [("test_meta", "test_meta_value")]
|
||||||
|
|
||||||
handler :: ByteString -> MetadataMap -> MethodName
|
handler :: U.ServerCall -> ByteString -> MetadataMap -> MethodName
|
||||||
-> IO (ByteString, MetadataMap, StatusDetails)
|
-> IO (ByteString, MetadataMap, StatusDetails)
|
||||||
handler reqBody _reqMeta _method = do
|
handler _call reqBody _reqMeta _method = do
|
||||||
--putStrLn $ "Got request for method: " ++ show method
|
--putStrLn $ "Got request for method: " ++ show method
|
||||||
--putStrLn $ "Got metadata: " ++ show reqMeta
|
--putStrLn $ "Got metadata: " ++ show reqMeta
|
||||||
return (reqBody, serverMeta, StatusDetails "")
|
return (reqBody, serverMeta, StatusDetails "")
|
||||||
|
@ -34,7 +35,7 @@ regMain = withGRPC $ \grpc -> do
|
||||||
forever $ do
|
forever $ do
|
||||||
let method = head (registeredMethods server)
|
let method = head (registeredMethods server)
|
||||||
result <- serverHandleNormalCall server method 15 serverMeta $
|
result <- serverHandleNormalCall server method 15 serverMeta $
|
||||||
\reqBody _reqMeta -> return (reqBody, serverMeta, serverMeta,
|
\_call reqBody _reqMeta -> return (reqBody, serverMeta,
|
||||||
StatusDetails "")
|
StatusDetails "")
|
||||||
case result of
|
case result of
|
||||||
Left x -> putStrLn $ "registered call result error: " ++ show x
|
Left x -> putStrLn $ "registered call result error: " ++ show x
|
||||||
|
@ -44,7 +45,7 @@ regMain = withGRPC $ \grpc -> do
|
||||||
regLoop :: Server -> RegisteredMethod -> IO ()
|
regLoop :: Server -> RegisteredMethod -> IO ()
|
||||||
regLoop server method = forever $ do
|
regLoop server method = forever $ do
|
||||||
result <- serverHandleNormalCall server method 15 serverMeta $
|
result <- serverHandleNormalCall server method 15 serverMeta $
|
||||||
\reqBody _reqMeta -> return (reqBody, serverMeta, serverMeta,
|
\_call reqBody _reqMeta -> return (reqBody, serverMeta,
|
||||||
StatusDetails "")
|
StatusDetails "")
|
||||||
case result of
|
case result of
|
||||||
Left x -> putStrLn $ "registered call result error: " ++ show x
|
Left x -> putStrLn $ "registered call result error: " ++ show x
|
||||||
|
|
|
@ -44,7 +44,6 @@ library
|
||||||
Network.GRPC.LowLevel
|
Network.GRPC.LowLevel
|
||||||
Network.GRPC.LowLevel.Server.Unregistered
|
Network.GRPC.LowLevel.Server.Unregistered
|
||||||
Network.GRPC.LowLevel.Client.Unregistered
|
Network.GRPC.LowLevel.Client.Unregistered
|
||||||
other-modules:
|
|
||||||
Network.GRPC.LowLevel.CompletionQueue
|
Network.GRPC.LowLevel.CompletionQueue
|
||||||
Network.GRPC.LowLevel.CompletionQueue.Internal
|
Network.GRPC.LowLevel.CompletionQueue.Internal
|
||||||
Network.GRPC.LowLevel.CompletionQueue.Unregistered
|
Network.GRPC.LowLevel.CompletionQueue.Unregistered
|
||||||
|
@ -117,6 +116,7 @@ test-suite test
|
||||||
, containers ==0.5.*
|
, containers ==0.5.*
|
||||||
other-modules:
|
other-modules:
|
||||||
LowLevelTests,
|
LowLevelTests,
|
||||||
|
LowLevelTests.Op,
|
||||||
UnsafeTests
|
UnsafeTests
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
ghc-options: -Wall -fwarn-incomplete-patterns -fno-warn-unused-do-bind -g -threaded
|
ghc-options: -Wall -fwarn-incomplete-patterns -fno-warn-unused-do-bind -g -threaded
|
||||||
|
|
|
@ -34,6 +34,7 @@ GRPC
|
||||||
, withServer
|
, withServer
|
||||||
, serverHandleNormalCall
|
, serverHandleNormalCall
|
||||||
, withServerCall
|
, withServerCall
|
||||||
|
, serverCallCancel
|
||||||
|
|
||||||
-- * Client
|
-- * Client
|
||||||
, ClientConfig(..)
|
, ClientConfig(..)
|
||||||
|
@ -45,6 +46,7 @@ GRPC
|
||||||
, clientRegisterMethod
|
, clientRegisterMethod
|
||||||
, clientRequest
|
, clientRequest
|
||||||
, withClientCall
|
, withClientCall
|
||||||
|
, clientCallCancel
|
||||||
|
|
||||||
-- * Ops
|
-- * Ops
|
||||||
, Op(..)
|
, Op(..)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import qualified Network.GRPC.Unsafe as C
|
||||||
import qualified Network.GRPC.Unsafe.ByteBuffer as C
|
import qualified Network.GRPC.Unsafe.ByteBuffer as C
|
||||||
import qualified Network.GRPC.Unsafe.Metadata as C
|
import qualified Network.GRPC.Unsafe.Metadata as C
|
||||||
import qualified Network.GRPC.Unsafe.Time as C
|
import qualified Network.GRPC.Unsafe.Time as C
|
||||||
|
import qualified Network.GRPC.Unsafe.Op as C
|
||||||
|
|
||||||
import Network.GRPC.LowLevel.GRPC (MetadataMap, grpcDebug)
|
import Network.GRPC.LowLevel.GRPC (MetadataMap, grpcDebug)
|
||||||
|
|
||||||
|
@ -56,6 +57,9 @@ data RegisteredMethod = RegisteredMethod {methodType :: GRPCMethodType,
|
||||||
-- This is used to associate send/receive 'Op's with a request.
|
-- This is used to associate send/receive 'Op's with a request.
|
||||||
data ClientCall = ClientCall { unClientCall :: C.Call }
|
data ClientCall = ClientCall { unClientCall :: C.Call }
|
||||||
|
|
||||||
|
clientCallCancel :: ClientCall -> IO ()
|
||||||
|
clientCallCancel cc = C.grpcCallCancel (unClientCall cc) C.reserved
|
||||||
|
|
||||||
-- | Represents one registered GRPC call on the server. Contains pointers to all
|
-- | Represents one registered GRPC call on the server. Contains pointers to all
|
||||||
-- the C state needed to respond to a registered call.
|
-- the C state needed to respond to a registered call.
|
||||||
data ServerCall = ServerCall
|
data ServerCall = ServerCall
|
||||||
|
@ -66,6 +70,10 @@ data ServerCall = ServerCall
|
||||||
callDeadline :: C.CTimeSpecPtr
|
callDeadline :: C.CTimeSpecPtr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverCallCancel :: ServerCall -> C.StatusCode -> String -> IO ()
|
||||||
|
serverCallCancel sc code reason =
|
||||||
|
C.grpcCallCancelWithStatus (unServerCall sc) code reason C.reserved
|
||||||
|
|
||||||
serverCallGetMetadata :: ServerCall -> IO MetadataMap
|
serverCallGetMetadata :: ServerCall -> IO MetadataMap
|
||||||
serverCallGetMetadata ServerCall{..} = do
|
serverCallGetMetadata ServerCall{..} = do
|
||||||
marray <- peek requestMetadataRecv
|
marray <- peek requestMetadataRecv
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Network.GRPC.LowLevel.Call (Host (..), MethodName (..))
|
||||||
import Network.GRPC.LowLevel.GRPC (MetadataMap, grpcDebug)
|
import Network.GRPC.LowLevel.GRPC (MetadataMap, grpcDebug)
|
||||||
import qualified Network.GRPC.Unsafe as C
|
import qualified Network.GRPC.Unsafe as C
|
||||||
import qualified Network.GRPC.Unsafe.Metadata as C
|
import qualified Network.GRPC.Unsafe.Metadata as C
|
||||||
|
import qualified Network.GRPC.Unsafe.Op as C
|
||||||
|
|
||||||
-- | Represents one unregistered GRPC call on the server.
|
-- | Represents one unregistered GRPC call on the server.
|
||||||
-- Contains pointers to all the C state needed to respond to an unregistered
|
-- Contains pointers to all the C state needed to respond to an unregistered
|
||||||
|
@ -21,6 +22,10 @@ data ServerCall = ServerCall
|
||||||
, callDetails :: C.CallDetails
|
, callDetails :: C.CallDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverCallCancel :: ServerCall -> C.StatusCode -> String -> IO ()
|
||||||
|
serverCallCancel sc code reason =
|
||||||
|
C.grpcCallCancelWithStatus (unServerCall sc) code reason C.reserved
|
||||||
|
|
||||||
serverCallGetMetadata :: ServerCall -> IO MetadataMap
|
serverCallGetMetadata :: ServerCall -> IO MetadataMap
|
||||||
serverCallGetMetadata ServerCall{..} = do
|
serverCallGetMetadata ServerCall{..} = do
|
||||||
marray <- peek requestMetadataRecv
|
marray <- peek requestMetadataRecv
|
||||||
|
|
|
@ -128,10 +128,9 @@ serverRequestCall :: C.Server
|
||||||
-> CompletionQueue
|
-> CompletionQueue
|
||||||
-> TimeoutSeconds
|
-> TimeoutSeconds
|
||||||
-> RegisteredMethod
|
-> RegisteredMethod
|
||||||
-> MetadataMap
|
|
||||||
-> IO (Either GRPCIOError ServerCall)
|
-> IO (Either GRPCIOError ServerCall)
|
||||||
serverRequestCall
|
serverRequestCall
|
||||||
server cq@CompletionQueue{..} timeLimit RegisteredMethod{..} initMeta =
|
server cq@CompletionQueue{..} timeLimit RegisteredMethod{..} =
|
||||||
withPermission Push cq $ do
|
withPermission Push cq $ do
|
||||||
-- TODO: Is gRPC supposed to populate this deadline?
|
-- TODO: Is gRPC supposed to populate this deadline?
|
||||||
-- NOTE: the below stuff is freed when we free the call we return.
|
-- NOTE: the below stuff is freed when we free the call we return.
|
||||||
|
@ -139,15 +138,6 @@ serverRequestCall
|
||||||
callPtr <- malloc
|
callPtr <- malloc
|
||||||
metadataArrayPtr <- C.metadataArrayCreate
|
metadataArrayPtr <- C.metadataArrayCreate
|
||||||
metadataArray <- peek metadataArrayPtr
|
metadataArray <- peek metadataArrayPtr
|
||||||
#ifdef DEBUG
|
|
||||||
metaCount <- C.metadataArrayGetCount metadataArray
|
|
||||||
metaCap <- C.metadataArrayGetCapacity metadataArray
|
|
||||||
kvPtr <- C.metadataArrayGetMetadata metadataArray
|
|
||||||
grpcDebug $ "grpc-created meta: count: " ++ show metaCount
|
|
||||||
++ " capacity: " ++ show metaCap ++ " ptr: " ++ show kvPtr
|
|
||||||
#endif
|
|
||||||
metadataContents <- C.createMetadata initMeta
|
|
||||||
C.metadataArraySetMetadata metadataArray metadataContents
|
|
||||||
bbPtr <- malloc
|
bbPtr <- malloc
|
||||||
tag <- newTag cq
|
tag <- newTag cq
|
||||||
callError <- C.grpcServerRequestRegisteredCall
|
callError <- C.grpcServerRequestRegisteredCall
|
||||||
|
|
|
@ -129,15 +129,17 @@ freeOpContext (OpRecvCloseOnServerContext pcancelled) =
|
||||||
grpcDebug ("freeOpContext: freeing pcancelled: " ++ show pcancelled)
|
grpcDebug ("freeOpContext: freeing pcancelled: " ++ show pcancelled)
|
||||||
>> free pcancelled
|
>> free pcancelled
|
||||||
|
|
||||||
-- | Converts a list of 'Op's into the corresponding 'OpContext's and guarantees
|
-- | Allocates an `OpArray` and a list of `OpContext`s from the given list of
|
||||||
-- they will be cleaned up correctly.
|
-- `Op`s.
|
||||||
withOpContexts :: [Op] -> ([OpContext] -> IO a) -> IO a
|
withOpArrayAndCtxts :: [Op] -> ((C.OpArray, [OpContext]) -> IO a) -> IO a
|
||||||
withOpContexts ops = bracket (mapM createOpContext ops)
|
withOpArrayAndCtxts ops = bracket setup teardown
|
||||||
(mapM freeOpContext)
|
where setup = do ctxts <- mapM createOpContext ops
|
||||||
|
let l = length ops
|
||||||
withOpArray :: Int -> (C.OpArray -> IO a) -> IO a
|
arr <- C.opArrayCreate l
|
||||||
withOpArray n = bracket (C.opArrayCreate n)
|
sequence_ $ zipWith (setOpArray arr) [0..l-1] ctxts
|
||||||
(flip C.opArrayDestroy n)
|
return (arr, ctxts)
|
||||||
|
teardown (arr, ctxts) = do C.opArrayDestroy arr (length ctxts)
|
||||||
|
mapM_ freeOpContext ctxts
|
||||||
|
|
||||||
-- | Container holding GC-managed results for 'Op's which receive data.
|
-- | Container holding GC-managed results for 'Op's which receive data.
|
||||||
data OpRecvResult =
|
data OpRecvResult =
|
||||||
|
@ -216,11 +218,8 @@ runOps :: C.Call
|
||||||
-> IO (Either GRPCIOError [OpRecvResult])
|
-> IO (Either GRPCIOError [OpRecvResult])
|
||||||
runOps call cq ops timeLimit =
|
runOps call cq ops timeLimit =
|
||||||
let l = length ops in
|
let l = length ops in
|
||||||
withOpArray l $ \opArray -> do
|
withOpArrayAndCtxts ops $ \(opArray, contexts) -> do
|
||||||
grpcDebug "runOps: created op array."
|
|
||||||
withOpContexts ops $ \contexts -> do
|
|
||||||
grpcDebug $ "runOps: allocated op contexts: " ++ show contexts
|
grpcDebug $ "runOps: allocated op contexts: " ++ show contexts
|
||||||
sequence_ $ zipWith (setOpArray opArray) [0..l-1] contexts
|
|
||||||
tag <- newTag cq
|
tag <- newTag cq
|
||||||
callError <- startBatch cq call opArray l tag
|
callError <- startBatch cq call opArray l tag
|
||||||
grpcDebug $ "runOps: called start_batch. callError: "
|
grpcDebug $ "runOps: called start_batch. callError: "
|
||||||
|
|
|
@ -125,42 +125,23 @@ serverRegisterMethod _ _ _ _ = error "Streaming methods not implemented yet."
|
||||||
serverCreateCall :: Server
|
serverCreateCall :: Server
|
||||||
-> RegisteredMethod
|
-> RegisteredMethod
|
||||||
-> TimeoutSeconds
|
-> TimeoutSeconds
|
||||||
-> MetadataMap
|
|
||||||
-> IO (Either GRPCIOError ServerCall)
|
-> IO (Either GRPCIOError ServerCall)
|
||||||
serverCreateCall Server{..} rm timeLimit initMeta =
|
serverCreateCall Server{..} rm timeLimit =
|
||||||
serverRequestCall internalServer serverCQ timeLimit rm initMeta
|
serverRequestCall internalServer serverCQ timeLimit rm
|
||||||
|
|
||||||
withServerCall :: Server
|
withServerCall :: Server
|
||||||
-> RegisteredMethod
|
-> RegisteredMethod
|
||||||
-> TimeoutSeconds
|
-> TimeoutSeconds
|
||||||
-> MetadataMap
|
|
||||||
-> (ServerCall -> IO (Either GRPCIOError a))
|
-> (ServerCall -> IO (Either GRPCIOError a))
|
||||||
-> IO (Either GRPCIOError a)
|
-> IO (Either GRPCIOError a)
|
||||||
withServerCall server regmethod timeout initMeta f = do
|
withServerCall server regmethod timeout f = do
|
||||||
createResult <- serverCreateCall server regmethod timeout initMeta
|
createResult <- serverCreateCall server regmethod timeout
|
||||||
case createResult of
|
case createResult of
|
||||||
Left x -> return $ Left x
|
Left x -> return $ Left x
|
||||||
Right call -> f call `finally` logDestroy call
|
Right call -> f call `finally` logDestroy call
|
||||||
where logDestroy c = grpcDebug "withServerRegisteredCall: destroying."
|
where logDestroy c = grpcDebug "withServerRegisteredCall: destroying."
|
||||||
>> destroyServerCall c
|
>> destroyServerCall c
|
||||||
|
|
||||||
-- | Sequence of 'Op's needed to receive a normal (non-streaming) call.
|
|
||||||
serverOpsGetNormalCall :: MetadataMap -> [Op]
|
|
||||||
serverOpsGetNormalCall initMetadata =
|
|
||||||
[OpSendInitialMetadata initMetadata,
|
|
||||||
OpRecvMessage]
|
|
||||||
|
|
||||||
-- | Sequence of 'Op's needed to respond to a normal (non-streaming) call.
|
|
||||||
serverOpsSendNormalResponse :: ByteString
|
|
||||||
-> MetadataMap
|
|
||||||
-> C.StatusCode
|
|
||||||
-> StatusDetails
|
|
||||||
-> [Op]
|
|
||||||
serverOpsSendNormalResponse body metadata code details =
|
|
||||||
[OpRecvCloseOnServer,
|
|
||||||
OpSendMessage body,
|
|
||||||
OpSendStatusFromServer metadata code details]
|
|
||||||
|
|
||||||
serverOpsSendNormalRegisteredResponse :: ByteString
|
serverOpsSendNormalRegisteredResponse :: ByteString
|
||||||
-> MetadataMap
|
-> MetadataMap
|
||||||
-- ^ initial metadata
|
-- ^ initial metadata
|
||||||
|
@ -180,13 +161,14 @@ serverOpsSendNormalRegisteredResponse
|
||||||
-- body, with the bytestring response body in the result tuple. The first
|
-- body, with the bytestring response body in the result tuple. The first
|
||||||
-- metadata parameter refers to the request metadata, with the two metadata
|
-- metadata parameter refers to the request metadata, with the two metadata
|
||||||
-- values in the result tuple being the initial and trailing metadata
|
-- values in the result tuple being the initial and trailing metadata
|
||||||
-- respectively.
|
-- respectively. We pass in the 'ServerCall' so that the server can call
|
||||||
|
-- 'serverCallCancel' on it if needed.
|
||||||
|
|
||||||
-- TODO: make a more rigid type for this with a Maybe MetadataMap for the
|
-- TODO: make a more rigid type for this with a Maybe MetadataMap for the
|
||||||
-- trailing meta, and use it for both kinds of call handlers.
|
-- trailing meta, and use it for both kinds of call handlers.
|
||||||
type ServerHandler
|
type ServerHandler
|
||||||
= ByteString -> MetadataMap
|
= ServerCall -> ByteString -> MetadataMap
|
||||||
-> IO (ByteString, MetadataMap, MetadataMap, StatusDetails)
|
-> IO (ByteString, MetadataMap, StatusDetails)
|
||||||
|
|
||||||
-- TODO: we will want to replace this with some more general concept that also
|
-- TODO: we will want to replace this with some more general concept that also
|
||||||
-- works with streaming calls in the future.
|
-- works with streaming calls in the future.
|
||||||
|
@ -198,12 +180,12 @@ serverHandleNormalCall :: Server
|
||||||
-- ^ Initial server metadata
|
-- ^ Initial server metadata
|
||||||
-> ServerHandler
|
-> ServerHandler
|
||||||
-> IO (Either GRPCIOError ())
|
-> IO (Either GRPCIOError ())
|
||||||
serverHandleNormalCall s@Server{..} rm timeLimit srvMetadata f = do
|
serverHandleNormalCall s@Server{..} rm timeLimit initMeta f = do
|
||||||
-- TODO: we use this timeLimit twice, so the max time spent is 2*timeLimit.
|
-- TODO: we use this timeLimit twice, so the max time spent is 2*timeLimit.
|
||||||
-- Should we just hard-code time limits instead? Not sure if client
|
-- Should we just hard-code time limits instead? Not sure if client
|
||||||
-- programmer cares, since this function will likely just be put in a loop
|
-- programmer cares, since this function will likely just be put in a loop
|
||||||
-- anyway.
|
-- anyway.
|
||||||
withServerCall s rm timeLimit srvMetadata $ \call -> do
|
withServerCall s rm timeLimit $ \call -> do
|
||||||
grpcDebug "serverHandleNormalCall(R): starting batch."
|
grpcDebug "serverHandleNormalCall(R): starting batch."
|
||||||
debugServerCall call
|
debugServerCall call
|
||||||
payload <- serverCallGetPayload call
|
payload <- serverCallGetPayload call
|
||||||
|
@ -213,7 +195,7 @@ serverHandleNormalCall s@Server{..} rm timeLimit srvMetadata f = do
|
||||||
Nothing -> error "serverHandleNormalCall(R): payload empty."
|
Nothing -> error "serverHandleNormalCall(R): payload empty."
|
||||||
Just requestBody -> do
|
Just requestBody -> do
|
||||||
requestMeta <- serverCallGetMetadata call
|
requestMeta <- serverCallGetMetadata call
|
||||||
(respBody, initMeta, trailingMeta, details) <- f requestBody requestMeta
|
(respBody, trailingMeta, details) <- f call requestBody requestMeta
|
||||||
let status = C.GrpcStatusOk
|
let status = C.GrpcStatusOk
|
||||||
let respOps = serverOpsSendNormalRegisteredResponse
|
let respOps = serverOpsSendNormalRegisteredResponse
|
||||||
respBody initMeta trailingMeta status details
|
respBody initMeta trailingMeta status details
|
||||||
|
|
|
@ -9,10 +9,8 @@ import Network.GRPC.LowLevel.Call.Unregistered
|
||||||
import Network.GRPC.LowLevel.CompletionQueue (TimeoutSeconds)
|
import Network.GRPC.LowLevel.CompletionQueue (TimeoutSeconds)
|
||||||
import Network.GRPC.LowLevel.CompletionQueue.Unregistered (serverRequestCall)
|
import Network.GRPC.LowLevel.CompletionQueue.Unregistered (serverRequestCall)
|
||||||
import Network.GRPC.LowLevel.GRPC
|
import Network.GRPC.LowLevel.GRPC
|
||||||
import Network.GRPC.LowLevel.Op (OpRecvResult (..), runOps)
|
import Network.GRPC.LowLevel.Op (Op(..), OpRecvResult (..), runOps)
|
||||||
import Network.GRPC.LowLevel.Server (Server (..),
|
import Network.GRPC.LowLevel.Server (Server (..))
|
||||||
serverOpsGetNormalCall,
|
|
||||||
serverOpsSendNormalResponse)
|
|
||||||
import qualified Network.GRPC.Unsafe.Op as C
|
import qualified Network.GRPC.Unsafe.Op as C
|
||||||
|
|
||||||
serverCreateCall :: Server -> TimeoutSeconds
|
serverCreateCall :: Server -> TimeoutSeconds
|
||||||
|
@ -31,10 +29,30 @@ withServerCall server timeout f = do
|
||||||
where logDestroy c = grpcDebug "withServerCall: destroying."
|
where logDestroy c = grpcDebug "withServerCall: destroying."
|
||||||
>> destroyServerCall c
|
>> destroyServerCall c
|
||||||
|
|
||||||
|
-- | Sequence of 'Op's needed to receive a normal (non-streaming) call.
|
||||||
|
-- TODO: We have to put 'OpRecvCloseOnServer' in the response ops, or else the
|
||||||
|
-- client times out. Given this, I have no idea how to check for cancellation on
|
||||||
|
-- the server.
|
||||||
|
serverOpsGetNormalCall :: MetadataMap -> [Op]
|
||||||
|
serverOpsGetNormalCall initMetadata =
|
||||||
|
[OpSendInitialMetadata initMetadata,
|
||||||
|
OpRecvMessage]
|
||||||
|
|
||||||
|
-- | Sequence of 'Op's needed to respond to a normal (non-streaming) call.
|
||||||
|
serverOpsSendNormalResponse :: ByteString
|
||||||
|
-> MetadataMap
|
||||||
|
-> C.StatusCode
|
||||||
|
-> StatusDetails
|
||||||
|
-> [Op]
|
||||||
|
serverOpsSendNormalResponse body metadata code details =
|
||||||
|
[OpRecvCloseOnServer,
|
||||||
|
OpSendMessage body,
|
||||||
|
OpSendStatusFromServer metadata code details]
|
||||||
|
|
||||||
-- | A handler for an unregistered server call; bytestring arguments are the
|
-- | A handler for an unregistered server call; bytestring arguments are the
|
||||||
-- request body and response body respectively.
|
-- request body and response body respectively.
|
||||||
type ServerHandler
|
type ServerHandler
|
||||||
= ByteString -> MetadataMap -> MethodName
|
= ServerCall -> ByteString -> MetadataMap -> MethodName
|
||||||
-> IO (ByteString, MetadataMap, StatusDetails)
|
-> IO (ByteString, MetadataMap, StatusDetails)
|
||||||
|
|
||||||
-- | Handle one unregistered call.
|
-- | Handle one unregistered call.
|
||||||
|
@ -58,7 +76,7 @@ serverHandleNormalCall s@Server{..} timeLimit srvMetadata f = do
|
||||||
methodName <- serverCallGetMethodName call
|
methodName <- serverCallGetMethodName call
|
||||||
hostName <- serverCallGetHost call
|
hostName <- serverCallGetHost call
|
||||||
grpcDebug $ "call_details host is: " ++ show hostName
|
grpcDebug $ "call_details host is: " ++ show hostName
|
||||||
(respBody, respMetadata, details) <- f body requestMeta methodName
|
(respBody, respMetadata, details) <- f call body requestMeta methodName
|
||||||
let status = C.GrpcStatusOk
|
let status = C.GrpcStatusOk
|
||||||
let respOps = serverOpsSendNormalResponse
|
let respOps = serverOpsSendNormalResponse
|
||||||
respBody respMetadata status details
|
respBody respMetadata status details
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module LowLevelTests (lowLevelTests) where
|
module LowLevelTests where
|
||||||
|
|
||||||
import Control.Concurrent (threadDelay)
|
import Control.Concurrent (threadDelay)
|
||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
|
@ -34,6 +34,7 @@ lowLevelTests = testGroup "Unit tests of low-level Haskell library"
|
||||||
-- , testWrongEndpoint
|
-- , testWrongEndpoint
|
||||||
, testPayload
|
, testPayload
|
||||||
, testPayloadUnregistered
|
, testPayloadUnregistered
|
||||||
|
, testServerCancel
|
||||||
, testGoaway
|
, testGoaway
|
||||||
, testSlowServer
|
, testSlowServer
|
||||||
]
|
]
|
||||||
|
@ -78,8 +79,8 @@ testServerTimeoutNoClient :: TestTree
|
||||||
testServerTimeoutNoClient =
|
testServerTimeoutNoClient =
|
||||||
serverOnlyTest "wait timeout when client DNE" [("/foo", Normal)] $ \s -> do
|
serverOnlyTest "wait timeout when client DNE" [("/foo", Normal)] $ \s -> do
|
||||||
let rm = head (registeredMethods s)
|
let rm = head (registeredMethods s)
|
||||||
r <- serverHandleNormalCall s rm 1 mempty $ \_ _ ->
|
r <- serverHandleNormalCall s rm 1 mempty $ \_ _ _ ->
|
||||||
return ("", mempty, mempty, StatusDetails "details")
|
return ("", mempty, StatusDetails "details")
|
||||||
r @?= Left GRPCIOTimeout
|
r @?= Left GRPCIOTimeout
|
||||||
|
|
||||||
-- TODO: fix this test: currently, client seems to hang and server times out,
|
-- TODO: fix this test: currently, client seems to hang and server times out,
|
||||||
|
@ -99,8 +100,8 @@ testWrongEndpoint =
|
||||||
server s = do
|
server s = do
|
||||||
length (registeredMethods s) @?= 1
|
length (registeredMethods s) @?= 1
|
||||||
let rm = head (registeredMethods s)
|
let rm = head (registeredMethods s)
|
||||||
r <- serverHandleNormalCall s rm 10 mempty $ \_ _ -> do
|
r <- serverHandleNormalCall s rm 10 mempty $ \_ _ _ -> do
|
||||||
return ("reply test", dummyMeta, dummyMeta, StatusDetails "details string")
|
return ("reply test", dummyMeta, StatusDetails "details string")
|
||||||
r @?= Right ()
|
r @?= Right ()
|
||||||
|
|
||||||
-- TODO: There seems to be a race here (and in other client/server pairs, of
|
-- TODO: There seems to be a race here (and in other client/server pairs, of
|
||||||
|
@ -126,10 +127,27 @@ testPayload =
|
||||||
server s = do
|
server s = do
|
||||||
length (registeredMethods s) @?= 1
|
length (registeredMethods s) @?= 1
|
||||||
let rm = head (registeredMethods s)
|
let rm = head (registeredMethods s)
|
||||||
r <- serverHandleNormalCall s rm 11 mempty $ \reqBody reqMD -> do
|
r <- serverHandleNormalCall s rm 11 dummyMeta $ \_ reqBody reqMD -> do
|
||||||
reqBody @?= "Hello!"
|
reqBody @?= "Hello!"
|
||||||
checkMD "Server metadata mismatch" clientMD reqMD
|
checkMD "Server metadata mismatch" clientMD reqMD
|
||||||
return ("reply test", dummyMeta, dummyMeta, StatusDetails "details string")
|
return ("reply test", dummyMeta, StatusDetails "details string")
|
||||||
|
r @?= Right ()
|
||||||
|
|
||||||
|
testServerCancel :: TestTree
|
||||||
|
testServerCancel =
|
||||||
|
csTest "server cancel call" client server [("/foo", Normal)]
|
||||||
|
where
|
||||||
|
client c = do
|
||||||
|
rm <- clientRegisterMethod c "/foo" Normal
|
||||||
|
res <- clientRequest c rm 10 "" mempty
|
||||||
|
res @?= Left (GRPCIOBadStatusCode GrpcStatusCancelled
|
||||||
|
(StatusDetails
|
||||||
|
"Received RST_STREAM err=8"))
|
||||||
|
server s = do
|
||||||
|
let rm = head (registeredMethods s)
|
||||||
|
r <- serverHandleNormalCall s rm 10 mempty $ \c _ _ -> do
|
||||||
|
serverCallCancel c GrpcStatusCancelled ""
|
||||||
|
return (mempty, mempty, "")
|
||||||
r @?= Right ()
|
r @?= Right ()
|
||||||
|
|
||||||
testPayloadUnregistered :: TestTree
|
testPayloadUnregistered :: TestTree
|
||||||
|
@ -143,7 +161,7 @@ testPayloadUnregistered =
|
||||||
rspBody @?= "reply test"
|
rspBody @?= "reply test"
|
||||||
details @?= "details string"
|
details @?= "details string"
|
||||||
server s = do
|
server s = do
|
||||||
r <- U.serverHandleNormalCall s 11 mempty $ \body _md meth -> do
|
r <- U.serverHandleNormalCall s 11 mempty $ \_ body _md meth -> do
|
||||||
body @?= "Hello!"
|
body @?= "Hello!"
|
||||||
meth @?= "/foo"
|
meth @?= "/foo"
|
||||||
return ("reply test", mempty, "details string")
|
return ("reply test", mempty, "details string")
|
||||||
|
@ -184,9 +202,9 @@ testSlowServer =
|
||||||
result == deadlineExceededStatus
|
result == deadlineExceededStatus
|
||||||
server s = do
|
server s = do
|
||||||
let rm = head (registeredMethods s)
|
let rm = head (registeredMethods s)
|
||||||
serverHandleNormalCall s rm 1 mempty $ \_ _ -> do
|
serverHandleNormalCall s rm 1 mempty $ \_ _ _ -> do
|
||||||
threadDelay (2*10^(6 :: Int))
|
threadDelay (2*10^(6 :: Int))
|
||||||
return ("", mempty, mempty, StatusDetails "")
|
return ("", mempty, StatusDetails "")
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
@ -195,9 +213,9 @@ testSlowServer =
|
||||||
dummyMeta :: M.Map ByteString ByteString
|
dummyMeta :: M.Map ByteString ByteString
|
||||||
dummyMeta = [("foo","bar")]
|
dummyMeta = [("foo","bar")]
|
||||||
|
|
||||||
dummyHandler :: ByteString -> MetadataMap
|
dummyHandler :: ServerCall -> ByteString -> MetadataMap
|
||||||
-> IO (ByteString, MetadataMap, MetadataMap, StatusDetails)
|
-> IO (ByteString, MetadataMap, StatusDetails)
|
||||||
dummyHandler _ _ = return ("", mempty, mempty, StatusDetails "")
|
dummyHandler _ _ _ = return ("", mempty, StatusDetails "")
|
||||||
|
|
||||||
unavailableStatus :: Either GRPCIOError a
|
unavailableStatus :: Either GRPCIOError a
|
||||||
unavailableStatus =
|
unavailableStatus =
|
||||||
|
|
104
tests/LowLevelTests/Op.hs
Normal file
104
tests/LowLevelTests/Op.hs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
module LowLevelTests.Op where
|
||||||
|
|
||||||
|
import Control.Concurrent (threadDelay)
|
||||||
|
import Control.Concurrent.Async
|
||||||
|
import Control.Monad
|
||||||
|
import Data.ByteString (ByteString, isPrefixOf)
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Foreign.Storable (peek)
|
||||||
|
import Test.Tasty
|
||||||
|
import Test.Tasty.HUnit as HU (testCase, (@?=),
|
||||||
|
assertBool)
|
||||||
|
|
||||||
|
import Network.GRPC.LowLevel
|
||||||
|
import Network.GRPC.LowLevel.Call
|
||||||
|
import Network.GRPC.LowLevel.Client
|
||||||
|
import Network.GRPC.LowLevel.Server
|
||||||
|
import Network.GRPC.LowLevel.Op
|
||||||
|
import Network.GRPC.LowLevel.CompletionQueue
|
||||||
|
|
||||||
|
lowLevelOpTests :: TestTree
|
||||||
|
lowLevelOpTests = testGroup "Synchronous unit tests of low-level Op interface"
|
||||||
|
[testCancelWhileHandling
|
||||||
|
,testCancelFromServer]
|
||||||
|
|
||||||
|
testCancelWhileHandling :: TestTree
|
||||||
|
testCancelWhileHandling =
|
||||||
|
testCase "Client/Server - cancel after handler starts does nothing" $
|
||||||
|
runSerialTest $ \grpc ->
|
||||||
|
withClientServerUnaryCall grpc $
|
||||||
|
\(c@Client{..}, s@Server{..}, cc@ClientCall{..}, sc@ServerCall{..}) -> do
|
||||||
|
withOpArrayAndCtxts serverEmptyRecvOps $ \(opArray, ctxts) -> do
|
||||||
|
tag <- newTag serverCQ
|
||||||
|
startBatch serverCQ unServerCall opArray 3 tag
|
||||||
|
pluck serverCQ tag 1
|
||||||
|
let (OpRecvCloseOnServerContext pcancelled) = last ctxts
|
||||||
|
cancelledBefore <- peek pcancelled
|
||||||
|
cancelledBefore @?= 0
|
||||||
|
clientCallCancel cc
|
||||||
|
threadDelay 1000000
|
||||||
|
cancelledAfter <- peek pcancelled
|
||||||
|
cancelledAfter @?= 0
|
||||||
|
return $ Right ()
|
||||||
|
|
||||||
|
testCancelFromServer :: TestTree
|
||||||
|
testCancelFromServer =
|
||||||
|
testCase "Client/Server - client receives server cancellation" $
|
||||||
|
runSerialTest $ \grpc ->
|
||||||
|
withClientServerUnaryCall grpc $
|
||||||
|
\(c@Client{..}, s@Server{..}, cc@ClientCall{..}, sc@ServerCall{..}) -> do
|
||||||
|
serverCallCancel sc GrpcStatusPermissionDenied "TestStatus"
|
||||||
|
clientRes <- runOps unClientCall clientCQ clientRecvOps 1
|
||||||
|
case clientRes of
|
||||||
|
Left x -> error $ "Client recv error: " ++ show x
|
||||||
|
Right [_,_,OpRecvStatusOnClientResult _ code details] -> do
|
||||||
|
code @?= GrpcStatusPermissionDenied
|
||||||
|
assertBool "Received status details or RST_STREAM error" $
|
||||||
|
details == "TestStatus"
|
||||||
|
||
|
||||||
|
isPrefixOf "Received RST_STREAM" details
|
||||||
|
return $ Right ()
|
||||||
|
|
||||||
|
|
||||||
|
runSerialTest :: (GRPC -> IO (Either GRPCIOError ())) -> IO ()
|
||||||
|
runSerialTest f =
|
||||||
|
withGRPC f >>= \case Left x -> error $ show x
|
||||||
|
Right () -> return ()
|
||||||
|
|
||||||
|
withClientServerUnaryCall :: GRPC
|
||||||
|
-> ((Client, Server, ClientCall, ServerCall)
|
||||||
|
-> IO (Either GRPCIOError a))
|
||||||
|
-> IO (Either GRPCIOError a)
|
||||||
|
withClientServerUnaryCall grpc f = do
|
||||||
|
withClient grpc clientConf $ \c -> do
|
||||||
|
crm <- clientRegisterMethod c "/foo" Normal
|
||||||
|
withServer grpc serverConf $ \s ->
|
||||||
|
withClientCall c crm 10 $ \cc -> do
|
||||||
|
let srm = head (registeredMethods s)
|
||||||
|
-- NOTE: We need to send client ops here or else `withServerCall` hangs,
|
||||||
|
-- because registered methods try to do recv ops immediately when
|
||||||
|
-- created. If later we want to send payloads or metadata, we'll need
|
||||||
|
-- to tweak this.
|
||||||
|
clientRes <- runOps (unClientCall cc) (clientCQ c) clientEmptySendOps 1
|
||||||
|
withServerCall s srm 10 $ \sc ->
|
||||||
|
f (c, s, cc, sc)
|
||||||
|
|
||||||
|
serverConf = (ServerConfig "localhost" 50051 [("/foo", Normal)])
|
||||||
|
|
||||||
|
clientConf = (ClientConfig "localhost" 50051)
|
||||||
|
|
||||||
|
clientEmptySendOps = [OpSendInitialMetadata mempty,
|
||||||
|
OpSendMessage "",
|
||||||
|
OpSendCloseFromClient]
|
||||||
|
|
||||||
|
clientRecvOps = [OpRecvInitialMetadata,
|
||||||
|
OpRecvMessage,
|
||||||
|
OpRecvStatusOnClient]
|
||||||
|
|
||||||
|
serverEmptyRecvOps = [OpSendInitialMetadata mempty,
|
||||||
|
OpRecvMessage,
|
||||||
|
OpRecvCloseOnServer]
|
|
@ -1,9 +1,11 @@
|
||||||
import LowLevelTests
|
import LowLevelTests
|
||||||
|
import LowLevelTests.Op
|
||||||
import Test.Tasty
|
import Test.Tasty
|
||||||
import UnsafeTests
|
import UnsafeTests
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = defaultMain $ testGroup "GRPC Unit Tests"
|
main = defaultMain $ testGroup "GRPC Unit Tests"
|
||||||
[ unsafeTests
|
[ unsafeTests
|
||||||
|
, lowLevelOpTests
|
||||||
, lowLevelTests
|
, lowLevelTests
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue