Skip to content

Commit b111c55

Browse files
authored
ref(server): Modularize startup code and add middlewares (#74)
Refactors the server startup code, inspired by Relay's startup routines. This separates routes from middlewares, binds an explicit listener so we can control the listen backlog, and adds middlewares to track request errors and panics.
1 parent 485841d commit b111c55

File tree

4 files changed

+74
-18
lines changed

4 files changed

+74
-18
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

objectstore-server/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ serde_json = "1.0.140"
2626
tokio = { version = "1.45.1", features = ["full"] }
2727
tokio-stream = "0.1.17"
2828
tower = { version = "0.5.2" }
29+
tower-http = { version = "0.6.6", default-features = false, features = [
30+
"catch-panic",
31+
"trace",
32+
] }
2933
tracing = { version = "0.1.41" }
3034
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
3135
uuid = { version = "1.17.0", features = ["v4", "v7"] }

objectstore-server/src/http.rs

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
use std::any::Any;
44
use std::io;
5+
use std::net::SocketAddr;
56
use std::sync::Arc;
67

8+
use anyhow::Result;
79
use axum::body::{Body, to_bytes};
810
use axum::extract::{Path, Request, State};
911
use axum::http::{HeaderMap, StatusCode};
@@ -16,34 +18,81 @@ use objectstore_service::{ObjectKey, StorageService};
1618
use objectstore_types::Metadata;
1719
use sentry::integrations::tower as sentry_tower;
1820
use serde::Serialize;
21+
use tokio::net::{TcpListener, TcpSocket};
22+
use tower::ServiceBuilder;
23+
use tower_http::catch_panic::CatchPanicLayer;
24+
use tower_http::trace::{DefaultOnFailure, TraceLayer};
25+
use tracing::Level;
1926
use uuid::Uuid;
2027

2128
use crate::authentication::{Claim, ExtractScope, Permission};
2229
use crate::config::Config;
2330
use crate::state::ServiceState;
2431

25-
pub async fn start_server(state: ServiceState) {
26-
let sentry_tower_service = state.config.sentry_dsn.as_ref().map(|_| {
27-
tower::ServiceBuilder::new()
28-
.layer(sentry_tower::NewSentryLayer::<Request>::new_from_top())
29-
.layer(sentry_tower::SentryHttpLayer::new().enable_transaction())
30-
});
31-
let http_addr = state.config.http_addr;
32+
const TCP_LISTEN_BACKLOG: u32 = 1024;
33+
34+
fn make_app(state: ServiceState) -> axum::Router {
35+
let middleware = ServiceBuilder::new()
36+
.layer(CatchPanicLayer::custom(handle_panic))
37+
.layer(sentry_tower::NewSentryLayer::<Request>::new_from_top())
38+
.layer(sentry_tower::SentryHttpLayer::new().enable_transaction())
39+
.layer(TraceLayer::new_for_http().on_failure(DefaultOnFailure::new().level(Level::DEBUG)));
3240

33-
let app = Router::new()
41+
let routes = Router::new()
3442
.route("/", put(put_blob))
35-
.route("/{*key}", get(get_blob).delete(delete_blob))
36-
.layer(option_layer(sentry_tower_service))
37-
.with_state(state)
38-
.into_make_service();
43+
.route("/{*key}", get(get_blob).delete(delete_blob));
44+
45+
routes.layer(middleware).with_state(state)
46+
}
47+
48+
/// Handler function for the [`CatchPanicLayer`] middleware.
49+
fn handle_panic(err: Box<dyn Any + Send + 'static>) -> Response {
50+
let detail = if let Some(s) = err.downcast_ref::<String>() {
51+
s.clone()
52+
} else if let Some(s) = err.downcast_ref::<&str>() {
53+
s.to_string()
54+
} else {
55+
"no error details".to_owned()
56+
};
57+
58+
tracing::error!("panic in web handler: {detail}");
59+
60+
let response = (StatusCode::INTERNAL_SERVER_ERROR, detail);
61+
response.into_response()
62+
}
63+
64+
fn listen(config: &Config) -> Result<TcpListener> {
65+
let addr = config.http_addr;
66+
let socket = match addr {
67+
SocketAddr::V4(_) => TcpSocket::new_v4(),
68+
SocketAddr::V6(_) => TcpSocket::new_v6(),
69+
}?;
3970

40-
tracing::info!("HTTP server listening on {http_addr}");
71+
#[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))]
72+
socket.set_reuseport(true)?;
73+
socket.bind(addr)?;
74+
75+
let listener = socket.listen(TCP_LISTEN_BACKLOG)?;
76+
tracing::info!("HTTP server listening on {addr}");
77+
78+
Ok(listener)
79+
}
80+
81+
async fn serve(listener: TcpListener, app: axum::Router) -> Result<()> {
4182
let guard = elegant_departure::get_shutdown_guard().shutdown_on_drop();
42-
let listener = tokio::net::TcpListener::bind(http_addr).await.unwrap();
43-
axum::serve(listener, app)
83+
axum::serve(listener, app.into_make_service())
4484
.with_graceful_shutdown(guard.wait_owned())
45-
.await
46-
.unwrap();
85+
.await?;
86+
87+
Ok(())
88+
}
89+
90+
pub async fn server(state: ServiceState) -> Result<()> {
91+
let http_addr = state.config.http_addr;
92+
let listener = listen(&state.config)?;
93+
94+
let app = make_app(state);
95+
serve(listener, app).await
4796
}
4897

4998
#[derive(Debug, Serialize)]

objectstore-server/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async fn main() -> Result<()> {
9898

9999
tracing::debug!(?config, "Starting service");
100100
let state = State::new(config).await?;
101-
tokio::spawn(http::start_server(state));
101+
tokio::spawn(http::server(state));
102102

103103
elegant_departure::tokio::depart()
104104
.on_termination()

0 commit comments

Comments
 (0)