Skip to main content

dynomite/stats/
rest.rs

1//! Hand-rolled HTTP/1.1 server exposing the stats snapshot as JSON.
2//!
3//! This module implements a minimal stats query surface: a `GET /`
4//! (and `GET /info`) request that returns the latest snapshot. Every
5//! other request returns an empty `200 OK` with body `OK\r\n`.
6
7#![allow(clippy::needless_continue)]
8
9use std::io;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Duration;
13
14use parking_lot::Mutex;
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::{TcpListener, TcpStream};
17use tracing::Instrument as _;
18
19use crate::admin::cluster_info::{format_text, ClusterInfoSnapshot, RingSnapshot};
20use crate::stats::prometheus::render_prometheus;
21use crate::stats::snapshot::Snapshot;
22
23/// Type alias for a closure that produces a fresh
24/// [`ClusterInfoSnapshot`] every time the `/cluster-info.txt`
25/// route is hit.
26///
27/// The closure must be `Send + Sync` so a clone can be moved
28/// into each accept-handler task. The runtime owns the closure;
29/// embedders set it via
30/// [`StatsServer::with_cluster_info_provider`].
31pub type ClusterInfoProvider = Arc<dyn Fn() -> ClusterInfoSnapshot + Send + Sync>;
32
33/// Type alias for a closure that produces a fresh
34/// [`RingSnapshot`] every time the `/ring` route is hit.
35///
36/// The closure must be `Send + Sync` so a clone can be moved
37/// into each accept-handler task. Embedders set it via
38/// [`StatsServer::with_ring_provider`]; when unset the `/ring`
39/// route returns `503 Service Unavailable`.
40pub type RingProvider = Arc<dyn Fn() -> RingSnapshot + Send + Sync>;
41
42/// Maximum number of bytes the server will read for an HTTP request
43/// line plus headers. Requests larger than this are rejected.
44///
45/// # Examples
46///
47/// ```
48/// assert!(dynomite::stats::MAX_REQUEST_BYTES >= 1024);
49/// ```
50pub const MAX_REQUEST_BYTES: usize = 8 * 1024;
51
52/// Maximum number of headers parsed in a single request.
53///
54/// # Examples
55///
56/// ```
57/// assert!(dynomite::stats::MAX_HEADERS > 0);
58/// ```
59pub const MAX_HEADERS: usize = 32;
60
61/// Maximum time the server waits for a single read from a connected
62/// peer before closing the socket. Protects tokio tasks from
63/// slow-loris clients.
64const READ_TIMEOUT: Duration = Duration::from_secs(5);
65
66/// A bound TCP listener serving the stats endpoint.
67///
68/// Construct via [`StatsServer::bind`] then call
69/// [`StatsServer::run`] to accept connections in a loop, or
70/// [`StatsServer::accept_one`] for one-shot tests.
71///
72/// # Examples
73///
74/// ```no_run
75/// use std::sync::Arc;
76/// use dynomite::stats::{Snapshot, StatsServer};
77/// use parking_lot::Mutex;
78///
79/// # async fn _example() -> std::io::Result<()> {
80/// let sink = Arc::new(Mutex::new(Snapshot::default()));
81/// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?;
82/// let _addr = server.local_addr()?;
83/// # Ok(())
84/// # }
85/// ```
86pub struct StatsServer {
87    listener: TcpListener,
88    source: Arc<Mutex<Snapshot>>,
89    cluster_info: Option<ClusterInfoProvider>,
90    ring: Option<RingProvider>,
91}
92
93impl StatsServer {
94    /// Bind a listener at `addr`. Returns the bound server alongside
95    /// its actual local address (useful when binding to port 0).
96    ///
97    /// # Examples
98    ///
99    /// ```no_run
100    /// use std::sync::Arc;
101    /// use dynomite::stats::{Snapshot, StatsServer};
102    /// use parking_lot::Mutex;
103    ///
104    /// # async fn _example() -> std::io::Result<()> {
105    /// let sink = Arc::new(Mutex::new(Snapshot::default()));
106    /// let _server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?;
107    /// # Ok(())
108    /// # }
109    /// ```
110    pub fn bind(addr: SocketAddr, source: Arc<Mutex<Snapshot>>) -> io::Result<Self> {
111        // Bind via the shared helper so the stats listener sets
112        // SO_REUSEADDR like the client and dnode listeners. Without it,
113        // a fast restart (kill + relaunch) fails with EADDRINUSE while
114        // the previous socket lingers in TIME_WAIT, and because the
115        // server builds all listeners as a unit that failure aborts the
116        // whole process.
117        let listener = crate::net::listener::bind_dual_stack(
118            addr,
119            crate::net::listener::BindOptions::default(),
120        )?;
121        Ok(Self {
122            listener,
123            source,
124            cluster_info: None,
125            ring: None,
126        })
127    }
128
129    /// Attach a [`ClusterInfoProvider`] so the server answers
130    /// `GET /cluster-info.txt` with a freshly assembled
131    /// snapshot. When no provider is registered the route
132    /// returns `503 Service Unavailable`.
133    ///
134    /// # Examples
135    ///
136    /// ```no_run
137    /// use std::sync::Arc;
138    /// use dynomite::admin::cluster_info::ClusterInfoSnapshot;
139    /// use dynomite::stats::{Snapshot, StatsServer};
140    /// use parking_lot::Mutex;
141    ///
142    /// # async fn _example() -> std::io::Result<()> {
143    /// let sink = Arc::new(Mutex::new(Snapshot::default()));
144    /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?
145    ///     .with_cluster_info_provider(Arc::new(ClusterInfoSnapshot::synthetic));
146    /// drop(server);
147    /// # Ok(())
148    /// # }
149    /// ```
150    #[must_use]
151    pub fn with_cluster_info_provider(mut self, provider: ClusterInfoProvider) -> Self {
152        self.cluster_info = Some(provider);
153        self
154    }
155
156    /// Attach a [`RingProvider`] so the server answers
157    /// `GET /ring` with a freshly assembled, JSON-serialized
158    /// view of every peer's tokens, dc, rack, and state. When no
159    /// provider is registered the route returns `503 Service
160    /// Unavailable`.
161    ///
162    /// # Examples
163    ///
164    /// ```no_run
165    /// use std::sync::Arc;
166    /// use dynomite::admin::cluster_info::RingSnapshot;
167    /// use dynomite::stats::{Snapshot, StatsServer};
168    /// use parking_lot::Mutex;
169    ///
170    /// # async fn _example() -> std::io::Result<()> {
171    /// let sink = Arc::new(Mutex::new(Snapshot::default()));
172    /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?
173    ///     .with_ring_provider(Arc::new(RingSnapshot::default));
174    /// drop(server);
175    /// # Ok(())
176    /// # }
177    /// ```
178    #[must_use]
179    pub fn with_ring_provider(mut self, provider: RingProvider) -> Self {
180        self.ring = Some(provider);
181        self
182    }
183
184    /// Returns the local socket address the server is listening on.
185    ///
186    /// # Examples
187    ///
188    /// ```no_run
189    /// use std::sync::Arc;
190    /// use dynomite::stats::{Snapshot, StatsServer};
191    /// use parking_lot::Mutex;
192    ///
193    /// # async fn _example() -> std::io::Result<()> {
194    /// let sink = Arc::new(Mutex::new(Snapshot::default()));
195    /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?;
196    /// let addr = server.local_addr()?;
197    /// assert!(addr.port() != 0);
198    /// # Ok(())
199    /// # }
200    /// ```
201    pub fn local_addr(&self) -> io::Result<SocketAddr> {
202        self.listener.local_addr()
203    }
204
205    /// Accept a single connection, serve one HTTP/1.1 request, and
206    /// return.
207    ///
208    /// # Examples
209    ///
210    /// ```no_run
211    /// use std::sync::Arc;
212    /// use dynomite::stats::{Snapshot, StatsServer};
213    /// use parking_lot::Mutex;
214    ///
215    /// # async fn _example() -> std::io::Result<()> {
216    /// let sink = Arc::new(Mutex::new(Snapshot::default()));
217    /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?;
218    /// server.accept_one().await?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    pub async fn accept_one(&self) -> io::Result<()> {
223        let (sock, _peer) = self.listener.accept().await?;
224        let snapshot = self.source.lock().clone();
225        let cluster_info = self.cluster_info.clone();
226        let ring = self.ring.clone();
227        serve_connection(sock, snapshot, cluster_info, ring).await
228    }
229
230    /// Run the accept loop until cancelled. Each connection is handled
231    /// on a fresh task so a slow client cannot stall the listener.
232    ///
233    /// # Examples
234    ///
235    /// ```no_run
236    /// use std::sync::Arc;
237    /// use dynomite::stats::{Snapshot, StatsServer};
238    /// use parking_lot::Mutex;
239    ///
240    /// # async fn _example() -> std::io::Result<()> {
241    /// let sink = Arc::new(Mutex::new(Snapshot::default()));
242    /// let server = StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink)?;
243    /// let _ = tokio::spawn(async move { server.run().await });
244    /// # Ok(())
245    /// # }
246    /// ```
247    pub async fn run(self) -> io::Result<()> {
248        let span = tracing::info_span!(
249            "stats_server.run",
250            local = %self.listener.local_addr().map_or_else(|_| String::from("?"), |a| a.to_string()),
251        );
252        let listener = self.listener;
253        let source = self.source;
254        let cluster_info = self.cluster_info;
255        let ring = self.ring;
256        async move {
257            loop {
258                let (sock, _peer) = listener.accept().await?;
259                let snapshot = source.lock().clone();
260                let ci = cluster_info.clone();
261                let rg = ring.clone();
262                tokio::spawn(async move {
263                    let _ = serve_connection(sock, snapshot, ci, rg).await;
264                });
265            }
266        }
267        .instrument(span)
268        .await
269    }
270}
271
272async fn serve_connection(
273    mut sock: TcpStream,
274    snapshot: Snapshot,
275    cluster_info: Option<ClusterInfoProvider>,
276    ring: Option<RingProvider>,
277) -> io::Result<()> {
278    let mut buf = vec![0u8; MAX_REQUEST_BYTES];
279    let mut filled = 0usize;
280    loop {
281        if filled == buf.len() {
282            return write_response(&mut sock, 400, "Bad Request", b"").await;
283        }
284        let read_result = tokio::time::timeout(READ_TIMEOUT, sock.read(&mut buf[filled..])).await;
285        let Ok(Ok(n)) = read_result else {
286            // Read error or timeout: close silently, matching the
287            // reference error path which drops the connection without
288            // writing a response.
289            let _ = sock.shutdown().await;
290            return Ok(());
291        };
292        if n == 0 {
293            break;
294        }
295        filled += n;
296        let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS];
297        let mut req = httparse::Request::new(&mut headers);
298        match req.parse(&buf[..filled]) {
299            Ok(httparse::Status::Complete(_)) => {
300                return handle_parsed(&mut sock, &req, snapshot, cluster_info, ring).await;
301            }
302            Ok(httparse::Status::Partial) => continue,
303            Err(_) => {
304                return write_response(&mut sock, 400, "Bad Request", b"").await;
305            }
306        }
307    }
308    Ok(())
309}
310
311async fn handle_parsed(
312    sock: &mut TcpStream,
313    req: &httparse::Request<'_, '_>,
314    snapshot: Snapshot,
315    cluster_info: Option<ClusterInfoProvider>,
316    ring: Option<RingProvider>,
317) -> io::Result<()> {
318    let path = req.path.unwrap_or("/");
319    if !matches!(req.method, Some("GET")) {
320        return write_response(sock, 405, "Method Not Allowed", b"").await;
321    }
322    match path {
323        "/" | "/info" | "/stats" => {
324            let body = snapshot.to_json();
325            write_json_response(sock, body.as_bytes()).await
326        }
327        "/metrics" => {
328            let body = render_prometheus(&snapshot);
329            write_metrics_response(sock, body.as_bytes()).await
330        }
331        "/cluster-info.txt" => match cluster_info {
332            Some(provider) => {
333                let snap = provider();
334                let mut body: Vec<u8> = Vec::with_capacity(4096);
335                if format_text(&snap, &mut body).is_err() {
336                    return write_response(sock, 500, "Internal Server Error", b"").await;
337                }
338                write_text_response(sock, &body).await
339            }
340            None => write_response(sock, 503, "Service Unavailable", b"").await,
341        },
342        "/ring" | "/ring.json" => match ring {
343            Some(provider) => match serde_json::to_vec(&provider()) {
344                Ok(body) => write_json_response(sock, &body).await,
345                Err(_) => write_response(sock, 500, "Internal Server Error", b"").await,
346            },
347            None => write_response(sock, 503, "Service Unavailable", b"").await,
348        },
349        _ => write_response(sock, 200, "OK", b"OK\r\n").await,
350    }
351}
352
353async fn write_text_response(sock: &mut TcpStream, body: &[u8]) -> io::Result<()> {
354    let header = format!(
355        "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=us-ascii\r\n\
356         Content-Length: {}\r\nConnection: close\r\n\r\n",
357        body.len()
358    );
359    sock.write_all(header.as_bytes()).await?;
360    sock.write_all(body).await?;
361    sock.shutdown().await?;
362    Ok(())
363}
364
365async fn write_response(
366    sock: &mut TcpStream,
367    code: u16,
368    reason: &str,
369    body: &[u8],
370) -> io::Result<()> {
371    let header = format!(
372        "HTTP/1.1 {code} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
373        body.len()
374    );
375    sock.write_all(header.as_bytes()).await?;
376    if !body.is_empty() {
377        sock.write_all(body).await?;
378    }
379    sock.shutdown().await?;
380    Ok(())
381}
382
383async fn write_json_response(sock: &mut TcpStream, body: &[u8]) -> io::Result<()> {
384    let header = format!(
385        "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\n\
386         Content-Length: {}\r\nConnection: close\r\n\r\n",
387        body.len()
388    );
389    sock.write_all(header.as_bytes()).await?;
390    sock.write_all(body).await?;
391    sock.shutdown().await?;
392    Ok(())
393}
394
395async fn write_metrics_response(sock: &mut TcpStream, body: &[u8]) -> io::Result<()> {
396    let header = format!(
397        "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\n\
398         Content-Length: {}\r\nConnection: close\r\n\r\n",
399        body.len()
400    );
401    sock.write_all(header.as_bytes()).await?;
402    sock.write_all(body).await?;
403    sock.shutdown().await?;
404    Ok(())
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    /// Regression: the stats listener must set SO_REUSEADDR so a fast
412    /// restart (kill + immediate rebind, as the qualification harness
413    /// does between consistency-level runs) does not fail with
414    /// EADDRINUSE while the previous socket lingers. Bind, capture the
415    /// concrete address, drop the server, and rebind the same address
416    /// immediately -- this succeeds with SO_REUSEADDR and would fail a
417    /// plain TcpListener::bind under TIME_WAIT.
418    #[tokio::test]
419    async fn stats_bind_is_reusable_after_drop() {
420        let sink = Arc::new(Mutex::new(Snapshot::default()));
421        let first =
422            StatsServer::bind("127.0.0.1:0".parse().unwrap(), sink.clone()).expect("first bind");
423        let addr = first.local_addr().expect("addr");
424        drop(first);
425        // Immediate rebind of the same concrete address must succeed.
426        let second = StatsServer::bind(addr, sink);
427        assert!(
428            second.is_ok(),
429            "rebind of {addr} failed (SO_REUSEADDR not set?): {:?}",
430            second.err()
431        );
432    }
433}