apsis-book/src/http/server.rs

244 lines
8.0 KiB
Rust

//! Connection acceptance and dispatch to a bounded worker pool.
use std::io;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::Duration;
use super::request::{Limits, Method, Request, RequestError, read_request};
use super::response::{Response, Status};
/// A request handler.
///
/// The handler receives the request plus the peer address and returns the
/// response to send. It must not block on shared state that another handler
/// could hold for long, because a bounded number of requests are served
/// concurrently.
pub trait Handler: Send + Sync + 'static {
/// Produces the response for one request.
fn handle(&self, request: &Request, peer: SocketAddr) -> Response;
}
impl<F> Handler for F
where
F: Fn(&Request, SocketAddr) -> Response + Send + Sync + 'static,
{
fn handle(&self, request: &Request, peer: SocketAddr) -> Response {
self(request, peer)
}
}
/// Tuning knobs for the server.
#[derive(Debug, Clone, Copy)]
pub struct ServerConfig {
/// Number of worker threads.
pub workers: usize,
/// Maximum number of connections waiting for a worker.
pub backlog: usize,
/// Read and write timeout applied to every connection.
pub timeout: Duration,
/// Request size limits.
pub limits: Limits,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
workers: 4,
backlog: 128,
timeout: Duration::from_secs(15),
limits: Limits::default(),
}
}
}
/// A running HTTP server.
pub struct Server {
listener: TcpListener,
config: ServerConfig,
}
impl Server {
/// Binds a server to a socket address.
///
/// This is the form the command line uses once the host and the port
/// have been resolved, so that a name is resolved once rather than left
/// to the listener.
pub fn bind_address(address: SocketAddr, config: ServerConfig) -> io::Result<Self> {
let listener = TcpListener::bind(address)?;
Ok(Self { listener, config })
}
/// Returns the address the server is bound to.
///
/// This is useful when the configured port is zero and the operating
/// system assigned one.
pub fn local_address(&self) -> io::Result<SocketAddr> {
self.listener.local_addr()
}
/// Runs the accept loop until the listener fails.
///
/// Every accepted connection is handed to a worker thread from a fixed
/// size pool. When all workers are busy a connection waits in the queue,
/// so memory use stays bounded by the pool size and the queue capacity.
pub fn run<H: Handler>(self, handler: Arc<H>) -> io::Result<()> {
let (sender, receiver) = mpsc::sync_channel::<TcpStream>(self.config.backlog);
let receiver = Arc::new(Mutex::new(receiver));
let workers = self.config.workers.max(1);
let mut handles = Vec::with_capacity(workers);
for _ in 0..workers {
let receiver = Arc::clone(&receiver);
let handler = Arc::clone(&handler);
let config = self.config;
handles.push(
thread::Builder::new()
.name("apsis-web-worker".to_string())
.spawn(move || worker_loop(&receiver, &*handler, config))?,
);
}
for connection in self.listener.incoming() {
match connection {
Ok(stream) => {
// A disconnected worker pool means the process is
// shutting down; dropping the stream closes it.
if sender.send(stream).is_err() {
break;
}
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
}
}
drop(sender);
for handle in handles {
let _ = handle.join();
}
Ok(())
}
}
/// Serves requests from the queue until every sender is dropped.
fn worker_loop<H: Handler>(
receiver: &Mutex<mpsc::Receiver<TcpStream>>,
handler: &H,
config: ServerConfig,
) {
loop {
// The lock is released before the request is served, so workers do
// not serialise on the queue.
let stream = {
let guard = match receiver.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.recv()
};
let Ok(stream) = stream else {
return;
};
serve_connection(stream, handler, config);
}
}
/// Reads one request from a connection and writes the response.
fn serve_connection<H: Handler>(mut stream: TcpStream, handler: &H, config: ServerConfig) {
let peer = stream
.peer_addr()
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
let _ = stream.set_read_timeout(Some(config.timeout));
let _ = stream.set_write_timeout(Some(config.timeout));
let _ = stream.set_nodelay(true);
let response = match read_request(&mut stream, config.limits) {
Ok(request) => {
let suppress_body = request.method == Method::Head;
let response = dispatch(handler, &request, peer);
let _ = response.write_to(&mut stream, suppress_body);
return;
}
Err(error) => error_response(error),
};
let _ = response.write_to(&mut stream, false);
}
/// Turns a request parsing failure into a response.
///
/// The body describes the class of failure without repeating the input, so
/// that a malformed request cannot be used to reflect arbitrary text.
fn error_response(error: RequestError) -> Response {
match error {
RequestError::TooLarge(detail) => Response::error(Status::ContentTooLarge, detail),
RequestError::Malformed(detail) => Response::error(Status::BadRequest, detail),
RequestError::Io(_) => {
Response::error(Status::BadRequest, "the connection could not be read")
}
}
}
/// Calls the handler, converting a panic into a server error response.
///
/// A handler is the only place where an unexpected condition can surface, so
/// it is the only place that needs this guard. Catching the panic keeps one
/// bad request from tearing down a worker thread.
fn dispatch<H: Handler>(handler: &H, request: &Request, peer: SocketAddr) -> Response {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handler.handle(request, peer)
}))
.unwrap_or_else(|_| Response::error(Status::InternalServerError, "internal server error"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
#[test]
fn serves_a_request_over_a_real_socket() {
let server = Server::bind_address(
"127.0.0.1:0"
.parse()
.expect("the literal address must parse"),
ServerConfig {
workers: 2,
..ServerConfig::default()
},
)
.unwrap();
let address = server.local_address().unwrap();
let handler =
Arc::new(|_request: &Request, _peer: SocketAddr| Response::json("{}".to_string()));
let running = thread::spawn(move || server.run(handler));
let mut stream = TcpStream::connect(address).unwrap();
stream
.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
.unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.ends_with("{}"));
drop(stream);
// The accept loop ends when the listener is dropped with the thread.
let _ = running;
}
#[test]
fn converts_parse_failures_into_statuses() {
assert_eq!(
error_response(RequestError::Malformed("x")).status,
Status::BadRequest
);
assert_eq!(
error_response(RequestError::TooLarge("x")).status,
Status::ContentTooLarge
);
}
}