@molecule/api-process-lifecycle-express
Provider bond · process-lifecycle · API (Node) · v1.3.3 · Apache-2.0
Express/Node process-lifecycle provider: GET /health/live and /health/ready, request tracking with Connection: close while draining, and SIGTERM/SIGINT graceful shutdown with ordered hooks.
npm install @molecule/api-process-lifecycle-expressnpm · Source on GitHub · Implements @molecule/api-process-lifecycle
How it works
@molecule/api-process-lifecycle-express is a provider bond on the API (Node) side: it implements the process-lifecycle core interface (@molecule/api-process-lifecycle) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { installExpressLifecycle } from '@molecule/api-process-lifecycle-express'
import { registerReadinessCheck, registerShutdownHook } from '@molecule/api-process-lifecycle'
const app = express()
const lifecycle = installExpressLifecycle({ app }) // mounts /health/live + /health/ready
registerReadinessCheck({ name: 'database', check: async () => db.ping() })
registerShutdownHook({ name: 'database-pool', run: () => db.pool.end() })
const server = app.listen(4000, () => lifecycle.markReady())
lifecycle.attachServer(server)Works with: @molecule/api-config, @molecule/api-process-lifecycle
Reference
Express/Node provider for @molecule/api-process-lifecycle.
Gives a scaffolded API the operational surface a rolling or blue/green deploy needs out of the box:
GET /health/live— is the process alive (200 while draining too).GET /health/ready— may a load balancer route here (503 while draining, or when a critical readiness check fails).- Request tracking with
Connection: closeon responses while draining. - SIGTERM/SIGINT handlers that drain in-flight requests (default bound 25 s,
SHUTDOWN_TIMEOUT_MS; load-balancer flip waitSHUTDOWN_LB_FLIP_WAIT_MS), run ordered shutdown hooks (database pool, Redis, queues), and exit 0.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { installExpressLifecycle } from '@molecule/api-process-lifecycle-express'
import { registerReadinessCheck, registerShutdownHook } from '@molecule/api-process-lifecycle'
const app = express()
const lifecycle = installExpressLifecycle({ app }) // mounts /health/live + /health/ready
registerReadinessCheck({ name: 'database', check: async () => db.ping() })
registerShutdownHook({ name: 'database-pool', run: () => db.pool.end() })
const server = app.listen(4000, () => lifecycle.markReady())
lifecycle.attachServer(server)
Type
provider
Installation
npm install @molecule/api-process-lifecycle-express @molecule/api-config @molecule/api-process-lifecycle express
npm install -D @types/express
API
Interfaces
HealthcheckDeclaration
The load-balancer health check the application declares, in milliseconds.
Fly does NOT expose its check interval or timeout as environment variables —
the Machine runtime environment carries only FLY_APP_NAME,
FLY_MACHINE_ID, FLY_REGION, FLY_PUBLIC_IP, FLY_PRIVATE_IP,
FLY_IMAGE_REF, FLY_MACHINE_VERSION, FLY_PROCESS_GROUP and
FLY_VM_MEMORY_MB — so an application (or its deployment config) mirrors
[http_service.checks] into these two variables. Without them the correct
flip window cannot be derived and the package falls back to its static
default.
interface HealthcheckDeclaration {
/** How often the load balancer probes `/health/ready`, when declared. */
intervalMs?: number
/** The probe's timeout, when declared. Treated as `0` when absent. */
timeoutMs?: number
}
ClosableServer
The subset of an HTTP server the graceful-shutdown installer needs.
Declared structurally so tests (and non-http.Server wrappers) can pass a
double without constructing a real listener.
interface ClosableServer {
/**
* Stop accepting new connections.
*
* @param callback - Optional completion callback.
*/
close(callback?: (error?: Error) => void): unknown
/** Close idle keep-alive connections (Node 18.2+). */
closeIdleConnections?(): void
/** Force-close every remaining connection (Node 18.2+). */
closeAllConnections?(): void
}
LifecycleLogger
The logging surface the provider uses. Every method is optional so a caller
can pass a partial logger (or nothing, in which case console is used).
interface LifecycleLogger {
/** Informational lifecycle transitions (draining, shutdown complete). */
info(message: string, context?: Record<string, unknown>): void
/** Recoverable problems (a drain timeout, a failed hook). */
warn(message: string, context?: Record<string, unknown>): void
/** Genuine failures during shutdown. */
error(message: string, context?: unknown): void
/** Best-effort detail that should stay quiet by default. */
debug(message: string, context?: Record<string, unknown>): void
}
ProcessLifecycleOptions
Options for createProcessLifecycleProvider.
interface ProcessLifecycleOptions {
/** Timeout applied to a check that declares no `timeoutMs`. Defaults to `2000`. */
defaultCheckTimeoutMs?: number
/** Timeout applied to a shutdown hook that declares no `timeoutMs`. Defaults to `10000`. */
defaultHookTimeoutMs?: number
/**
* Timeout applied to a warm-up step that declares no `timeoutMs`.
* Defaults to `45000` — a safety valve, not an expectation: normal warm-ups
* settle in seconds.
*/
defaultWarmupStepTimeoutMs?: number
/**
* Consecutive passing readiness probes required before readiness answers
* `true`. Defaults to `2`, so an instance is not handed traffic on the first
* check it manages to answer after a restart.
*/
consecutiveReadyPasses?: number
/** Clock used for durations. Defaults to `Date.now` (tests may inject a fake clock). */
now?: () => number
/** Called after a shutdown hook throws or times out, in addition to the error log. */
onHookError?: (name: string, error: unknown) => void
/** Logger used for hook failures. Defaults to `@molecule/api-logger` when bonded. */
logger?: LifecycleLogger
}
GracefulShutdownOptions
Options for installGracefulShutdown.
interface GracefulShutdownOptions {
/** The listening HTTP(S) server whose connections are drained. */
server: ClosableServer
/** The lifecycle provider that owns drain state, request count and hooks. */
provider: ProcessLifecycleProvider
/**
* Maximum time to wait for in-flight requests after `server.close()`.
* Defaults to `SHUTDOWN_TIMEOUT_MS` (read through `@molecule/api-config`
* when bonded, else `process.env`), falling back to `25000`.
*/
drainTimeoutMs?: number
/**
* Time to keep listening after draining starts, so a load balancer can
* observe `/health/ready` returning 503 before the listener closes. Defaults
* to `SHUTDOWN_LB_FLIP_WAIT_MS` (falling back to `5000` in production and
* `0` elsewhere).
*/
flipWaitMs?: number
/**
* How to end the process. Defaults to `process.exit`. Injected in tests so a
* signal can be exercised without terminating the test runner.
*
* @param code - Exit code (`0` for a drained shutdown, `1` for a failed or forced one).
*/
exit?: (code: number) => void
/** Signals to handle. Defaults to `['SIGTERM', 'SIGINT']`. */
signals?: NodeJS.Signals[]
/** Logger override. */
logger?: LifecycleLogger
}
GracefulShutdownHandle
The handle returned by installGracefulShutdown.
interface GracefulShutdownHandle {
/**
* Run the shutdown sequence as a signal would.
*
* @param signal - Label recorded in logs and the shutdown report (defaults to `'SIGTERM'`).
*/
stop(signal?: string): Promise<void>
/** Remove the installed signal listeners. Used by tests after `stop()`. */
uninstall(): void
}
ExpressLifecycleOptions
Options for installExpressLifecycle.
interface ExpressLifecycleOptions extends Omit<GracefulShutdownOptions, 'server' | 'provider'> {
/** The Express app to mount request tracking and health routes on. */
app: express.Express
/**
* The real HTTP(S) server, when it already exists. When omitted, call
* `ExpressLifecycleHandle.attachServer` once it does.
*/
server?: ClosableServer
/** A provider to use instead of a fresh one (tests, or a pre-bonded instance). */
provider?: ProcessLifecycleProvider
/** Mount `GET /health/live` and `GET /health/ready`. Defaults to `true`. */
mountRoutes?: boolean
/**
* Deployment-specific headers on a readiness response that is already 503
* because the process is draining. Never called for boot/check failures or
* liveness. A proxy may hand this refused request to a serving instance.
*
* The caller owns request eligibility and platform routing semantics. No
* application handler has executed at this point; defaults to no handoff.
*
* @param request - The refused readiness request.
* @returns Trusted response headers, or undefined to preserve ordinary 503.
*/
drainingResponseHeaders?: (
request: express.Request,
) => Readonly<Record<string, string>> | undefined
/**
* Bond the provider through `setProvider()` so `@molecule/api-process-lifecycle`
* accessors resolve to it. Defaults to `true`.
*/
wireProvider?: boolean
/** Install the SIGTERM/SIGINT handlers once a server is attached. Defaults to `true`. */
installSignalHandlers?: boolean
}
ExpressLifecycleHandle
The handle returned by installExpressLifecycle.
interface ExpressLifecycleHandle {
/** The provider backing this app. */
provider: ProcessLifecycleProvider
/** The Express middleware that tracks in-flight requests and sets `Connection: close` while draining. */
middleware: express.RequestHandler
/** Mark the process ready. Call from the server's `listening` callback. */
markReady(): void
/**
* Attach the listening server, installing the signal handlers.
*
* @param server - The real HTTP(S) server.
*/
attachServer(server: ClosableServer): void
/**
* Run the shutdown sequence as a signal would.
*
* @param signal - Label recorded in logs and the shutdown report.
*/
stop(signal?: string): Promise<void>
/** Remove the installed signal listeners. */
uninstall(): void
}
LivenessResponse
The JSON body returned by GET /health/live.
interface LivenessResponse {
/** `'live'` when the process is alive. */
status: 'live' | 'not_live'
/** Milliseconds since the provider was created. */
uptimeMs: number
/** Per-check results. */
checks: LifecycleCheckReport[]
}
ReadinessResponse
The JSON body returned by GET /health/ready.
interface ReadinessResponse {
/** `'ready'`, `'draining'` or `'not_ready'`. */
status: 'ready' | 'draining' | 'not_ready'
/** The lifecycle phase the report was computed in. */
state: string
/** `true` once the boot warm-up has settled. */
warm: boolean
/** Number of requests being served when the report was computed. */
inFlightRequests: number
/** Per-check results. Empty while draining. */
checks: LifecycleCheckReport[]
}
WarmupGateOptions
Options for createWarmupGate.
interface WarmupGateOptions {
/** Default per-step timeout when a step declares none. Defaults to 45 s. */
timeoutMs?: number
/** Passing probes required in a row before readiness passes. Defaults to 2. */
consecutivePasses?: number
/** Clock used for durations. Defaults to `Date.now`. */
now?: () => number
/** Logger for failed and timed-out steps. Defaults to `console`. */
logger?: WarmupGateLogger
}
WarmupGateRunOptions
Options for one WarmupGate.run call (tests and one-off overrides).
interface WarmupGateRunOptions {
/** Default per-step timeout for this pass. Overrides the gate's value. */
timeoutMs?: number
/** Logger for this pass. Overrides the gate's logger. */
logger?: WarmupGateLogger
}
WarmupGate
The warm-up gate: a one-shot pass over named steps, plus the consecutive passing-probe counter that decides when readiness may answer 200.
interface WarmupGate {
/**
* Register a step to run on every `WarmupGate.run` call.
*
* @param step - The step to register.
* @returns A function that removes the step.
*/
registerStep(step: WarmupStep): () => void
/**
* Run the registered steps (plus any one-off `extraSteps`) once.
*
* Idempotent: a second call joins the in-flight pass. Never rejects.
*
* @param extraSteps - One-off steps, run after the registered ones.
* @param options - Per-pass timeout and logger overrides.
* @returns The pass report.
*/
run(extraSteps?: WarmupStep[], options?: WarmupGateRunOptions): Promise<WarmupReport>
/** Whether the pass has settled (every step completed or timed out). */
isSettled(): boolean
/** Mark the pass settled without running it (tests, or nothing to warm). */
markSettled(): void
/**
* Record one readiness observation.
*
* @param passing - Whether every other readiness gate passed for this probe.
* @returns `true` when the gate is settled and N consecutive passes were seen.
*/
observe(passing: boolean): boolean
/** Forget everything: a new process instance has not warmed up yet. */
reset(): void
}
ProcessLifecycleProvider
The contract every process-lifecycle provider implements.
Application code reaches the bonded provider through the accessors in
@molecule/api-process-lifecycle (setProvider, getProvider,
registerReadinessCheck, isReady, …) rather than importing a provider.
interface ProcessLifecycleProvider {
/** The current lifecycle phase. */
state(): ProcessLifecycleState
/** Move from `starting` to `ready` once the boot sequence has finished. */
markReady(): void
/** `true` while draining or stopped. */
isDraining(): boolean
/** Begin draining: readiness reports not-ready from this call onward. Idempotent. */
startDraining(reason?: string): void
/**
* Record that a request started. The returned function releases the
* request; it is idempotent so a `finish`/`close` pair cannot double-count.
*/
beginRequest(): () => void
/** Number of requests currently being served. */
inFlightRequests(): number
/**
* Resolve `true` when the in-flight count reaches zero, or `false` when
* `timeoutMs` elapses first.
*/
waitForRequestsToDrain(timeoutMs: number): Promise<boolean>
/**
* Register a named readiness check.
*
* @returns A function that removes the check.
*/
registerReadinessCheck(check: LifecycleCheck): () => void
/**
* Register a named liveness check.
*
* @returns A function that removes the check.
*/
registerLivenessCheck(check: LifecycleCheck): () => void
/**
* Register an ordered shutdown hook.
*
* @returns A function that removes the hook.
*/
registerShutdownHook(hook: ShutdownHook): () => void
/**
* Register a named boot warm-up step.
*
* Steps registered before `ProcessLifecycleProvider.markReady` run as
* one pass when it is called; readiness stays closed until every step has
* completed or timed out.
*
* @returns A function that removes the step.
*/
registerWarmupStep(step: WarmupStep): () => void
/**
* Run every registered warm-up step once.
*
* Idempotent: a second call joins the in-flight pass instead of running the
* work again. Never rejects — a failing or hanging step is reported.
*
* @returns The per-step warm-up report.
*/
runWarmup(): Promise<WarmupReport>
/**
* Whether the boot warm-up has settled (every step completed or timed out).
*
* @returns `true` once the warm-up pass has finished.
*/
isWarm(): boolean
/**
* Register a drain hook, run at the start of a shutdown before the
* load-balancer flip window.
*
* @returns A function that removes the hook.
*/
registerDrainHook(hook: DrainHook): () => void
/**
* Run every drain hook in order, then resolve. Never rejects.
*
* The graceful-shutdown sequence calls this BEFORE its flip window and runs
* the window even when a hook fails.
*
* @param reason - Why the shutdown started (usually a signal name).
* @returns The per-hook drain report.
*/
runDrainHooks(reason?: string): Promise<DrainHookReport[]>
/** Run every readiness check and report whether traffic may be routed here. */
isReady(): Promise<ReadinessReport>
/** Run every liveness check and report whether the process is alive. */
isLive(): Promise<LivenessReport>
/** Run every shutdown hook in order, then mark the process stopped. Idempotent. */
runShutdownHooks(reason?: string): Promise<ShutdownReport>
}
Types
WarmupGateLogger
The logging surface the gate needs; a full LifecycleLogger satisfies it.
type WarmupGateLogger = Pick<LifecycleLogger, 'warn' | 'error'>
Functions
applyDrainingResponseHeaders(provider, res, headers)
Set Connection: close on a response while the process is draining.
Use handoff headers only on a response that refuses the request before any application operation runs. A routing proxy may execute that request elsewhere. This function never changes the HTTP status, body or readiness verdict.
function applyDrainingResponseHeaders(
provider: ProcessLifecycleProvider,
res: express.Response,
headers?: Readonly<Record<string, string>>,
): void
provider— The lifecycle provider that owns the drain state.res— The response being prepared.headers— Optional deployment-specific response headers for a refused request.
createDrainHeaderMiddleware(provider)
Mark responses to requests arriving while draining Connection: close.
An application that keeps its own request counting (a server that predates
this package's middleware) mounts this instead of
createLifecycleMiddleware. State is checked at request entry. The
header tells the peer not to reuse this connection; routing withdrawal is
the load balancer's responsibility. Mount this before every route.
function createDrainHeaderMiddleware(provider: ProcessLifecycleProvider): express.RequestHandler
provider— The lifecycle provider that owns the drain state.
Returns: An Express request handler.
createLifecycleMiddleware(provider)
Create the Express middleware that tracks in-flight requests.
The middleware counts the request for the whole response lifetime
(finish/close), so a slow handler or an SSE stream is visible to the
drain. Requests arriving while draining get Connection: close, telling
the client (and any proxy reusing a keep-alive socket) not to send another
request on this connection.
function createLifecycleMiddleware(provider: ProcessLifecycleProvider): express.RequestHandler
provider— The lifecycle provider that owns the counter and drain state.
Returns: An Express request handler.
mountLifecycleRoutes(app, provider, options)
Mount GET /health/live and GET /health/ready on an Express app.
/health/liveanswers 200 while the process is alive, including while draining — a liveness probe must not restart a process that is finishing its in-flight work./health/readyanswers 200 only when the process is ready, the boot warm-up has settled, consecutive probes have passed, and every critical check passes; 503 while draining (withConnection: close), while the warm-up runs, or when a critical check fails. The body'swarmfield says whether the warm-up is what is holding the instance back.
Both responses are Cache-Control: no-store.
function mountLifecycleRoutes(
app: express.Express,
provider: ProcessLifecycleProvider,
options?: Pick<ExpressLifecycleOptions, 'drainingResponseHeaders'>,
): void
app— The Express app to mount the routes on.provider— The lifecycle provider that computes the reports.options— Optional headers for readiness responses while draining.
installExpressLifecycle(options)
Wire the whole Express lifecycle in one call: bond a provider (unless one is supplied), mount request tracking and health routes, and — once a server is attached — install SIGTERM/SIGINT graceful shutdown.
Call this before mounting application routes so every request is counted.
function installExpressLifecycle(options: ExpressLifecycleOptions): ExpressLifecycleHandle
options— The app, optional server/provider, bounds, logger and exit function.
Returns: A handle exposing the provider, middleware, markReady, attachServer, stop and uninstall.
createProcessLifecycleProvider(options)
Create a process-lifecycle provider.
The returned provider is the implementation half of the core contract: it holds the lifecycle phase, runs readiness/liveness checks with per-check timeouts, counts in-flight requests, and runs shutdown hooks in order.
function createProcessLifecycleProvider(options?: ProcessLifecycleOptions): ProcessLifecycleProvider
options— Provider tuning (timeouts, clock, logger, hook-error callback).
Returns: A fresh, independent provider.
resolveLifecycleNumber(key, fallback)
Read a numeric lifecycle setting.
Precedence: the bonded @molecule/api-config provider (when one is wired,
so a Vault/SSM provider is honored), then process.env, then the fallback.
A non-numeric value is ignored rather than turning into NaN.
function resolveLifecycleNumber(key: string, fallback: number): number
key— The configuration key (e.g.'SHUTDOWN_TIMEOUT_MS').fallback— Value used when neither source provides a number.
Returns: The resolved number.
readLifecycleNumber(key)
Read an optional numeric lifecycle setting.
Same precedence as resolveLifecycleNumber (bonded config provider,
then process.env), but an absent or non-numeric value stays undefined so
a caller can tell "not declared" from "declared as zero".
function readLifecycleNumber(key: string): number | undefined
key— The configuration key.
Returns: The resolved number, or undefined when the key is not declared.
resolveHealthcheckDeclaration()
Read the declared load-balancer check from LB_HEALTHCHECK_INTERVAL_MS and
LB_HEALTHCHECK_TIMEOUT_MS.
function resolveHealthcheckDeclaration(): HealthcheckDeclaration
Returns: The declaration; both fields are absent when nothing is declared.
derivedFlipWaitMs(declaration)
The flip window a declared check needs: three intervals plus the timeout plus a 2 s margin.
Three intervals, not two: Fly does not document whether its proxy flips after two or three consecutive failed checks, and the measured 2026-10-19 restart showed a 5 s window was too short for a 30 s interval.
function derivedFlipWaitMs(declaration: HealthcheckDeclaration): number
declaration— The declared check.
Returns: The derived window in milliseconds, or 0 when no interval is declared.
resolveFlipWaitMs(args)
The effective load-balancer flip window for this process.
Precedence: an explicit option, then SHUTDOWN_LB_FLIP_WAIT_MS, then a
value derived from the declared check, then the static default (5 s in
production, 0 elsewhere).
function resolveFlipWaitMs(args?: {
flipWaitMs?: number
declaration?: HealthcheckDeclaration
}): number
args— The option override and an already-resolved declaration.
Returns: The window in milliseconds.
checkHealthcheckConsistency(args)
Check the flip window against the declared health check.
A flip window shorter than 2 x interval + timeout cannot give the load
balancer time to observe two consecutive failed checks before the listener
closes, so a rolling restart drops requests. The message names both numbers
and the fix; the caller logs it once, at boot.
function checkHealthcheckConsistency(args: {
declaration: HealthcheckDeclaration
flipWaitMs: number
}): string | null
args— The declaration and the effective flip window.
Returns: The warning message, or null when the window is long enough.
installGracefulShutdown(options)
Install SIGTERM/SIGINT handlers that drain and shut the process down cleanly.
The sequence, in order:
provider.startDraining(signal)—/health/readyanswers 503 from this instant, so a load balancer stops routing here.- Run the registered drain hooks (bounded, never fatal). A hook that throws does not skip step 3.
- Wait
flipWaitMswhile the listener is still open, giving the load balancer time to observe the 503 before the connection is refused. The window is derived fromLB_HEALTHCHECK_INTERVAL_MS/LB_HEALTHCHECK_TIMEOUT_MSwhen those are declared, and a window too short for them logs one warning at boot. closeIdleConnections()thenserver.close()— no new connections are accepted, and a proxy holding an idle keep-alive socket must open a new one; responses already in flight carryConnection: close.- Wait up to
drainTimeoutMsfor the in-flight count to reach zero. If the bound expires (an SSE/WebSocket stream, a stuck handler), log it andcloseAllConnections(). - Run the ordered shutdown hooks (database pool, Redis, queues, …).
- Exit
0.
A second signal while the first is still draining exits 1 immediately
rather than stacking a second drain. The handlers never throw.
function installGracefulShutdown(options: GracefulShutdownOptions): GracefulShutdownHandle
options— The server, provider, bounds, logger and exit function.
Returns: A handle exposing the same sequence (stop) and uninstall.
createWarmupGate(options)
Create a warm-up gate.
function createWarmupGate(options?: WarmupGateOptions): WarmupGate
options— Timeout, consecutive-pass count, clock and logger.
Returns: A fresh, independent gate.
Constants
provider
The process-lifecycle provider singleton for this process.
Exported so a bond can be wired with the conventional
import { provider } from '@molecule/api-process-lifecycle-express' shape.
Applications that need an isolated instance (tests, embedded servers) call
createProcessLifecycleProvider instead.
const provider: ProcessLifecycleProvider
DEFAULT_WARMUP_TIMEOUT_MS
How long a warmed step may take when it declares no timeoutMs.
const DEFAULT_WARMUP_TIMEOUT_MS: 45000
DEFAULT_CONSECUTIVE_READY_PASSES
Passing probes required in a row before readiness answers 200.
Two (not one) because a single pass can be a moment between two heavy imports: with a load balancer polling every few seconds, two in a row means the event loop has actually had to schedule the probe.
const DEFAULT_CONSECUTIVE_READY_PASSES: 2
Core Interface
Implements @molecule/api-process-lifecycle interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-process-lifecycle'
import { provider } from '@molecule/api-process-lifecycle-express'
export function setupProcessLifecycleExpress(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-config^1.0.1@molecule/api-process-lifecycle^1.0.0express^4.0.0 || ^5.0.0
Runtime Dependencies
-
@molecule/api-config -
@molecule/api-process-lifecycle -
express -
Mount
installExpressLifecyclebefore application routes so every request is counted by the drain. -
Readiness and liveness are deliberately different: liveness stays 200 while draining, so a supervisor does not restart a process that is finishing in-flight work.
-
A deployment may supply
drainingResponseHeadersto hand a refused readiness request to its routing proxy. For example, Fly's documentedFly-Replay: elsewhere=trueexcludes the responding Machine. Direct probes still receive 503; this does not mark an unready process ready. Only configure replay for requests the platform supports, before any application operation executes. A custom readiness handler can callapplyDrainingResponseHeaderson its existing refused response. -
A second signal during a drain exits immediately with code 1; the handlers never throw.