packages feed

grapesy-1.2.0: src/Network/GRPC/Util/Session/Server.hs

-- | Node with server role (i.e., its peer is a client)
module Network.GRPC.Util.Session.Server (
    ConnectionToClient(..)
  , setupResponseChannel
  ) where

import Control.Concurrent
import Control.Exception
import Network.HTTP.Semantics qualified as HTTP
import Network.HTTP.Semantics.Server qualified as Server

import Network.GRPC.Common.Exception
import Network.GRPC.Util.Imports
import Network.GRPC.Util.ServerStream
import Network.GRPC.Util.Session.API
import Network.GRPC.Util.Session.Channel
import Network.GRPC.Util.Stream
import Network.GRPC.Util.Thread

{-------------------------------------------------------------------------------
  Connection
-------------------------------------------------------------------------------}

-- | Connection to the client, as provided by @http2@
data ConnectionToClient = ConnectionToClient {
      request :: Server.Request
    , respond :: Server.Response -> IO ()
    }

{-------------------------------------------------------------------------------
  Internal auxiliary: constructing responses
-------------------------------------------------------------------------------}

respondStreamingWithResult :: forall a.
     HasCallStack
  => ConnectionToClient
  -> Server.TrailersMaker
  -> ResponseInfo
  -> (OutputStream -> IO a)
  -> IO a
respondStreamingWithResult conn trailers responseInfo body = do
    resultVar :: MVar (Either ExactException a) <- newEmptyMVar
    let resp :: Server.Response
        resp = flip Server.setResponseTrailersMaker trailers
             . Server.responseStreamingIface
                 (responseStatus  responseInfo)
                 (responseHeaders responseInfo)
             $ auxThreadBody resultVar

    respond conn resp

    -- Any exception thrown here is thrown in the context of a 'Thread'.
    either throwExact return =<< takeMVar resultVar
  where
    -- This will be running in an auxiliary thread, spawned by http2. Any
    -- exceptions that are thrown by that thread will remain uncaught, and
    -- will trigger the top-level uncaught exception handler. We therefore
    -- catch all of these and store them in 'resultVar', which the main
    -- grapesy outbound thread is waiting on.
    --
    -- When the connection is closed, this thread will be cleaned up by
    -- the ThreadManager in http2.
    auxThreadBody ::
         MVar (Either ExactException a)
      -> HTTP.OutBodyIface
      -> IO ()
    auxThreadBody resultVar iface = do
        result <- try $ do
          -- It is important we create the output stream inside the body of the
          -- exception handler, since that too can fail.
          ostrm <- serverOutputStream iface
          body ostrm
        putMVar resultVar result

respondNoBody :: ConnectionToClient -> ResponseInfo -> IO ()
respondNoBody conn responseInfo =
    respond conn resp
  where
    resp :: Server.Response
    resp = Server.responseNoBody
             (responseStatus  responseInfo)
             (responseHeaders responseInfo)

{-------------------------------------------------------------------------------
  Initiate response
-------------------------------------------------------------------------------}

-- | Setup response channel
--
-- Notes:
--
-- * The actual response will not immediately be initiated; see below.
-- * We assume that the client is allowed to close their outbound stream to us.
-- * 'setupResponseChannel' will not throw any exceptions.
setupResponseChannel :: forall sess.
     (HasCallStack, IsSession sess)
  => sess
  -> ConnectionToClient
  -> FlowStart (Inbound sess)
  -> IO (FlowStart (Outbound sess), ResponseInfo)
  -- ^ Construct headers for the initial response
  --
  -- This function is allowed to block. If it does, no response will not be
  -- initiated until it returns.
  --
  -- If this function throws an exception, the response is never initiated;
  -- this is treated the same was as when we fail to set up the outbound
  -- connection due to a network failure.
  -> IO (Channel sess)
setupResponseChannel sess
                     conn
                     inboundStart
                     startOutbound
                   = do
    channel <- initChannel "server"
    monitorInbound channel

    forkThread "grapesy:serverInbound" (channelInbound channel) $ \unmask ctxt -> unmask $
      case inboundStart of
        FlowStartRegular headers -> do
          regular <- initFlowStateRegular headers
          stream  <- serverInputStream (request conn)
          threadMainBody ctxt regular $ \markDone ->
            markDone =<< recvMessageLoop sess regular stream
        FlowStartNoMessages trailers ->
          -- The client sent a request with an empty body
          threadTrivial ctxt trailers

    forkThread "grapesy:serverOutbound" (channelOutbound channel) $ \unmask ctxt -> unmask $ do
      (outboundStart, responseInfo) <- startOutbound
      case outboundStart of
        FlowStartRegular headers -> do
          regular <- initFlowStateRegular headers
          threadMainBody ctxt regular $ \markDone -> do
            respondStreamingWithResult
                conn
                (outboundTrailersMaker sess channel regular)
                responseInfo $ \stream ->
              sendMessageLoop sess regular stream markDone
        FlowStartNoMessages trailers -> do
          respondNoBody conn responseInfo
          threadTrivial ctxt trailers

    return channel

-- | Make outbound thread monitor the input thread
--
-- Monitoring here is a bit subtle: http2 will spawn an auxiliary thread, which
-- will be the one that will actually run the 'sendMessageLoop'. Meanwhile, our
-- \"outbound thread\" will simply be waiting for that auxiliary thread to
-- terminate; if the monitor fires, is it /this/ thread that receives it, not
-- the auxiliary http2 thread.
monitorInbound :: forall sess. Channel sess -> IO ()
monitorInbound channel = do
    _monitorRef <-
      threadMonitor
        (channelOutbound channel)
        (channelInbound channel)
        monitorPred
    return ()
  where
    -- Unlike on the client-side, if the input thread terminates normally, the
    -- server can continue to run normally (the stream can be half-closed from
    -- the client). We only only terminate if the inbound thread threw an
    -- exception, indicating that the client disconnected abruptly.
    --
    -- See also 'Network.GRPC.Util.Session.Client.setupRequestChannel'.
    monitorPred ::
          Either
            ThreadException
            ( Either
                (NoMessages (Inbound sess))
                (Trailers   (Inbound sess))
            )
       -> Maybe ExactException
    monitorPred = \case
        Left  e         -> Just $ threadException e
        Right _trailers -> Nothing