use std::any::Any;
use std::net::SocketAddr;
use std::sync::Arc;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
use super::service::{FlatbedService, ServiceContext};
use super::shutdown::shutdown_signal;
use crate::supervisor::supervise;
use crate::{get_worker_drains, get_workers};
#[derive(Clone, Copy)]
struct TokioExecutor;
impl<F> hyper::rt::Executor<F> for TokioExecutor
where
F: std::future::Future + Send + 'static,
F::Output: Send + 'static,
{
fn execute(&self, fut: F) {
tokio::spawn(fut);
}
}
pub struct AutoServer<C> {
bind_addr: SocketAddr,
service_ctx: ServiceContext<C>,
healthz_tx: watch::Sender<bool>,
shutdown_timeout_secs: u64,
}
impl<C: Clone + Send + Sync + 'static> AutoServer<C> {
pub fn new(
bind_addr: SocketAddr,
service_ctx: ServiceContext<C>,
healthz_tx: watch::Sender<bool>,
) -> Self {
Self {
bind_addr,
service_ctx,
healthz_tx,
shutdown_timeout_secs: 30,
}
}
pub fn shutdown_timeout(mut self, secs: u64) -> Self {
self.shutdown_timeout_secs = secs;
self
}
pub async fn serve(self) -> std::io::Result<()> {
let listener = TcpListener::bind(self.bind_addr).await?;
#[cfg(feature = "telemetry")]
if let Some(telemetry) = self.service_ctx.config.telemetry.as_ref() {
crate::supervisor::install_state_gauge(telemetry);
}
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
let shutdown_tx_clone = shutdown_tx.clone();
tokio::spawn(async move {
shutdown_signal().await;
let _ = shutdown_tx_clone.send(true);
});
let mut booted_rx = self.service_ctx.booted_rx.clone();
let context = Arc::clone(&self.service_ctx.context);
let healthz_tx = self.healthz_tx.clone();
let shutdown_tx_for_workers = shutdown_tx.clone();
tokio::spawn(async move {
loop {
if *booted_rx.borrow() {
break;
}
if booted_rx.changed().await.is_err() {
return; }
}
let ctx_guard = context.read().await;
let Some(app_ctx) = ctx_guard.as_ref() else {
warn!("ready signal received but context not set");
return;
};
let cloned: Arc<C> = Arc::clone(app_ctx);
drop(ctx_guard); let worker_ctx: Arc<dyn Any + Send + Sync> = cloned;
let workers = get_workers();
for w in &workers {
info!(
name = w.name,
description = w.description.unwrap_or("none"),
"registered worker"
);
}
for worker_info in workers {
tokio::spawn(supervise(
*worker_info,
worker_ctx.clone(),
healthz_tx.clone(),
shutdown_tx_for_workers.clone(),
));
}
});
let service_ctx = self.service_ctx;
loop {
tokio::select! {
result = listener.accept() => {
let (stream, _addr) = result?;
let service = FlatbedService::new(service_ctx.clone());
let mut conn_shutdown_rx = shutdown_rx.clone();
tokio::spawn(async move {
let io = TokioIo::new(stream);
let builder = hyper_util::server::conn::auto::Builder::new(TokioExecutor);
let conn = builder.serve_connection(io, service);
tokio::select! {
result = conn => {
if let Err(e) = result {
debug!(error = %e, "connection error");
}
}
_ = conn_shutdown_rx.changed() => {
}
}
});
}
_ = shutdown_rx.changed() => {
break;
}
}
}
let budget = tokio::time::Duration::from_secs(self.shutdown_timeout_secs);
let epilogue = CONNECTION_EPILOGUE.min(budget);
let drain_budget = budget
.saturating_sub(epilogue)
.max(tokio::time::Duration::from_secs(1));
let drained = run_worker_drains(&service_ctx, drain_budget).await;
tokio::time::sleep(if drained { epilogue } else { budget }).await;
Ok(())
}
}
const CONNECTION_EPILOGUE: tokio::time::Duration = tokio::time::Duration::from_secs(2);
async fn run_worker_drains<C: Clone + Send + Sync + 'static>(
service_ctx: &ServiceContext<C>,
budget: tokio::time::Duration,
) -> bool {
let drains = get_worker_drains();
if drains.is_empty() {
return false;
}
let worker_ctx: Arc<dyn Any + Send + Sync> = {
let guard = service_ctx.context.read().await;
let Some(app_ctx) = guard.as_ref() else {
warn!("context not initialised; skipping worker drains");
return false;
};
Arc::clone(app_ctx) as Arc<dyn Any + Send + Sync>
};
let handles: Vec<_> = drains
.iter()
.map(|drain_info| {
let name = drain_info.name;
let ctx = worker_ctx.clone();
let drain_fn = drain_info.drain;
let handle = tokio::spawn(async move {
if let Err(e) = drain_fn(ctx).await {
error!(worker = name, error = %e, "drain failed");
} else {
debug!(worker = name, "drain complete");
}
});
(name, handle)
})
.collect();
let _ = tokio::time::timeout(budget, async {
for (name, handle) in handles {
if let Err(e) = handle.await {
error!(worker = name, error = %e, "drain task did not complete");
}
}
})
.await;
true
}