use futures::FutureExt; use hydrant::config::Config; use hydrant::control::{ApiBinds, Hydrant}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; const DEFAULT_API_PORT: u16 = 3000; const DEFAULT_DEBUG_PORT: u16 = DEFAULT_API_PORT + 1; fn default_api_binds() -> ApiBinds { ApiBinds::try_from_iter([ SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_API_PORT), SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), DEFAULT_API_PORT), ]) .expect("two-element literal is non-empty") } struct AppConfig { api_binds: ApiBinds, enable_debug: bool, debug_port: u16, } impl AppConfig { fn from_env() -> miette::Result { use hydrant::__cfg as cfg; let api_binds = parse_api_binds()?; let enable_debug = cfg!("ENABLE_DEBUG", false); let debug_port_default = api_binds .iter() .next() .expect("ApiBinds is non-empty by construction") .port() .checked_add(1) .unwrap_or(DEFAULT_DEBUG_PORT); let debug_port = cfg!("DEBUG_PORT", debug_port_default); Ok(Self { api_binds, enable_debug, debug_port, }) } } fn parse_api_binds() -> miette::Result { let Ok(raw) = std::env::var("HYDRANT_API_BIND") else { return Ok(default_api_binds()); }; let parsed = raw .split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(|s| { s.parse::() .map_err(|e| miette::miette!("invalid HYDRANT_API_BIND entry `{s}`: {e}")) }) .collect::>>()?; ApiBinds::try_from_iter(parsed) .ok_or_else(|| miette::miette!("HYDRANT_API_BIND is set but contains no addresses")) } #[cfg(feature = "alloc-mimalloc")] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[cfg(feature = "alloc-jemalloc")] #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; // bake the tuned jemalloc runtime config into the binary. the prefixed tikv build reads // the `_rjem_malloc_conf` symbol (plain `malloc_conf` silently no-ops). `percpu_arena` // pins one arena per cpu, bounding per-arena dirty-page retention — the dominant rss lever // here (~2.4x less resident than the default 4*ncpu arenas); `background_thread` purges // asynchronously so `dirty_decay_ms:1000`/`muzzy_decay_ms:0` return pages without sync // munmap churn. measured 1.72 GiB steady vs 4.14 for mimalloc. both options are linux-only // (they no-op or warn elsewhere), so gate to linux; other targets get vanilla jemalloc. // overridable at runtime via _RJEM_MALLOC_CONF. #[cfg(all(feature = "alloc-jemalloc", target_os = "linux"))] #[allow(non_upper_case_globals)] #[unsafe(export_name = "_rjem_malloc_conf")] pub static _rjem_malloc_conf: Option<&'static core::ffi::CStr> = Some(c"background_thread:true,percpu_arena:percpu,dirty_decay_ms:1000,muzzy_decay_ms:0"); #[cfg(feature = "alloc-snmalloc")] #[global_allocator] static GLOBAL: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc; #[tokio::main] async fn main() -> miette::Result<()> { rustls::crypto::aws_lc_rs::default_provider() .install_default() .ok(); let cfg = Config::from_env()?; let app = AppConfig::from_env()?; let env_filter = tracing_subscriber::EnvFilter::builder() .with_default_directive(tracing::Level::INFO.into()) .from_env_lossy(); tracing_subscriber::fmt().with_env_filter(env_filter).init(); let hydrant = Hydrant::new(cfg).await?; let debug_fut = app .enable_debug .then(|| hydrant.serve_debug(app.debug_port).boxed()) .unwrap_or_else(|| std::future::pending().boxed()); tokio::select! { r = hydrant.run()? => r, r = hydrant.serve(app.api_binds) => r, r = debug_fut => r, } }