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}