Skip to main content

didbot_serve/
lib.rs

1//! HTTP surface and the development server.
2//!
3//! This crate is the outside of the system: it turns provisioning into
4//! [`axum`] routes, and turns what the provisioner does into a log a developer
5//! can read while it happens.
6//!
7//! The whole surface is built around one object-safe trait,
8//! [`didbot_pds::Registry`]. The provisioner is generic over its DNS provider and its
9//! account store; the HTTP layer must not be, or every handler signature
10//! carries type parameters that no route cares about. `Arc<dyn Registry>` is
11//! where that genericity stops.
12//!
13//! # Running it
14//!
15//! The `didbot-pds` binary in this crate wires a development stack --
16//! loopback DNS, an in-memory store -- and serves it, so that one command
17//! produces a server for `didbot-swarm` to drive:
18//!
19//! ```text
20//! cargo run -p didbot-serve --bin didbot-pds
21//! ```
22//!
23//! # Routes
24//!
25//! | Route | Purpose |
26//! | --- | --- |
27//! | `GET /` | on the server's own hostname, a page naming it, its zones and its claim; elsewhere the same 404 as any unserved path |
28//! | `GET /health` | liveness, and which zone this server mints under |
29//! | `GET /.well-known/did.json` | the DID document for the request's Host |
30//! | `GET /.well-known/atproto-did` | the DID of the account whose handle is the request's Host, 404 when nobody holds it |
31//! | `POST /xrpc/bot.did.createAccount` | create an account beneath the creator the credential names |
32//! | `POST /xrpc/bot.did.createSession` | a session as a hosted account, from a key-signed JWT or an OpenID Connect token |
33//! | `POST /xrpc/bot.did.parkKey` | hold a public key for minutes, for the operator to admit under a name |
34//! | `GET /xrpc/bot.did.describeParkedKey` | the key parked under a name |
35//! | `POST /xrpc/bot.did.deleteAccount` | erase one: data gone, identity kept |
36//! | `POST /xrpc/bot.did.freezeAccount`, `unfreezeAccount` | hang or lift the account's own `frozen` lock |
37//! | `POST /xrpc/bot.did.deactivateAccount`, `activateAccount` | hang or lift the account's own `deactivated` lock |
38//! | `GET /xrpc/bot.did.listAccounts` | list them |
39//! | `POST /xrpc/bot.did.pollOperatorClaim` | ask this server to look for its operator's claim now — no credential, because the nudge confers nothing; see `auth` |
40//! | `POST /xrpc/com.atproto.repo.createRecord` | write a record to a repository |
41//! | `POST /xrpc/com.atproto.repo.putRecord` | write one at a key you choose |
42//! | `POST /xrpc/com.atproto.repo.deleteRecord` | remove one, or make sure it is gone |
43//! | `POST /xrpc/com.atproto.repo.applyWrites` | a batch of the three above |
44//! | `GET /xrpc/com.atproto.repo.getRecord` | one record by collection and key |
45//! | `GET /xrpc/com.atproto.repo.listRecords` | read a collection back, newest first |
46//! | `GET /xrpc/com.atproto.repo.describeRepo` | the account, its document and its collections |
47//! | `GET /xrpc/bot.did.stats` | accounts, records, bytes per collection, blobs by lifecycle state, the policy revision enforced |
48//! | `GET /xrpc/com.atproto.server.describeServer` | this server's DID and its real zone-derived handle domains |
49//! | `GET /xrpc/com.atproto.server.getServiceAuth` | a service-auth token signed as the caller's account |
50//! | `GET /xrpc/com.atproto.server.getSession` | the account the caller's credential signs in as: its DID, handle and status |
51//! | `GET /xrpc/com.atproto.identity.resolveHandle` | the DID a handle points at, hosted here or not |
52//! | `GET /xrpc/com.atproto.identity.resolveDid` | a DID's document, hosted here or not |
53//! | `GET /xrpc/app.bsky.actor.getPreferences` | the account's preferences: none, since this server stores none |
54//! | `POST /xrpc/app.bsky.actor.putPreferences` | `MethodNotImplemented`, and never proxied |
55//! | any other `/xrpc/` method, with `atproto-proxy` | the call, forwarded to the service that header names; see `proxy` |
56//! | any other `app.bsky.*` method, without it | the same, to `[proxy] app_view` when one is set |
57//!
58//! Every `repo` above is an at-identifier: a DID or a handle this deployment
59//! issued. Which credential each route needs, if any, is
60//! [`auth::ROUTE_CREDENTIALS`] — the read surface stays `Public`, and every
61//! `com.atproto.repo.*` write route takes the agent token `createAccount`
62//! hands back; see that module's own doc for the full taxonomy.
63//!
64//! Failures are returned in atproto's error shape, `{"error":..,"message":..}`,
65//! with the names the lexicons declare -- `RecordNotFound`, `RepoNotFound`,
66//! `InvalidRequest`, `InvalidRecord`, `MethodNotImplemented`. See [`ApiError`]
67//! for the whole mapping, and for the two conditions that get a name no lexicon
68//! declares.
69
70#![forbid(unsafe_code)]
71
72mod analytics;
73mod auth;
74pub mod blob_collect;
75mod blobs;
76pub mod confinement_poll;
77mod dashboard;
78mod dashboard_accounts;
79pub use dashboard_accounts::NEVER_WROTE_MIN_IDLE;
80mod dashboard_apps;
81mod error;
82mod foreign;
83pub mod handles;
84mod health;
85mod home;
86mod landing;
87pub mod lookups;
88pub mod oauth;
89pub mod operator;
90pub mod operator_handle;
91pub mod operator_poll;
92pub mod policy_poll;
93mod proxy;
94#[cfg(test)]
95mod race;
96pub mod rate_limit;
97mod refusal;
98pub mod relay;
99pub mod replay;
100mod routes;
101mod service_auth;
102pub mod stale_sweep;
103mod subscribe;
104pub mod subscribers;
105mod surface;
106#[cfg(test)]
107mod tests;
108pub mod tree;
109pub mod tree_reload;
110mod wire;
111pub mod write_meter;
112
113pub use auth::{Credential, Disclosure, ROUTE_CREDENTIALS};
114pub use blob_collect::{collect_once, BlobCollector, DEFAULT_COLLECT_INTERVAL};
115pub use confinement_poll::{ConfinementPoll, DEFAULT_CONFINEMENT_INTERVAL};
116pub use error::{repo_surface, ApiError, ErrorBody};
117pub use health::{
118    CertificateAlerts, CertificateHealth, DiskAlert, DiskHealth, DiskWatermarks, HealthSnapshot,
119    HealthState, HealthTick, ZoneCertificateHealth, DEFAULT_HEALTH_INTERVAL,
120};
121pub use lookups::{DeclaredLookups, Reader as LookupReader};
122pub use oauth::scope::{
123    Action, ActionSet, MimePattern, NsidPattern, Scope, ScopeParseError, ScopeRefused, ScopeSet,
124    Transition,
125};
126pub use operator_handle::OperatorHandle;
127pub use operator_poll::{
128    OperatorPoll, OperatorRecords, PollNudge, PollSchedule, MIN_POLL_INTERVAL,
129};
130pub use policy_poll::{list_records, PolicyPoll, PolicyPollError, PollError};
131pub use proxy::{ProxyLimits, ProxySettings};
132pub use routes::{
133    app, app_with_auth, app_with_health, app_with_repos, cross_origin_layer, AuthState,
134    StrictTransportSecurity, XrpcBody, APP_BSKY_METHODS, ATPROTO_METHODS, BOT_DID_METHODS,
135    MAX_REQUEST_BODY,
136};
137pub use routes::{TURN_HEADER, TURN_HEADER_MAX_LENGTH};
138pub use stale_sweep::{
139    sweep_once, StaleSweeper, SweepSchedule, DEFAULT_SWEEP_INTERVAL, DEFAULT_SWEEP_LIMIT,
140    DEFAULT_UNFINISHED_AGE,
141};
142pub use subscribe::{
143    encode_error, encode_message, Exclusions, Frame, FrameKind, ReplayBounds, Repos, ReposStream,
144    DEFAULT_REPLAY_BYTES, DEFAULT_REPLAY_FRAMES, FUTURE_CURSOR, OUTDATED_CURSOR,
145};
146pub use tree_reload::{TreeReloader, DEFAULT_TREE_RELOAD_INTERVAL};
147pub use wire::{
148    record_uri, refuse_skipped_validation, AccountProof, AccountSummary, ApplyWritesRequest,
149    BlobsView, CreateAccountRequest, CreateRecordRequest, CreateRecordResponse,
150    DeleteRecordRequest, DescribeRepoQuery, DescribeRepoResponse, DescribeServerResponse,
151    DidRequest, EdgeState, Fragment, GetRecordQuery, GetRecordResponse, LifecycleLine,
152    ListRecordsQuery, ParkKeyRequest, ParkedKeyQuery, PolicyBindingView, PolicyRecordView,
153    PolicyRefusalView, PolicyView, PutRecordRequest, RecordView, RepoWrite, StatsResponse, Swap,
154    TallyView, WriteResult, MAX_LIST_LIMIT,
155};
156
157use std::convert::Infallible;
158use std::future::{ready, Ready};
159use std::net::SocketAddr;
160use std::task::{Context, Poll};
161
162use axum::extract::connect_info::{ConnectInfo, Connected};
163use axum::http::Request;
164use axum::Router;
165use tokio::net::TcpListener;
166use tower::limit::{ConcurrencyLimit, ConcurrencyLimitLayer};
167use tower::{Layer, Service};
168use tracing::{info, warn};
169use tracing_subscriber::fmt::time::uptime;
170use tracing_subscriber::EnvFilter;
171
172/// How many requests this server answers at once before a new one waits for
173/// a slot to free up.
174///
175/// Backpressure rather than an outright refusal — `tower::limit::ConcurrencyLimit`
176/// makes a request wait for a permit instead of failing it — because a
177/// caller within its rate-limit budget (`crate::rate_limit`) asking at a bad
178/// moment should be slowed, not bounced. The two are complementary: this
179/// bounds how much concurrent work every route together may impose (memory,
180/// open file descriptors, threads blocked on I/O), where a rate limiter
181/// bounds how much *one caller* may ask for.
182///
183/// Applied by `limited_make_service`, which both [`serve_router`] and
184/// [`serve_tls`] serve through, not as a layer on the [`Router`] itself. That
185/// placement is load-bearing, not a style choice: `axum::Router::layer`
186/// re-applies whatever [`tower::Layer`] it is given at more than one internal
187/// dispatch point, each application constructing its own middleware
188/// instance. A stateless layer does not care — a fresh `Duration` behaves
189/// identically every time, which is why the router's own request timeout is
190/// layered directly onto it. `ConcurrencyLimitLayer` is not
191/// stateless: `Layer::layer` allocates a fresh `Semaphore` on every call, so
192/// a limit applied through `Router::layer` ends up enforced against several
193/// independent, unshared semaphores instead of the one number a deployment
194/// configured — the bound would compile, look correct in a code review, and
195/// do nothing. Wrapping the already-built `Router` once, there, and turning
196/// the result into a `MakeService` the way [`tower::make::Shared`] would is
197/// what keeps every per-connection clone pointing at the same semaphore. See
198/// this crate's `tests::concurrency_past_the_limit_waits_instead_of_running_unbounded`
199/// for the regression this would otherwise be.
200pub const MAX_CONCURRENT_REQUESTS: usize = 512;
201
202/// Filter used when `RUST_LOG` is unset.
203///
204/// `info` for this project's own crates, and the HTTP stack's internals turned
205/// down. `hyper` and `h2` narrate every frame at DEBUG; leaving them on turns
206/// the provisioning log -- the thing this server exists to show -- into a
207/// minority of the output.
208///
209/// `tower` is deliberately not silenced here even though it is noisy in the
210/// same way. `EnvFilter` matches a target by string prefix, so a `tower=warn`
211/// directive also silences `tower_http::trace`, which is where this crate's
212/// request log comes from.
213pub const DEFAULT_LOG_FILTER: &str = "info,hyper=warn,hyper_util=warn,h2=warn,mio=warn";
214
215/// Installs the tracing subscriber.
216///
217/// Honours `RUST_LOG`, falling back to [`DEFAULT_LOG_FILTER`]. Timestamps are
218/// process uptime rather than wall clock: what a developer watching a
219/// provisioning run wants to see is the gap between two events, and a relative
220/// stamp is both shorter and easier to compare at a glance.
221///
222/// Safe to call more than once; the second call is a no-op rather than a
223/// panic, because a test binary that installs a subscriber twice should not
224/// fail for that reason.
225pub fn init_tracing() {
226    let filter =
227        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_FILTER));
228
229    let _ = tracing_subscriber::fmt()
230        .with_env_filter(filter)
231        .with_timer(uptime())
232        // The target says which crate spoke, which is how you tell a DNS
233        // publish from a store write without reading the message.
234        .with_target(true)
235        .with_level(true)
236        .compact()
237        .try_init();
238}
239
240/// Serves an already-built router on `listener` until `SIGINT` or
241/// `SIGTERM`, waiting at most [`SHUTDOWN_GRACE`] on connections that will
242/// not close by themselves.
243///
244/// `router` is served through `limited_make_service`, the wrapping
245/// [`serve_tls`] serves through too, so [`MAX_CONCURRENT_REQUESTS`] bounds
246/// this listener the same way — see its own doc for why that bound is wired
247/// there rather than as a layer on `router` itself.
248pub async fn serve_router(listener: TcpListener, router: Router) -> std::io::Result<()> {
249    // First, and before `listening` says the server is up: the accept loop
250    // does not poll the future below until it is running, and a stop that
251    // arrives before then is handed only to the listeners this process already
252    // had. In this binary there is one — the long poll's — so the stop would be
253    // logged and the accept loop would go on serving.
254    let stopped = installed_shutdown_signal();
255    match listener.local_addr() {
256        Ok(addr) => info!(%addr, "listening"),
257        Err(err) => warn!(%err, "listening on an address that could not be read back"),
258    }
259    // The signal is observed twice: once by `axum::serve`, to stop accepting
260    // and let in-flight requests finish, and once here to start the clock on
261    // [`SHUTDOWN_GRACE`]. `axum::serve` has no timeout of its own, and the
262    // streams this server holds open never close by themselves, so without
263    // the second observation a shutdown with one subscriber attached waits
264    // forever.
265    let (started, shutdown_started) = tokio::sync::oneshot::channel();
266    let serve =
267        axum::serve(listener, limited_make_service(router)).with_graceful_shutdown(async move {
268            stopped.await;
269            let _ = started.send(());
270        });
271    tokio::select! {
272        result = serve => result,
273        () = grace_expired(shutdown_started) => {
274            warn!(
275                after = ?SHUTDOWN_GRACE,
276                "connections were still open when the shutdown grace ran out; stopping anyway"
277            );
278            Ok(())
279        }
280    }
281}
282
283/// Resolves [`SHUTDOWN_GRACE`] after `started` fires, and never if it does
284/// not.
285///
286/// The sender is dropped without sending when serving ends for any reason
287/// other than a signal — a bind that died, a listener that stopped
288/// accepting — and that must not be read as a shutdown whose grace has
289/// begun, so a closed channel pends here rather than starting the clock.
290async fn grace_expired(started: tokio::sync::oneshot::Receiver<()>) {
291    if started.await.is_err() {
292        std::future::pending::<()>().await;
293    }
294    tokio::time::sleep(SHUTDOWN_GRACE).await;
295}
296
297/// What both listeners hand each accepted connection: `router` behind
298/// [`MAX_CONCURRENT_REQUESTS`], carrying the peer's address as
299/// [`ConnectInfo`].
300///
301/// One function rather than a wrapping per listener, because the wrapping
302/// is the cap: a listener that turns `router` into a make-service any other
303/// way serves without it.
304fn limited_make_service(router: Router) -> LimitedMakeService {
305    make_service_limited_to(router, MAX_CONCURRENT_REQUESTS)
306}
307
308/// [`limited_make_service`] at a limit a test can overrun with two requests.
309fn make_service_limited_to(router: Router, limit: usize) -> LimitedMakeService {
310    MakeServiceWithConnectInfo::new(ConcurrencyLimitLayer::new(limit).layer(router))
311}
312
313/// The service both listeners are driven by; see [`limited_make_service`].
314type LimitedMakeService = MakeServiceWithConnectInfo<ConcurrencyLimit<Router>>;
315
316/// Turns an already-[`Clone`] service — `limited_make_service`'s
317/// [`ConcurrencyLimit`]-wrapped router — into a [`tower::Service`]
318/// `axum::serve` and `axum_server` can both drive per accepted connection,
319/// the same way [`tower::make::Shared`] does, but also carrying a
320/// [`ConnectInfo`] extension for the connection's peer address, which
321/// `crate::rate_limit::caller_key` reads by default.
322///
323/// `axum`'s own `Router::into_make_service_with_connect_info` cannot stand
324/// in for this: it is only defined on a bare [`Router`], not on whatever
325/// type wrapping one in a [`tower::Layer`] produces, and
326/// [`MAX_CONCURRENT_REQUESTS`]'s own doc explains why that wrapping must
327/// happen exactly once, here, rather than as a `Router::layer` — this type
328/// exists so that requirement and per-connection `ConnectInfo` are not a
329/// choice between one or the other.
330#[derive(Clone)]
331struct MakeServiceWithConnectInfo<S> {
332    inner: S,
333}
334
335impl<S> MakeServiceWithConnectInfo<S> {
336    fn new(inner: S) -> Self {
337        Self { inner }
338    }
339}
340
341impl<S, T> Service<T> for MakeServiceWithConnectInfo<S>
342where
343    S: Clone,
344    SocketAddr: Connected<T>,
345{
346    type Response = ConnectInfoService<S>;
347    type Error = Infallible;
348    type Future = Ready<Result<Self::Response, Self::Error>>;
349
350    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
351        Poll::Ready(Ok(()))
352    }
353
354    fn call(&mut self, target: T) -> Self::Future {
355        ready(Ok(ConnectInfoService {
356            inner: self.inner.clone(),
357            addr: SocketAddr::connect_info(target),
358        }))
359    }
360}
361
362/// The per-connection service [`MakeServiceWithConnectInfo`] hands back:
363/// `inner`, unchanged, with `ConnectInfo(addr)` inserted into every
364/// request's extensions before the request reaches it.
365#[derive(Clone)]
366struct ConnectInfoService<S> {
367    inner: S,
368    addr: SocketAddr,
369}
370
371impl<S, ReqBody> Service<Request<ReqBody>> for ConnectInfoService<S>
372where
373    S: Service<Request<ReqBody>>,
374{
375    type Response = S::Response;
376    type Error = S::Error;
377    type Future = S::Future;
378
379    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
380        self.inner.poll_ready(cx)
381    }
382
383    fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
384        req.extensions_mut().insert(ConnectInfo(self.addr));
385        self.inner.call(req)
386    }
387}
388
389/// Serves an already-built router on `listener`, terminating TLS with
390/// `config`, until `SIGINT` or `SIGTERM`, on the same [`SHUTDOWN_GRACE`]
391/// [`serve_router`] gives the plain-HTTP path.
392///
393/// `config` is expected to come from `didbot_tls::manager::CertificateManager
394/// ::server_config`, or anything else built the same way: **one**
395/// `rustls::ServerConfig` for the whole life of this call, whose certificate
396/// resolver is itself swappable. A renewal happening elsewhere replaces what
397/// that resolver hands back; it does not call this function again, does not
398/// rebind `listener`, and does not touch a connection already past its
399/// handshake — the certificate only ever participates in the handshake that
400/// created a connection. This crate does not know how to renew a
401/// certificate, and does not need to: it only needs one `ServerConfig` that
402/// keeps answering correctly for as long as the process runs.
403///
404/// This is why `serve_tls` takes a config rather than, say, a certificate
405/// path: baking "reload the files and rebuild the config" in here would put
406/// the rebind-avoidance property back in the caller's hands, exactly where
407/// the epic that asked for this said it must not be.
408///
409/// Served through `limited_make_service`, the wrapping [`serve_router`]
410/// serves through, so [`MAX_CONCURRENT_REQUESTS`] bounds this listener's
411/// in-flight requests the same way, and with
412/// [`HEADER_READ_TIMEOUT`] on the reading of a request's headers.
413pub async fn serve_tls(
414    listener: TcpListener,
415    router: Router,
416    config: std::sync::Arc<rustls::ServerConfig>,
417) -> std::io::Result<()> {
418    serve_tls_with_header_timeout(listener, router, config, HEADER_READ_TIMEOUT).await
419}
420
421/// [`serve_tls`], with the header-read timeout named rather than taken from
422/// [`HEADER_READ_TIMEOUT`].
423///
424/// Public so a test can pick a timeout it can actually wait out; a
425/// deployment calls [`serve_tls`].
426pub async fn serve_tls_with_header_timeout(
427    listener: TcpListener,
428    router: Router,
429    config: std::sync::Arc<rustls::ServerConfig>,
430    header_read_timeout: std::time::Duration,
431) -> std::io::Result<()> {
432    // Registered before the listener is announced, for the reason
433    // `serve_router` gives: the task below is spawned, so nothing polls it
434    // until the runtime gets to it.
435    let stopped = installed_shutdown_signal();
436    serve_tls_inner(
437        listener,
438        router,
439        config,
440        header_read_timeout,
441        stopped,
442        SHUTDOWN_GRACE,
443    )
444    .await
445}
446
447/// [`serve_tls`], stopping when `stopped` resolves rather than on a signal
448/// observed from here on, and waiting `drain` rather than
449/// [`SHUTDOWN_GRACE`] for requests in flight. A process whose startup runs
450/// after its handlers are installed passes [`Stopping::wait`], so a stop
451/// sent during that startup is not lost.
452pub async fn serve_tls_until(
453    listener: TcpListener,
454    router: Router,
455    config: std::sync::Arc<rustls::ServerConfig>,
456    stopped: impl std::future::Future<Output = ()> + Send + 'static,
457    drain: std::time::Duration,
458) -> std::io::Result<()> {
459    serve_tls_inner(
460        listener,
461        router,
462        config,
463        HEADER_READ_TIMEOUT,
464        stopped,
465        drain,
466    )
467    .await
468}
469
470async fn serve_tls_inner(
471    listener: TcpListener,
472    router: Router,
473    config: std::sync::Arc<rustls::ServerConfig>,
474    header_read_timeout: std::time::Duration,
475    stopped: impl std::future::Future<Output = ()> + Send + 'static,
476    drain: std::time::Duration,
477) -> std::io::Result<()> {
478    match listener.local_addr() {
479        Ok(addr) => info!(%addr, "listening (tls)"),
480        Err(err) => warn!(%err, "listening on an address that could not be read back"),
481    }
482    let listener = listener.into_std()?;
483    let rustls_config = axum_server::tls_rustls::RustlsConfig::from_config(config);
484    let handle = axum_server::Handle::new();
485    let shutdown_handle = handle.clone();
486    tokio::spawn(async move {
487        stopped.await;
488        info!(?drain, "no longer accepting; waiting on requests in flight");
489        // Graceful: let in-flight requests finish rather than cutting
490        // connections mid-response, bounded by `drain` -- a subscriber
491        // holding a stream open must not be able to hold the shutdown open
492        // with it.
493        shutdown_handle.graceful_shutdown(Some(drain));
494    });
495    let mut server = axum_server::from_tcp_rustls(listener, rustls_config)?;
496    // A connection that has finished its TLS handshake and then sends its
497    // request headers one byte at a time holds a connection, a task and a
498    // buffer for free: `TimeoutLayer` on the router only starts once the
499    // request has been read, so before this there was nothing bounding the
500    // reading of it. `axum_server`'s own handshake timeout bounds the step
501    // before; this bounds the step after.
502    //
503    // The timer is required: `hyper`'s HTTP/1 builder panics on a
504    // `header_read_timeout` with no timer to measure it with.
505    server
506        .http_builder()
507        .http1()
508        .timer(hyper_util::rt::TokioTimer::new())
509        .header_read_timeout(Some(header_read_timeout));
510    server
511        .handle(handle)
512        .serve(limited_make_service(router))
513        .await
514}
515
516/// How long a connection has to finish sending its request headers.
517///
518/// Generous for a real client on a slow link — a request head is a few
519/// hundred bytes — and short enough that holding a connection open costs an
520/// attacker a fresh handshake every time it elapses.
521pub const HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
522
523/// How long a shutdown waits for connections that are still open before it
524/// stops anyway, unless the caller names its own bound; `didbot-pds` reads
525/// `[server] drain_secs`.
526///
527/// A bound is not optional here, for a reason specific to what this server
528/// serves: `com.atproto.sync.subscribeRepos` is a stream that never ends on
529/// its own. A graceful shutdown that waits for every connection to close
530/// waits forever whenever one subscriber is attached — which, for a server
531/// whose whole point is to be subscribed to, is the normal case rather than
532/// the edge one.
533///
534/// What runs *after* serving ends has to fit inside the deployment's own
535/// window too: the write-ahead log is synced there, and a sync that is
536/// `SIGKILL`ed mid-way is the one outcome this whole path exists to avoid.
537/// The unit file in `terraform-aws-didbot-pds/templates/user_data.sh.tftpl` allows 200
538/// seconds from `SIGTERM` to `SIGKILL`.
539pub const SHUTDOWN_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
540
541/// How long a shutdown waits, once serving has ended, for each
542/// `subscribeRepos` connection to send what it holds and close.
543///
544/// A consumer that is reading takes far less to drain what it is owed, and
545/// one that is not is closed by the process exiting.
546pub const STREAM_CLOSE_GRACE: std::time::Duration = std::time::Duration::from_secs(2);
547
548/// The handlers for the signals that stop this process, registered when this
549/// is constructed rather than when it is awaited.
550///
551/// When they are registered is the whole point of the type. tokio hands a
552/// signal to the listeners that existed when it arrived and to no others, so a
553/// listener built after the signal has been delivered never fires, however
554/// promptly it is then awaited.
555struct StopHandlers {
556    #[cfg(unix)]
557    interrupt: Option<tokio::signal::unix::Signal>,
558    #[cfg(unix)]
559    terminate: Option<tokio::signal::unix::Signal>,
560}
561
562impl StopHandlers {
563    #[cfg(unix)]
564    fn install() -> Self {
565        fn handler(
566            kind: tokio::signal::unix::SignalKind,
567            name: &'static str,
568        ) -> Option<tokio::signal::unix::Signal> {
569            match tokio::signal::unix::signal(kind) {
570                Ok(stream) => Some(stream),
571                Err(err) => {
572                    // A handler that cannot be installed simply never fires,
573                    // rather than taking down a process that is otherwise
574                    // serving fine: panicking would end a healthy server, and
575                    // resolving would stop it the instant it started.
576                    warn!(%err, signal = name, "could not install a stop handler");
577                    None
578                }
579            }
580        }
581
582        Self {
583            interrupt: handler(tokio::signal::unix::SignalKind::interrupt(), "SIGINT"),
584            terminate: handler(tokio::signal::unix::SignalKind::terminate(), "SIGTERM"),
585        }
586    }
587
588    #[cfg(not(unix))]
589    fn install() -> Self {
590        Self {}
591    }
592
593    #[cfg(unix)]
594    async fn stopped(mut self) {
595        async fn fired(stream: Option<&mut tokio::signal::unix::Signal>) {
596            match stream {
597                Some(stream) => {
598                    stream.recv().await;
599                }
600                None => std::future::pending().await,
601            }
602        }
603
604        tokio::select! {
605            () = fired(self.interrupt.as_mut()) => (),
606            () = fired(self.terminate.as_mut()) => (),
607        }
608        info!("shutdown signal received");
609    }
610
611    #[cfg(not(unix))]
612    async fn stopped(self) {
613        match tokio::signal::ctrl_c().await {
614            Ok(()) => info!("shutdown signal received"),
615            Err(err) => {
616                warn!(%err, "could not install the Ctrl-C handler");
617                std::future::pending::<()>().await
618            }
619        }
620    }
621}
622
623/// Resolves when this process is asked to stop: `SIGTERM`, or Ctrl-C. The
624/// handlers are registered before this returns.
625///
626/// `SIGTERM` is the half a deployment actually uses: `docker stop` sends it,
627/// systemd's own `ExecStop` sends it, and a process that is PID 1 in a
628/// container — which this one is, see the `Dockerfile`'s `ENTRYPOINT` — gets
629/// no default disposition for it, so an unhandled `SIGTERM` is not a
630/// termination but a signal that is silently discarded. Waiting only on
631/// Ctrl-C therefore meant every stop ran to its grace period and ended in
632/// `SIGKILL`, skipping the write-ahead log's flush on the way out.
633///
634/// Public because more than one thing has to observe it. [`serve_router`]
635/// stops accepting on it, and a caller that parks a request — the decision
636/// long poll — waits on its own copy so a shutdown does not have to sit out
637/// that request's whole window. Awaiting it more than once is fine; each
638/// caller installs its own listener.
639///
640/// tokio hands a signal only to the listeners that existed when it arrived, so
641/// the gap between building an observation and first polling it is a gap a
642/// stop can fall into. [`serve_router`] holds that gap open across the whole of
643/// its setup — it announces the listener, and only the accept loop that runs
644/// afterwards polls the shutdown future — which is long enough for a real
645/// `docker stop` to land in it.
646pub fn installed_shutdown_signal() -> impl std::future::Future<Output = ()> + Send + 'static {
647    StopHandlers::install().stopped()
648}
649
650/// One observation of the stop signal for a whole run, installed once at
651/// its start and awaited by everything that stops on it.
652///
653/// A handler installed partway through startup misses a stop sent before
654/// it. This one remembers: a stop that arrived while startup was busy is
655/// still there for [`Stopping::wait`] when serving begins.
656#[derive(Debug, Clone)]
657pub struct Stopping(tokio::sync::watch::Receiver<bool>);
658
659impl Stopping {
660    /// Installs the handlers now. Needs a running tokio runtime.
661    pub fn install() -> Self {
662        let stopped = installed_shutdown_signal();
663        let (sender, receiver) = tokio::sync::watch::channel(false);
664        tokio::spawn(async move {
665            stopped.await;
666            let _ = sender.send(true);
667        });
668        Self(receiver)
669    }
670
671    /// A stop that arrives once `stopped` reads true, for a caller that
672    /// decides for itself when a run stops.
673    pub fn when(stopped: tokio::sync::watch::Receiver<bool>) -> Self {
674        Self(stopped)
675    }
676
677    /// A stop that never arrives.
678    pub fn never() -> Self {
679        Self(tokio::sync::watch::channel(false).1)
680    }
681
682    /// Resolves once a stop has arrived, at once if one already has.
683    pub async fn wait(mut self) {
684        if self.0.wait_for(|stopped| *stopped).await.is_err() {
685            std::future::pending::<()>().await;
686        }
687    }
688
689    /// This stop, arriving only once `work` has run after it.
690    ///
691    /// For what has to happen while the process still serves: everything
692    /// that waits on the returned one keeps going until `work` is done.
693    /// `work` is not polled before the stop arrives.
694    pub fn after(self, work: impl std::future::Future<Output = ()> + Send + 'static) -> Self {
695        let (sender, receiver) = tokio::sync::watch::channel(false);
696        tokio::spawn(async move {
697            self.wait().await;
698            work.await;
699            let _ = sender.send(true);
700        });
701        Self(receiver)
702    }
703}