use std::sync::Arc;
use std::time::Duration;
use gregg_protocol::{ReadinessState, SCHEMA_VERSION_V1};
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tracing::info;
use crate::collector::SystemCollector;
use crate::config::Config;
use crate::sampler::{RealClock, Sampler};
use crate::server::error::ServerError;
use crate::server::{Config as ServerConfig, ServerState};
const SHUTDOWN_DEADLINE: Duration = Duration::from_secs(10);
#[derive(Debug)]
pub(crate) enum RunOutcome {
#[allow(dead_code)]
Signal(&'static str),
Server(Result<Result<(), ServerError>, tokio::task::JoinError>),
Sampler(Result<(), tokio::task::JoinError>),
}
impl RunOutcome {
#[allow(clippy::match_same_arms)]
fn into_result(self) -> Result<(), Box<dyn std::error::Error>> {
match self {
Self::Signal(_) => Ok(()),
Self::Server(Ok(Ok(()))) => Err("HTTP server exited unexpectedly".into()),
Self::Server(Ok(Err(e))) => Err(Box::new(e)),
Self::Server(Err(e)) => Err(Box::new(e)),
Self::Sampler(Ok(())) => Err("sampler exited unexpectedly".into()),
Self::Sampler(Err(e)) => Err(Box::new(e)),
}
}
}
pub async fn run<C: SystemCollector + 'static>(
collector: C,
config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
run_with_shutdown(collector, config, wait_for_shutdown_signal()).await
}
#[cfg(unix)]
pub async fn run_with_control_path<C: SystemCollector + 'static>(
collector: C,
config: Config,
config_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
let shutdown = shutdown_with_control(config_path)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
run_with_shutdown(collector, config, shutdown).await
}
pub async fn run_with_control_path_or_default<C: SystemCollector + 'static>(
collector: C,
config: Config,
config_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(unix)]
{
run_with_control_path(collector, config, config_path).await
}
#[cfg(target_os = "windows")]
{
let _ = config_path;
run(collector, config).await
}
}
#[allow(clippy::too_many_lines)]
pub async fn run_with_shutdown<C, S>(
collector: C,
config: Config,
shutdown: S,
) -> Result<(), Box<dyn std::error::Error>>
where
C: SystemCollector + 'static,
S: std::future::Future<Output = &'static str>,
{
run_with_shutdown_on_ready(collector, config, shutdown, || Ok(())).await
}
#[allow(clippy::too_many_lines)]
pub(crate) async fn run_with_shutdown_on_ready<C, S, F>(
collector: C,
config: Config,
shutdown: S,
on_ready: F,
) -> Result<(), Box<dyn std::error::Error>>
where
C: SystemCollector + 'static,
S: std::future::Future<Output = &'static str>,
F: FnOnce() -> Result<(), Box<dyn std::error::Error>>,
{
info!(
version = env!("CARGO_PKG_VERSION"),
schema_version = SCHEMA_VERSION_V1,
os = std::env::consts::OS,
arch = std::env::consts::ARCH,
"greggd starting"
);
let server_config = ServerConfig {
host: config.host(),
port: config.port(),
sample_interval_ms: config.sample_interval_ms(),
..ServerConfig::default()
};
if let Err(e) = server_config.validate() {
return Err(Box::new(e));
}
let interval_ms = match Sampler::<C, RealClock>::validate_interval(config.sample_interval_ms())
{
Ok(ms) => ms,
Err(e) => {
return Err(Box::new(e));
}
};
info!(
listen_addr = %server_config.socket_addr(),
sample_interval_ms = interval_ms,
stale_after_ms = config.stale_after_ms(),
"effective configuration"
);
let (shutdown_tx, _) = broadcast::channel::<()>(1);
let server_state =
ServerState::with_stale_policy(0, Duration::from_millis(config.stale_after_ms()));
let addr = server_config.socket_addr();
let listener = TcpListener::bind(addr)
.await
.map_err(|e| Box::new(ServerError::Bind(e)) as Box<dyn std::error::Error>)?;
on_ready()?;
let sampler_handle = {
let shutdown_rx = shutdown_tx.subscribe();
let state = server_state.clone();
let mut sampler = Sampler::with_interval(collector, RealClock, interval_ms)?;
tokio::spawn(async move {
sampler
.run(shutdown_rx, |readiness, snap, snap_v2| {
let state = state.clone();
async move {
sync_sampler_state(&state, readiness, snap, snap_v2).await;
}
})
.await;
})
};
let server_handle = {
let shutdown_rx = shutdown_tx.subscribe();
tokio::spawn(crate::server::serve(listener, server_state, shutdown_rx))
};
let mut server_handle = Some(server_handle);
let mut sampler_handle = Some(sampler_handle);
let outcome = supervise(&mut server_handle, &mut sampler_handle, shutdown).await;
let _ = shutdown_tx.send(());
join_remaining_tasks(server_handle, sampler_handle).await;
info!("greggd stopped");
outcome.into_result()
}
pub(crate) async fn supervise<S>(
server_handle: &mut Option<tokio::task::JoinHandle<Result<(), ServerError>>>,
sampler_handle: &mut Option<tokio::task::JoinHandle<()>>,
shutdown: S,
) -> RunOutcome
where
S: std::future::Future<Output = &'static str>,
{
let outcome = {
let mut server_fut = server_handle.as_mut().unwrap();
let mut sampler_fut = sampler_handle.as_mut().unwrap();
tokio::select! {
signal_result = shutdown => {
info!(reason = %signal_result, "shutdown signal received");
RunOutcome::Signal(signal_result)
}
result = &mut server_fut => {
match result {
Ok(Ok(())) => {
RunOutcome::Server(Ok(Ok(())))
}
Ok(Err(e)) => {
RunOutcome::Server(Ok(Err(e)))
}
Err(e) => {
RunOutcome::Server(Err(e))
}
}
}
result = &mut sampler_fut => {
match result {
Ok(()) => {
RunOutcome::Sampler(Ok(()))
}
Err(e) => {
RunOutcome::Sampler(Err(e))
}
}
}
}
};
match &outcome {
RunOutcome::Server(_) => {
let _ = server_handle.take();
}
RunOutcome::Sampler(_) => {
let _ = sampler_handle.take();
}
RunOutcome::Signal(_) => {}
}
outcome
}
pub(crate) async fn join_remaining_tasks(
server_handle: Option<tokio::task::JoinHandle<Result<(), ServerError>>>,
sampler_handle: Option<tokio::task::JoinHandle<()>>,
) {
join_remaining_tasks_with_deadline(server_handle, sampler_handle, SHUTDOWN_DEADLINE).await;
}
pub(crate) async fn join_remaining_tasks_with_deadline(
server_handle: Option<tokio::task::JoinHandle<Result<(), ServerError>>>,
sampler_handle: Option<tokio::task::JoinHandle<()>>,
deadline: Duration,
) {
let deadline_instant = tokio::time::Instant::now() + deadline;
if let Some(mut handle) = server_handle {
tokio::select! {
result = &mut handle => {
match result {
Ok(Ok(())) => info!("HTTP server shut down cleanly"),
Ok(Err(e)) => eprintln!("HTTP server error during shutdown: {e}"),
Err(e) => eprintln!("HTTP server task panicked during shutdown: {e}"),
}
}
() = tokio::time::sleep_until(deadline_instant) => {
eprintln!("HTTP server did not shut down within deadline; aborting");
handle.abort();
let _ = handle.await;
}
}
}
if let Some(mut handle) = sampler_handle {
tokio::select! {
result = &mut handle => {
match result {
Ok(()) => info!("sampler shut down cleanly"),
Err(e) => eprintln!("sampler error during shutdown: {e}"),
}
}
() = tokio::time::sleep_until(deadline_instant) => {
eprintln!("sampler did not shut down within deadline; aborting");
handle.abort();
let _ = handle.await;
}
}
}
}
async fn sync_sampler_state(
server_state: &ServerState,
readiness: ReadinessState,
snap: Option<Arc<gregg_protocol::StatusSnapshot>>,
snap_v2: Option<Arc<gregg_protocol::v2::StatusPayloadV2>>,
) {
match readiness {
ReadinessState::Ready => {
match (snap, snap_v2) {
(Some(snap), Some(snap_v2)) => {
server_state
.update_snapshot((*snap).clone(), (*snap_v2).clone())
.await;
}
(None, Some(snap_v2)) => {
server_state
.update_snapshot_v2_only((*snap_v2).clone())
.await;
}
(Some(snap), None) => {
server_state.update_snapshot_v1_only((*snap).clone()).await;
}
(None, None) => {
}
}
}
ReadinessState::Warming => {
server_state.set_warming().await;
}
ReadinessState::Failed => {
let msg = "collector failure";
server_state.set_failed(msg).await;
}
}
}
fn wait_for_shutdown_signal() -> impl std::future::Future<Output = &'static str> {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
async move {
let mut sigterm =
signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");
let mut sigint =
signal(SignalKind::interrupt()).expect("failed to register SIGINT handler");
tokio::select! {
_ = sigterm.recv() => "SIGTERM",
_ = sigint.recv() => "SIGINT",
}
}
}
#[cfg(not(unix))]
{
async {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for Ctrl-C");
"Ctrl-C"
}
}
}
#[cfg(unix)]
fn shutdown_with_control(
config_path: &std::path::Path,
) -> Result<impl std::future::Future<Output = &'static str>, crate::control::ControlSetupError> {
use crate::control;
let bound = control::bind_listener(config_path);
let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
let stop_rx = match bound {
control::ControlBind::Bound { listener, path } => {
let _handle = control::spawn_stop_task(listener, path, stop_tx);
Some(stop_rx)
}
control::ControlBind::NotBound => {
let _ = stop_tx;
return Err(crate::control::ControlSetupError::NoSecureControl {
primary: control::primary_control_path(config_path),
fallback: control::fallback_control_path(config_path),
});
}
};
Ok(async move {
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to register SIGTERM handler");
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
.expect("failed to register SIGINT handler");
if let Some(rx) = stop_rx {
let stop_fut = control::wait_for_stop_task(rx);
tokio::select! {
reason = stop_fut => reason.unwrap_or("control-stop"),
_ = sigterm.recv() => "SIGTERM",
_ = sigint.recv() => "SIGINT",
}
} else {
tokio::select! {
_ = sigterm.recv() => "SIGTERM",
_ = sigint.recv() => "SIGINT",
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collector::error::CollectError;
use crate::collector::{CollectedMetrics, SystemCollector};
use gregg_protocol::{MetricCapabilities, SystemIdentity};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
struct ReadinessCollector;
impl SystemCollector for ReadinessCollector {
fn identity(&self) -> Result<SystemIdentity, CollectError> {
Ok(SystemIdentity {
name: "test".into(),
hostname: "test".into(),
os_name: "test".into(),
os_version: "test".into(),
kernel_name: "test".into(),
kernel_release: "test".into(),
architecture: "test".into(),
})
}
fn sample(&mut self) -> Result<CollectedMetrics, CollectError> {
Err(CollectError::warming("readiness test"))
}
fn capabilities(&self) -> MetricCapabilities {
MetricCapabilities { cpu_iowait: false }
}
}
fn readiness_test_config(port: u16) -> Config {
Config {
name: "readiness-test".into(),
host: IpAddr::V4(Ipv4Addr::LOCALHOST),
port,
sample_interval_ms: 250,
stale_after_ms: 1000,
}
}
fn unused_local_port() -> u16 {
let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.expect("test listener should bind");
listener
.local_addr()
.expect("test listener should have an address")
.port()
}
fn spawn_server(
result: Result<(), ServerError>,
) -> tokio::task::JoinHandle<Result<(), ServerError>> {
tokio::spawn(async move { result })
}
fn spawn_server_slow(
result: Result<(), ServerError>,
) -> tokio::task::JoinHandle<Result<(), ServerError>> {
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
result
})
}
fn spawn_sampler_ok() -> tokio::task::JoinHandle<()> {
tokio::spawn(async {})
}
fn spawn_sampler_slow() -> tokio::task::JoinHandle<()> {
tokio::spawn(async {
tokio::time::sleep(Duration::from_millis(50)).await;
})
}
fn spawn_sampler_panic() -> tokio::task::JoinHandle<()> {
tokio::spawn(async {
panic!("sampler panic");
})
}
fn spawn_server_panic() -> tokio::task::JoinHandle<Result<(), ServerError>> {
tokio::spawn(async {
panic!("server panic");
})
}
fn spawn_server_never() -> tokio::task::JoinHandle<Result<(), ServerError>> {
tokio::spawn(async {
std::future::pending::<()>().await;
Ok(())
})
}
#[tokio::test]
async fn server_error_shuts_down_and_joins_sampler() {
let mut server = Some(spawn_server(Err(ServerError::Bind(std::io::Error::new(
std::io::ErrorKind::AddrInUse,
"test",
)))));
let mut sampler = Some(spawn_sampler_slow());
let outcome = supervise(&mut server, &mut sampler, std::future::pending()).await;
assert!(server.is_none());
assert!(sampler.is_some());
match &outcome {
RunOutcome::Server(Ok(Err(_))) => {}
other => panic!("expected Server error, got {other:?}"),
}
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn server_panic_shuts_down_and_joins_sampler() {
let mut server = Some(spawn_server_panic());
let mut sampler = Some(spawn_sampler_slow());
let outcome = supervise(&mut server, &mut sampler, std::future::pending()).await;
assert!(server.is_none());
assert!(sampler.is_some());
match &outcome {
RunOutcome::Server(Err(_)) => {}
other => panic!("expected Server panic, got {other:?}"),
}
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn unexpected_clean_server_exit_is_failure_and_joins_sampler() {
let mut server = Some(spawn_server(Ok(())));
let mut sampler = Some(spawn_sampler_slow());
let outcome = supervise(&mut server, &mut sampler, std::future::pending()).await;
assert!(server.is_none());
assert!(sampler.is_some());
match &outcome {
RunOutcome::Server(Ok(Ok(()))) => {}
other => panic!("expected Server Ok(Ok(())), got {other:?}"),
}
assert!(outcome.into_result().is_err());
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn sampler_panic_shuts_down_and_joins_server() {
let mut server = Some(spawn_server_slow(Ok(())));
let mut sampler = Some(spawn_sampler_panic());
let outcome = supervise(&mut server, &mut sampler, std::future::pending()).await;
assert!(sampler.is_none());
assert!(server.is_some());
match &outcome {
RunOutcome::Sampler(Err(_)) => {}
other => panic!("expected Sampler panic, got {other:?}"),
}
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn unexpected_clean_sampler_exit_is_failure_and_joins_server() {
let mut server = Some(spawn_server_slow(Ok(())));
let mut sampler = Some(spawn_sampler_ok());
let outcome = supervise(&mut server, &mut sampler, std::future::pending()).await;
assert!(sampler.is_none());
assert!(server.is_some());
match &outcome {
RunOutcome::Sampler(Ok(())) => {}
other => panic!("expected Sampler Ok(()), got {other:?}"),
}
assert!(outcome.into_result().is_err());
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn signal_shutdown_joins_both_tasks_and_returns_success() {
let mut server = Some(spawn_server(Ok(())));
let mut sampler = Some(spawn_sampler_ok());
let shutdown = async { "test-signal" };
let outcome = supervise(&mut server, &mut sampler, shutdown).await;
assert!(server.is_some());
assert!(sampler.is_some());
match &outcome {
RunOutcome::Signal(s) => assert_eq!(*s, "test-signal"),
other => panic!("expected Signal, got {other:?}"),
}
assert!(outcome.into_result().is_ok());
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn non_cooperative_task_is_aborted_after_deadline() {
let mut server = Some(spawn_server_never());
let mut sampler = Some(spawn_sampler_ok());
let shutdown = async { "test-signal" };
let outcome = supervise(&mut server, &mut sampler, shutdown).await;
assert!(server.is_some());
assert!(sampler.is_some());
join_remaining_tasks_with_deadline(server, sampler, Duration::from_millis(100)).await;
assert!(outcome.into_result().is_ok());
}
#[tokio::test]
async fn original_error_preserved_after_cleanup() {
let server_error = ServerError::Bind(std::io::Error::new(
std::io::ErrorKind::AddrInUse,
"original error",
));
let mut server = Some(spawn_server(Err(server_error)));
let mut sampler = Some(spawn_sampler_slow());
let outcome = supervise(&mut server, &mut sampler, std::future::pending()).await;
let result = outcome.into_result();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("AddrInUse") || err.contains("bind") || err.contains("Bind"),
"expected original error in: {err}"
);
join_remaining_tasks(server, sampler).await;
}
#[tokio::test]
async fn join_remaining_tasks_handles_none() {
let none: Option<tokio::task::JoinHandle<Result<(), ServerError>>> = None;
let none2: Option<tokio::task::JoinHandle<()>> = None;
join_remaining_tasks(none, none2).await;
let done = Some(spawn_server(Ok(())));
let done2: Option<tokio::task::JoinHandle<()>> = None;
join_remaining_tasks(done, done2).await;
}
#[tokio::test]
async fn readiness_runs_once_after_successful_bind() {
let calls = AtomicUsize::new(0);
let result = run_with_shutdown_on_ready(
ReadinessCollector,
readiness_test_config(unused_local_port()),
async { "test-signal" },
|| {
calls.fetch_add(1, Ordering::SeqCst);
Ok(())
},
)
.await;
assert!(result.is_ok());
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn bind_failure_does_not_publish_readiness() {
let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.expect("test listener should bind");
let port = listener
.local_addr()
.expect("test listener should have an address")
.port();
let calls = AtomicUsize::new(0);
let result = run_with_shutdown_on_ready(
ReadinessCollector,
readiness_test_config(port),
async { "test-signal" },
|| {
calls.fetch_add(1, Ordering::SeqCst);
Ok(())
},
)
.await;
assert!(result.is_err());
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[tokio::test]
async fn readiness_failure_stops_startup_before_spawning_tasks() {
let calls = AtomicUsize::new(0);
let result = run_with_shutdown_on_ready(
ReadinessCollector,
readiness_test_config(unused_local_port()),
async { "test-signal" },
|| {
calls.fetch_add(1, Ordering::SeqCst);
Err("readiness failed".into())
},
)
.await;
assert!(result.is_err());
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn run_outcome_signal_is_success() {
let outcome = RunOutcome::Signal("SIGTERM");
assert!(outcome.into_result().is_ok());
}
#[test]
fn run_outcome_server_error_is_failure() {
let outcome = RunOutcome::Server(Ok(Err(ServerError::Bind(std::io::Error::new(
std::io::ErrorKind::AddrInUse,
"test",
)))));
assert!(outcome.into_result().is_err());
}
#[test]
fn run_outcome_server_clean_exit_is_failure() {
let outcome = RunOutcome::Server(Ok(Ok(())));
assert!(outcome.into_result().is_err());
}
#[tokio::test]
async fn run_outcome_server_panic_is_failure() {
let join_error = spawn_server_panic().await.unwrap_err();
let outcome = RunOutcome::Server(Err(join_error));
assert!(outcome.into_result().is_err());
}
#[test]
fn run_outcome_sampler_clean_exit_is_failure() {
let outcome = RunOutcome::Sampler(Ok(()));
assert!(outcome.into_result().is_err());
}
#[tokio::test]
async fn run_outcome_sampler_panic_is_failure() {
let join_error = spawn_sampler_panic().await.unwrap_err();
let outcome = RunOutcome::Sampler(Err(join_error));
assert!(outcome.into_result().is_err());
}
#[test]
fn shutdown_deadline_is_bounded() {
assert!(SHUTDOWN_DEADLINE >= Duration::from_secs(5));
assert!(SHUTDOWN_DEADLINE <= Duration::from_secs(30));
}
}