#![cfg_attr(
not(test),
warn(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)
)]
mod access_log;
mod admin;
mod body;
mod compression;
mod conn_limit;
mod dispatch;
mod dns;
mod error;
mod forward;
mod h3;
mod headers;
mod health;
mod listener;
mod metrics;
mod otlp;
mod proxy;
pub mod proxy_protocol;
mod respond;
mod retry;
mod tunnel;
mod upstream_client;
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::combinators::{BoxBody, UnsyncBoxBody};
use hyper::header::HeaderValue;
use hyper_util::client::legacy::connect::HttpConnector;
use plecto_control::Control;
use tokio::sync::Semaphore;
use crate::metrics::ServerMetrics;
use crate::upstream_client::UpstreamClients;
pub use listener::{DEFAULT_DRAIN_DEADLINE, serve, serve_with_shutdown};
const DEFAULT_MALLOC_ARENA_MAX: i32 = 32;
fn parse_malloc_arena_max(raw: Option<&str>) -> i32 {
raw.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(DEFAULT_MALLOC_ARENA_MAX)
}
pub fn cap_malloc_arenas() {
let max = parse_malloc_arena_max(std::env::var("PLECTO_MALLOC_ARENA_MAX").ok().as_deref());
apply_arena_cap(max);
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn apply_arena_cap(max: i32) {
if max <= 0 {
return; }
unsafe {
libc::mallopt(libc::M_ARENA_MAX, max as core::ffi::c_int);
}
}
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
fn apply_arena_cap(_max: i32) {}
pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub(crate) type ResponseBody = UnsyncBoxBody<Bytes, BoxError>;
pub(crate) type ReqBody = BoxBody<Bytes, BoxError>;
pub(crate) const MAX_CONNECTIONS: usize = 10_000;
pub(crate) const MAX_CONNECTIONS_PER_IP: u32 = 256;
#[cfg(unix)]
pub fn raise_nofile_limit() {
match unix_raise_nofile_limit() {
Ok((soft, hard)) => {
let wanted = MAX_CONNECTIONS as u64 * 4;
if soft < wanted {
tracing::warn!(
soft,
hard,
wanted,
"RLIMIT_NOFILE is below the recommended floor for MAX_CONNECTIONS; raise the \
HARD limit externally (systemd LimitNOFILE=, docker --ulimit nofile=, or \
ulimit -Hn) — Plecto does not request CAP_SYS_RESOURCE to do this itself"
);
} else {
tracing::info!(soft, hard, "RLIMIT_NOFILE raised to the hard limit");
}
}
Err(e) => {
tracing::warn!(
error = %e,
"failed to read/raise RLIMIT_NOFILE; leaving the OS default in place"
);
}
}
}
#[cfg(not(unix))]
pub fn raise_nofile_limit() {}
#[cfg(unix)]
fn unix_raise_nofile_limit() -> std::io::Result<(u64, u64)> {
let mut rlim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut rlim) } != 0 {
return Err(std::io::Error::last_os_error());
}
let hard = rlim.rlim_max;
if rlim.rlim_cur < hard {
rlim.rlim_cur = hard;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) } != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok((rlim.rlim_cur, hard))
}
pub(crate) const MAX_CONCURRENT_STREAMS: u32 = 100;
pub(crate) const MAX_INFLIGHT_REQUESTS: usize = 1024;
pub(crate) struct ServerState {
control: Arc<Control>,
clients: UpstreamClients,
alt_svc: Option<HeaderValue>,
trusted_proxy: Option<plecto_control::TrustedProxyTrust>,
conn_limit: Arc<Semaphore>,
per_ip_conn_limit: Arc<conn_limit::PerIpConnLimit>,
body_buffer_budget: Arc<Semaphore>,
request_limit: Arc<Semaphore>,
metrics: Arc<ServerMetrics>,
otlp: Option<Arc<plecto_control::otlp::OtlpBuffer>>,
drain: tokio::sync::watch::Receiver<bool>,
ready: tokio::sync::watch::Receiver<bool>,
}
pub(crate) fn upstream_connector() -> HttpConnector {
let mut c = HttpConnector::new();
c.set_nodelay(true);
c.enforce_http(false);
c
}
#[cfg(test)]
mod alloc_tuning_tests {
#[cfg(all(target_os = "linux", target_env = "gnu"))]
#[test]
fn mallopt_arena_max_is_accepted_by_glibc() {
let rc = unsafe { libc::mallopt(libc::M_ARENA_MAX, super::DEFAULT_MALLOC_ARENA_MAX) };
assert_eq!(
rc, 1,
"glibc mallopt(M_ARENA_MAX, 32) should return 1 on success"
);
}
#[test]
fn cap_is_a_noop_when_disabled() {
super::apply_arena_cap(0);
}
#[test]
fn parse_malloc_arena_max_defaults_to_32() {
assert_eq!(super::DEFAULT_MALLOC_ARENA_MAX, 32);
assert_eq!(super::parse_malloc_arena_max(None), 32);
assert_eq!(super::parse_malloc_arena_max(Some("")), 32);
assert_eq!(super::parse_malloc_arena_max(Some("garbage")), 32);
assert_eq!(super::parse_malloc_arena_max(Some("32x")), 32);
assert_eq!(
super::parse_malloc_arena_max(Some("9999999999999999999999")),
32
);
}
#[test]
fn parse_malloc_arena_max_preserves_numbers_and_noop() {
assert_eq!(super::parse_malloc_arena_max(Some("1")), 1);
assert_eq!(super::parse_malloc_arena_max(Some("4")), 4);
assert_eq!(super::parse_malloc_arena_max(Some("16")), 16);
assert_eq!(super::parse_malloc_arena_max(Some("24")), 24);
assert_eq!(super::parse_malloc_arena_max(Some("32")), 32);
assert_eq!(super::parse_malloc_arena_max(Some("64")), 64);
assert_eq!(super::parse_malloc_arena_max(Some("0")), 0);
assert_eq!(super::parse_malloc_arena_max(Some("-1")), -1);
}
}
#[cfg(all(test, unix))]
mod nofile_tests {
#[test]
fn raises_soft_limit_to_match_the_hard_limit() {
let (soft, hard) =
super::unix_raise_nofile_limit().expect("getrlimit/setrlimit must succeed");
assert_eq!(
soft, hard,
"soft limit must be raised to match the hard limit"
);
let (soft2, hard2) =
super::unix_raise_nofile_limit().expect("a second call must also succeed");
assert_eq!((soft2, hard2), (soft, hard));
}
}