ecluse:ecluse-runtime
Safe HaskellNone
LanguageGHC2021

Ecluse.Runtime.Server

Description

The HTTP front door: the raw wai Application, its dispatch, the middleware stack, and runWarp. It is a raw Application rather than a framework because matching on pathInfo keeps the encoded-slash handling and the streaming control the proxy depends on (docs/architecture/web-layer.md). Dispatch matches a request's leading segments to a configured MountBinding, strips the prefix, and asks that mount's router what the remainder names, so this module holds no path grammar and no status of its own. A path under no mount is the neutral 404, and /livez and /readyz are answered above the mounts.

Synopsis

The WAI application

data ServerConfig Source #

The settings the web layer needs to serve that the composition-root Env does not carry. Not the request-body cap: the publish route bounds its own body as a value.

Constructors

ServerConfig 

Fields

  • scPort :: Int

    The TCP port warp listens on.

  • scMounts :: [MountBinding]

    The mounts served. The first whose prefix matches the request's leading segments wins, and a path under no mount is the neutral 404.

  • scDrain :: DrainSignal

    The shutdown-drain flag the front door observes. Once raised, the readiness probe fails and every response carries Connection: close. Defaults to neverDraining.

  • scDrainTimeout :: ShutdownDrainTimeout

    How long the graceful drain waits for in-flight requests and in-progress artifact streams to finish before the process exits (defaultShutdownDrainTimeout).

  • scCheckReady :: IO Readiness

    The readiness verdict the composition root installs, which /readyz renders and the drain check overrides. Each mount's flip is one way, so readiness never flaps a pod out of rotation.

  • scCheckLive :: IO Liveness

    The liveness check /livez answers from, beyond the listener itself. A worker heartbeat is wired here only when a worker runs, so a serve-only deployment stays live.

  • scOnException :: Maybe Request -> SomeException -> IO ()

    warp's exception hook, for a post-commit escape the request perimeter rethrew or a fault in warp's own connection handling. The mkServerConfig default is inert.

mkServerConfig :: [MountBinding] -> ServerConfig Source #

Build a ServerConfig over the mount bindings on defaultPort. There is no built-in mount: the web layer serves only the ecosystems the composition root binds here.

data MountBinding #

Constructors

MountBinding 

Fields

application :: ServerConfig -> Env -> Application Source #

The proxy's WAI Application: the request dispatch under the cross-cutting middleware stack (serverMiddleware).

tracedApplication :: ServerConfig -> Env -> IO Application Source #

application with the OpenTelemetry server-span middleware wrapped outermost, so one server span covers the whole request. The wrapper is id when telemetry is off.

Running the server

runWarp :: LogEnv -> Text -> ServerConfig -> (ServerConfig -> IO Application) -> IO () Source #

Serve the front door over one live DrainSignal, which the probe, the going-away header, and the shutdown handler share. warp drains under scDrainTimeout, and a TTY adds Ctrl-D.

proxyListener :: Text Source #

The proxy's listener name, which starts the line it logs once bound.

listeningPrefix :: Text -> Text Source #

The start of the line a named listener logs once bound, followed by the port.

raceServerAgainstLoop :: MonadUnliftIO m => m () -> m () -> m () Source #

Race a server arm against a never-returning background loop, the single-process shutdown shape. race_ is the invariant: concurrently_ would wait forever, brackets un-unwound.

probeApplication :: DrainSignal -> IO Readiness -> IO Liveness -> Application Source #

The control-plane health probes, answered above any mount: /livez from the injected liveness check, /readyz from the drain signal and startup gate. Any other path is a 404.

probeOnlyApplication :: ServerConfig -> IO Application Source #

The WAI Application of a role that serves the health probes and nothing else. Every path outside /livez and /readyz is the neutral 404.

Graceful shutdown

data DrainSignal Source #

The shutdown-drain flag the front door observes during a graceful rollover. Nothing lowers it again. The readiness probe and the going-away middleware read it per request.

newDrainSignal :: IO DrainSignal Source #

Allocate a live drain signal, lowered. runWarp allocates one per launch and raises it from the shutdown signal handler.

neverDraining :: DrainSignal Source #

The inert drain signal: permanently lowered, and raising it does nothing. It is the mkServerConfig default, so a socket-free test reports ready and stamps no going-away header.

beginDrain :: DrainSignal -> IO () Source #

Raise a drain signal: the one-way transition into draining. Idempotent.

isDraining :: DrainSignal -> IO Bool Source #

Read whether a drain signal is raised.

newtype ShutdownDrainTimeout Source #

The bound on the graceful drain, in seconds. The server stops accepting connections, waits this long for in-flight requests and artifact streams, then exits regardless.

defaultShutdownDrainTimeout :: ShutdownDrainTimeout Source #

The default graceful-drain bound: 30 seconds. It covers an in-flight metadata fetch or a moderate artifact stream, and stops a stuck request pinning the old instance.

Local-dev immediate halt

data InteractiveHalt Source #

The local-development immediate-halt wiring, as injection points a test drives without a terminal. Ctrl-D exits the process at once and aborts the drain. It is inert on a non-TTY.

Constructors

InteractiveHalt 

Fields

  • haltOnInteractive :: IO Bool

    Whether to arm the halt. A non-interactive process never installs the watcher.

  • awaitHaltSignal :: IO ()

    Block until the dev's halt signal. The real wiring reads standard input until end-of-input (Ctrl-D). It returns when the watcher should fire.

  • halt :: IO ()

    Stop the process immediately, bypassing the drain wait. The real wiring is a direct _exit (exitImmediately).

defaultInteractiveHalt :: InteractiveHalt Source #

The real local-dev halt: armed only on a terminal and fired by end-of-input, exiting at once without a drain. Status 130 is the conventional "terminated from the terminal" code.

withInteractiveHalt :: InteractiveHalt -> IO a -> IO a Source #

Run an action with the immediate-halt watcher armed only when haltOnInteractive is True. The watcher lives exactly as long as the action, so it never outlives it.

Middleware

serverMiddleware :: ServerConfig -> Middleware Source #

The cross-cutting stack around the proxy Application. The body cap is not a middleware: it would throw across the request perimeter, and Autohead and Gzip fight streaming.