use crate::application::{ServeState, dispatch_via_h1};
use crate::h1_backend::h2_fallback::HyperH2;
use crate::logging::{error, info};
use crate::pipeline::PipelineConfig;
use armature_h1::{Config, Limits, Server, ServerHandle, TcpConfig};
use hyper_util::rt::TokioExecutor;
use std::net::SocketAddr;
use std::time::Duration;
const NO_DEADLINE: Duration = Duration::from_secs(365 * 24 * 60 * 60);
struct ShutdownOnDrop(ServerHandle);
impl Drop for ShutdownOnDrop {
fn drop(&mut self) {
self.0.shutdown();
}
}
pub(crate) fn h1_config(
addr: SocketAddr,
pipeline: &PipelineConfig,
workers: Option<usize>,
) -> Config {
let limits = Limits {
max_head_bytes: pipeline.max_header_size,
max_body_bytes: u64::MAX,
idle_timeout: pipeline.keep_alive_timeout,
body_timeout: pipeline.request_timeout.unwrap_or(NO_DEADLINE),
write_timeout: pipeline.write_timeout.unwrap_or(NO_DEADLINE),
..Limits::default()
};
let mut cfg = Config::new(addr)
.limits(limits)
.pin_cores(false);
cfg.tcp = TcpConfig {
nodelay: pipeline.tcp_nodelay,
..TcpConfig::default()
};
if let Some(n) = workers {
cfg = cfg.workers(n);
}
cfg
}
pub(crate) async fn serve(
cfg: Config,
state: ServeState,
h2: Option<hyper::server::conn::http2::Builder<TokioExecutor>>,
) -> Result<(), crate::Error> {
serve_bound(cfg, state, h2, |_, _| {}).await
}
pub(crate) async fn serve_bound(
cfg: Config,
state: ServeState,
h2: Option<hyper::server::conn::http2::Builder<TokioExecutor>>,
on_bound: impl FnOnce(SocketAddr, ServerHandle),
) -> Result<(), crate::Error> {
let requested = cfg.addr;
let tls = cfg.tls.is_some();
let workers = cfg.workers;
let idle_timeout = cfg.limits.idle_timeout;
let server = Server::bind(cfg).map_err(|e| {
error!(address = %requested, error = %e, "failed to bind");
crate::Error::Io(e)
})?;
let addr = server.local_addr();
info!(
address = %addr,
tls,
workers,
?idle_timeout,
"server listening (armature-h1 backend)"
);
on_bound(addr, server.handle());
let _stop = ShutdownOnDrop(server.handle());
let joined = tokio::task::spawn_blocking(move || match h2 {
Some(builder) => server.serve_with_fallback(
{
let state = state.clone();
move || {
let state = state.clone();
move |req| dispatch_via_h1(req, state.clone())
}
},
move || HyperH2::new(state.clone(), builder.clone()),
),
None => server.serve({
let state = state.clone();
move || {
let state = state.clone();
move |req| dispatch_via_h1(req, state.clone())
}
}),
})
.await;
match joined {
Ok(Ok(())) if !_stop.0.is_shutting_down() => {
error!(address = %addr, "armature-h1 workers stopped without a shutdown signal");
Err(crate::Error::Internal(format!(
"armature-h1 server on {addr} exited without a shutdown signal; \
its worker threads stopped before serving"
)))
}
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(crate::Error::from(e)),
Err(join) => {
error!(error = %join, "the armature-h1 server thread did not exit cleanly");
Err(crate::Error::Internal(format!(
"armature-h1 server thread panicked: {join}"
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn addr() -> SocketAddr {
"127.0.0.1:0".parse().expect("addr")
}
#[test]
fn the_two_previously_unwired_pipeline_fields_reach_the_limits() {
let pipeline = PipelineConfig {
keep_alive_timeout: Duration::from_secs(7),
max_header_size: 4096,
tcp_nodelay: false,
..PipelineConfig::default()
};
let cfg = h1_config(addr(), &pipeline, Some(2));
assert_eq!(
cfg.limits.idle_timeout,
Duration::from_secs(7),
"keep_alive_timeout has an idle-timeout knob under armature-h1"
);
assert_eq!(
cfg.limits.max_head_bytes, 4096,
"max_header_size is a byte cap and armature-h1 takes one"
);
assert_eq!(
cfg.limits.max_body_bytes,
u64::MAX,
"armature-core enforces the body cap on this path, so armature-h1 \
must not reject first with its bare status line and cost the \
client the framework's JSON envelope"
);
assert!(!cfg.tcp.nodelay);
assert_eq!(cfg.workers, 2);
}
#[test]
fn the_request_and_write_deadlines_default_asymmetrically() {
let cfg = h1_config(addr(), &PipelineConfig::default(), None);
assert!(
cfg.limits.body_timeout >= Duration::from_secs(300 * 24 * 60 * 60),
"no handler deadline unless the caller asks for one: inheriting \
armature-h1's 30s default would cancel every long-poll and slow \
upload with a bare 408, no envelope, and nothing logged"
);
assert_eq!(
cfg.limits.write_timeout,
Duration::from_secs(300),
"a write deadline must stay finite: armature-h1 caps neither \
connection count nor write duration, so an unbounded one lets a \
client that stops reading hold a worker slot for free"
);
}
#[test]
fn a_configured_request_timeout_reaches_the_handler_deadline() {
let pipeline = PipelineConfig {
request_timeout: Some(Duration::from_secs(45)),
write_timeout: None,
..PipelineConfig::default()
};
let cfg = h1_config(addr(), &pipeline, None);
assert_eq!(
cfg.limits.body_timeout,
Duration::from_secs(45),
"a deployment that knows its handlers' upper bound must be able to \
say so"
);
assert!(
cfg.limits.write_timeout >= Duration::from_secs(300 * 24 * 60 * 60),
"and must be able to opt out of the write deadline explicitly"
);
}
#[test]
fn core_pinning_is_left_to_the_caller() {
assert!(!h1_config(addr(), &PipelineConfig::default(), None).pin_cores);
}
#[test]
fn an_over_ceiling_header_count_is_clamped_by_config() {
let cfg = h1_config(addr(), &PipelineConfig::default(), None);
assert!(cfg.limits.max_headers <= armature_h1::limits::MAX_HEADERS_CEILING);
}
}