packages feed

keiro-ops-http-0.22.0.0: src/Keiro/Ops/Http/Server.hs

module Keiro.Ops.Http.Server
  ( OpsHttpServer (..),
    startOpsHttpServer,
    stopOpsHttpServer,
    withOpsHttpServer,
  )
where

import Control.Concurrent.Async (Async, async, asyncThreadId, asyncWithUnmask, cancel, link)
import Control.Concurrent.MVar
import Control.Exception (bracket, bracketOnError, finally, mask, onException)
import Data.Map.Strict qualified as Map
import Data.Text qualified as Text
import Keiro.Ops.Http.Config
import Network.Socket qualified as Socket
import Network.Wai (Application)
import Network.Wai.Handler.Warp qualified as Warp

data OpsHttpServer = OpsHttpServer
  { serverThread :: !(Async ()),
    serverPort :: !Int
  }

startOpsHttpServer :: OpsHttpServerConfig -> Application -> IO OpsHttpServer
startOpsHttpServer config app
  | config.port < 0 || config.port > 65535 || config.requestTimeoutSeconds <= 0 = ioError (userError "invalid inspection server port or timeout")
  | otherwise = mask $ \restore -> do
      -- The runner owns the listener and every connection worker. Closing only
      -- the accept loop leaves keep-alive clients and their handlers alive.
      socket <- openSocket config
      actualPort <- fromIntegral <$> Socket.socketPort socket
      ready <- newEmptyMVar
      clients <- newMVar Map.empty
      let spawn :: ((forall a. IO a -> IO a) -> IO ()) -> IO ()
          spawn action = mask $ \_ -> do
            registered <- newEmptyMVar
            client <- asyncWithUnmask $ \unmask ->
              -- Warp must install its accepted-socket cleanup before waiting
              -- for registration. Its first unmask occurs inside that bracket.
              action (\io -> readMVar registered >> unmask io) `finally` do
                tid <- readMVar registered
                modifyMVar_ clients (\connections -> pure $! Map.delete tid connections)
            let tid = asyncThreadId client
            (modifyMVar_ clients (\connections -> pure $! Map.insert tid client connections) >> putMVar registered tid)
              `onException` (tryPutMVar registered tid >> cancel client)
          stopClients = do
            live <- modifyMVar clients (\connections -> pure (Map.empty, Map.elems connections))
            mapM_ cancel live
          settings = Warp.setFork spawn (Warp.setBeforeMainLoop (putMVar ready ()) (Warp.setTimeout config.requestTimeoutSeconds Warp.defaultSettings))
      worker <- async (restore (Warp.runSettingsSocket settings socket app) `finally` (Socket.close socket >> stopClients)) `onException` Socket.close socket
      (link worker >> restore (takeMVar ready)) `onException` (cancel worker >> Socket.close socket)
      pure (OpsHttpServer worker actualPort)

stopOpsHttpServer :: OpsHttpServer -> IO ()
stopOpsHttpServer server = cancel server.serverThread

withOpsHttpServer :: OpsHttpServerConfig -> Application -> (OpsHttpServer -> IO a) -> IO a
withOpsHttpServer config app = bracket (startOpsHttpServer config app) stopOpsHttpServer

openSocket :: OpsHttpServerConfig -> IO Socket.Socket
openSocket config = do
  let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream, Socket.addrFlags = [Socket.AI_PASSIVE]}
      host = if config.bindHost == "*" then Nothing else Just (Text.unpack config.bindHost)
  addresses <- Socket.getAddrInfo (Just hints) host (Just (show config.port))
  case addresses of
    [] -> ioError (userError "inspection bind host has no addresses")
    address : _ -> bracketOnError (Socket.socket (Socket.addrFamily address) Socket.Stream Socket.defaultProtocol) Socket.close $ \socket -> do
      Socket.setSocketOption socket Socket.ReuseAddr 1
      Socket.bind socket (Socket.addrAddress address)
      Socket.listen socket Socket.maxListenQueue
      pure socket