-module(livery_drain). -moduledoc """ Graceful shutdown. `drain/1,2` stops a running service the polite way: it stops the listeners from accepting new connections, waits for the requests already in flight to finish within a configurable window, then stops the service. Returns `ok` once fully drained or `{error, timeout}` if the window elapsed with requests still running (the service is stopped either way). In-flight requests are counted node-wide via the global `livery_req_sup` in-flight counter — every request, on every protocol, runs in a `livery_req_proc` worker it tracks. A single-service node drains exactly its own requests; on a multi-service node `drain/2` waits for all of them. Stopping acceptance closes the listen socket (no new connections); it does not send GOAWAY on existing keep-alive connections. ```erlang {ok, Pid} = livery:start_service(#{http => #{port => 8080}, router => R}), %% ... serve ... ok = livery:drain(Pid, #{timeout => 30000}). ``` """. -export([drain/1, drain/2, await/0, await/1, in_flight/0]). -export_type([opts/0]). -type opts() :: #{ timeout => timeout(), poll_interval => non_neg_integer() }. -define(DEFAULT_TIMEOUT, 30000). -define(DEFAULT_POLL, 100). %%==================================================================== %% Public API %%==================================================================== -doc "Gracefully drain and stop a service with default options.". -spec drain(pid()) -> ok | {error, timeout}. drain(Service) -> drain(Service, #{}). -doc """ Gracefully drain and stop a service. Stops accepting new connections, waits up to `timeout` (default 30s) for in-flight requests to finish, then stops the service. The service is stopped regardless of whether the drain completed. """. -spec drain(pid(), opts()) -> ok | {error, timeout}. drain(Service, Opts) when is_pid(Service) -> ok = livery_service:stop_accepting(Service), Outcome = await(Opts), ok = livery_service:stop(Service), Outcome. -doc "Wait for in-flight requests to finish, default 30s window.". -spec await() -> ok | {error, timeout}. await() -> await(#{}). -doc """ Wait until no requests are in flight, or the timeout elapses. `Opts`: `timeout` (default 30000 ms; `infinity` allowed) and `poll_interval` (default 100 ms). """. -spec await(opts()) -> ok | {error, timeout}. await(Opts) -> Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT), Interval = maps:get(poll_interval, Opts, ?DEFAULT_POLL), Deadline = deadline(Timeout), wait_loop(Deadline, Interval). -doc "Number of requests currently in flight (0 if the app is down).". -spec in_flight() -> non_neg_integer(). in_flight() -> livery_req_sup:in_flight(). %%==================================================================== %% Internals %%==================================================================== -spec deadline(timeout()) -> integer() | infinity. deadline(infinity) -> infinity; deadline(Ms) -> erlang:monotonic_time(millisecond) + Ms. -spec wait_loop(integer() | infinity, non_neg_integer()) -> ok | {error, timeout}. wait_loop(Deadline, Interval) -> case in_flight() of 0 -> ok; _N -> case expired(Deadline) of true -> {error, timeout}; false -> timer:sleep(Interval), wait_loop(Deadline, Interval) end end. -spec expired(integer() | infinity) -> boolean(). expired(infinity) -> false; expired(Deadline) -> erlang:monotonic_time(millisecond) >= Deadline.