mirror of
https://github.com/unclechu/gRPC-haskell.git
synced 2024-07-06 03:14:31 +02:00
* fix testWrongEndpoint * test that unregistered requests are ignored by request_registered_call * handler returns status code, drop Grpc prefix from status codes
206 lines
9.5 KiB
Haskell
206 lines
9.5 KiB
Haskell
{-# LANGUAGE RecordWildCards #-}
|
|
|
|
-- | This module defines data structures and operations pertaining to registered
|
|
-- servers using registered calls; for unregistered support, see
|
|
-- `Network.GRPC.LowLevel.Server.Unregistered`.
|
|
module Network.GRPC.LowLevel.Server where
|
|
|
|
import Control.Exception (bracket, finally)
|
|
import Control.Monad
|
|
import Data.ByteString (ByteString)
|
|
import Foreign.Ptr (nullPtr)
|
|
import Network.GRPC.LowLevel.Call
|
|
import Network.GRPC.LowLevel.CompletionQueue (CompletionQueue,
|
|
TimeoutSeconds,
|
|
createCompletionQueue,
|
|
pluck,
|
|
serverRegisterCompletionQueue,
|
|
serverRequestCall,
|
|
serverShutdownAndNotify,
|
|
shutdownCompletionQueue)
|
|
import Network.GRPC.LowLevel.GRPC
|
|
import Network.GRPC.LowLevel.Op
|
|
import qualified Network.GRPC.Unsafe as C
|
|
import qualified Network.GRPC.Unsafe.Op as C
|
|
|
|
-- | Wraps various gRPC state needed to run a server.
|
|
data Server = Server
|
|
{ internalServer :: C.Server
|
|
, serverCQ :: CompletionQueue
|
|
, registeredMethods :: [RegisteredMethod]
|
|
, serverConfig :: ServerConfig
|
|
}
|
|
|
|
-- | Configuration needed to start a server.
|
|
data ServerConfig = ServerConfig
|
|
{ host :: Host
|
|
-- ^ Name of the host the server is running on. Not sure how this is
|
|
-- used. Setting to "localhost" works fine in tests.
|
|
, port :: Port
|
|
-- ^ Port on which to listen for requests.
|
|
, methodsToRegister :: [(MethodName, GRPCMethodType)]
|
|
-- ^ List of (method name, method type) tuples specifying all methods to
|
|
-- register. You can also handle other unregistered methods with
|
|
-- `serverHandleNormalCall`.
|
|
}
|
|
deriving (Show, Eq)
|
|
|
|
serverEndpoint :: ServerConfig -> Endpoint
|
|
serverEndpoint ServerConfig{..} = endpoint host port
|
|
|
|
startServer :: GRPC -> ServerConfig -> IO Server
|
|
startServer grpc conf@ServerConfig{..} = do
|
|
let e = serverEndpoint conf
|
|
server <- C.grpcServerCreate nullPtr C.reserved
|
|
actualPort <- C.grpcServerAddInsecureHttp2Port server (unEndpoint e)
|
|
when (actualPort /= unPort port) $
|
|
error $ "Unable to bind port: " ++ show port
|
|
cq <- createCompletionQueue grpc
|
|
serverRegisterCompletionQueue server cq
|
|
methods <- forM methodsToRegister $ \(name, mtype) ->
|
|
serverRegisterMethod server name e mtype
|
|
C.grpcServerStart server
|
|
return $ Server server cq methods conf
|
|
|
|
stopServer :: Server -> IO ()
|
|
-- TODO: Do method handles need to be freed?
|
|
stopServer (Server server cq _ _) = do
|
|
grpcDebug "stopServer: calling shutdownNotify."
|
|
shutdownNotify
|
|
grpcDebug "stopServer: cancelling all calls."
|
|
C.grpcServerCancelAllCalls server
|
|
grpcDebug "stopServer: call grpc_server_destroy."
|
|
C.grpcServerDestroy server
|
|
grpcDebug "stopServer: shutting down CQ."
|
|
shutdownCQ
|
|
|
|
where shutdownCQ = do
|
|
shutdownResult <- shutdownCompletionQueue cq
|
|
case shutdownResult of
|
|
Left _ -> do putStrLn "Warning: completion queue didn't shut down."
|
|
putStrLn "Trying to stop server anyway."
|
|
Right _ -> return ()
|
|
shutdownNotify = do
|
|
let shutdownTag = C.tag 0
|
|
serverShutdownAndNotify server cq shutdownTag
|
|
grpcDebug "called serverShutdownAndNotify; plucking."
|
|
shutdownEvent <- pluck cq shutdownTag (Just 30)
|
|
grpcDebug $ "shutdownNotify: got shutdown event" ++ show shutdownEvent
|
|
case shutdownEvent of
|
|
-- This case occurs when we pluck but the queue is already in the
|
|
-- 'shuttingDown' state, implying we already tried to shut down.
|
|
(Left GRPCIOShutdown) -> error "Called stopServer twice!"
|
|
(Left _) -> error "Failed to stop server."
|
|
(Right _) -> return ()
|
|
|
|
-- Uses 'bracket' to safely start and stop a server, even if exceptions occur.
|
|
withServer :: GRPC -> ServerConfig -> (Server -> IO a) -> IO a
|
|
withServer grpc cfg f = bracket (startServer grpc cfg) stopServer f
|
|
|
|
-- | Register a method on a server. The 'RegisteredMethod' type can then be used
|
|
-- to wait for a request to arrive. Note: gRPC claims this must be called before
|
|
-- the server is started, so we do it during startup according to the
|
|
-- 'ServerConfig'.
|
|
serverRegisterMethod :: C.Server
|
|
-> MethodName
|
|
-- ^ method name, e.g. "/foo"
|
|
-> Endpoint
|
|
-- ^ Endpoint name name, e.g. "localhost:9999". I have no
|
|
-- idea why this is needed since we have to provide these
|
|
-- parameters to start a server in the first place. It
|
|
-- doesn't seem to have any effect, even if it's filled
|
|
-- with nonsense.
|
|
-> GRPCMethodType
|
|
-- ^ Type of method this will be. In the future, this will
|
|
-- be used to switch to the correct handling logic.
|
|
-- Currently, the only valid choice is 'Normal'.
|
|
-> IO RegisteredMethod
|
|
serverRegisterMethod internalServer meth e Normal = do
|
|
handle <- C.grpcServerRegisterMethod internalServer
|
|
(unMethodName meth) (unEndpoint e)
|
|
grpcDebug $ "registered method to handle " ++ show handle
|
|
return $ RegisteredMethod Normal meth e handle
|
|
serverRegisterMethod _ _ _ _ = error "Streaming methods not implemented yet."
|
|
|
|
-- | Create a 'Call' with which to wait for the invocation of a registered
|
|
-- method.
|
|
serverCreateCall :: Server
|
|
-> RegisteredMethod
|
|
-> TimeoutSeconds
|
|
-> IO (Either GRPCIOError ServerCall)
|
|
serverCreateCall Server{..} rm timeLimit =
|
|
serverRequestCall internalServer serverCQ timeLimit rm
|
|
|
|
withServerCall :: Server
|
|
-> RegisteredMethod
|
|
-> TimeoutSeconds
|
|
-> (ServerCall -> IO (Either GRPCIOError a))
|
|
-> IO (Either GRPCIOError a)
|
|
withServerCall server regmethod timeout f = do
|
|
createResult <- serverCreateCall server regmethod timeout
|
|
case createResult of
|
|
Left x -> return $ Left x
|
|
Right call -> f call `finally` logDestroy call
|
|
where logDestroy c = grpcDebug "withServerRegisteredCall: destroying."
|
|
>> destroyServerCall c
|
|
|
|
serverOpsSendNormalRegisteredResponse :: ByteString
|
|
-> MetadataMap
|
|
-- ^ initial metadata
|
|
-> MetadataMap
|
|
-- ^ trailing metadata
|
|
-> C.StatusCode
|
|
-> StatusDetails
|
|
-> [Op]
|
|
serverOpsSendNormalRegisteredResponse
|
|
body initMetadata trailingMeta code details =
|
|
[OpSendInitialMetadata initMetadata,
|
|
OpRecvCloseOnServer,
|
|
OpSendMessage body,
|
|
OpSendStatusFromServer trailingMeta code details]
|
|
|
|
-- | A handler for an registered server call; bytestring parameter is request
|
|
-- body, with the bytestring response body in the result tuple. The first
|
|
-- metadata parameter refers to the request metadata, with the two metadata
|
|
-- values in the result tuple being the initial and trailing metadata
|
|
-- 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
|
|
-- trailing meta, and use it for both kinds of call handlers.
|
|
type ServerHandler
|
|
= ServerCall -> ByteString -> MetadataMap
|
|
-> IO (ByteString, MetadataMap, C.StatusCode, StatusDetails)
|
|
|
|
-- TODO: we will want to replace this with some more general concept that also
|
|
-- works with streaming calls in the future.
|
|
-- | Wait for and then handle a normal (non-streaming) call.
|
|
serverHandleNormalCall :: Server
|
|
-> RegisteredMethod
|
|
-> TimeoutSeconds
|
|
-> MetadataMap
|
|
-- ^ Initial server metadata
|
|
-> ServerHandler
|
|
-> IO (Either GRPCIOError ())
|
|
serverHandleNormalCall s@Server{..} rm timeLimit initMeta f = do
|
|
withServerCall s rm timeLimit $ \call -> do
|
|
grpcDebug "serverHandleNormalCall(R): starting batch."
|
|
debugServerCall call
|
|
payload <- serverCallGetPayload call
|
|
case payload of
|
|
--TODO: what should we do with an empty payload? Have the handler take
|
|
-- @Maybe ByteString@? Need to figure out when/why payload would be empty.
|
|
Nothing -> error "serverHandleNormalCall(R): payload empty."
|
|
Just requestBody -> do
|
|
requestMeta <- serverCallGetMetadata call
|
|
(respBody, trailingMeta, status, details) <- f call
|
|
requestBody
|
|
requestMeta
|
|
let respOps = serverOpsSendNormalRegisteredResponse
|
|
respBody initMeta trailingMeta status details
|
|
respOpsResults <- runOps (unServerCall call) serverCQ respOps
|
|
grpcDebug "serverHandleNormalCall(R): finished response ops."
|
|
case respOpsResults of
|
|
Left x -> return $ Left x
|
|
Right _ -> return $ Right ()
|