packages feed

grapesy-1.2.2: 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
import Network.GRPC.Util.GHC

{-------------------------------------------------------------------------------
  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 = mask $ \unmask -> do
        labelThisThread "grapesy:respondStreamingWithResult:auxThreadBody"
        result <- try $ unmask $ 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
        labelThisThread "grapesy:respondStreamingWithResult:auxThreadBody:done"

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