use std::time::Duration;
use tokio::process::{Child, Command};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct LifecycleConfig {
pub ping_interval: Duration,
pub ping_timeout: Duration,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub max_restarts: u32,
}
impl Default for LifecycleConfig {
fn default() -> Self {
Self {
ping_interval: Duration::from_secs(5),
ping_timeout: Duration::from_secs(2),
initial_backoff: Duration::from_secs(1),
max_backoff: Duration::from_secs(30),
max_restarts: u32::MAX,
}
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum LifecycleError {
#[error("spawn failed: {0}")]
Spawn(#[source] std::io::Error),
#[error("restart budget exhausted after {0} attempts")]
BudgetExhausted(u32),
}
pub struct Sidecar {
program: String,
args: Vec<String>,
child: Option<Child>,
config: LifecycleConfig,
restart_count: u32,
}
impl Sidecar {
#[must_use]
pub fn new(program: impl Into<String>, args: Vec<String>, config: LifecycleConfig) -> Self {
Self {
program: program.into(),
args,
child: None,
config,
restart_count: 0,
}
}
pub fn spawn(&mut self) -> Result<(), LifecycleError> {
let child = Command::new(&self.program)
.args(&self.args)
.kill_on_drop(true)
.spawn()
.map_err(LifecycleError::Spawn)?;
self.child = Some(child);
Ok(())
}
pub async fn wait(&mut self) -> Option<std::process::ExitStatus> {
if let Some(child) = self.child.as_mut() {
child.wait().await.ok()
} else {
None
}
}
#[must_use]
pub fn next_backoff(&self) -> Duration {
let factor = 2_u32.saturating_pow(self.restart_count.min(20));
let scaled = self
.config
.initial_backoff
.checked_mul(factor)
.unwrap_or(self.config.max_backoff);
scaled.min(self.config.max_backoff)
}
pub fn record_restart(&mut self) -> u32 {
self.restart_count = self.restart_count.saturating_add(1);
self.restart_count
}
pub fn reset_restart_count(&mut self) {
self.restart_count = 0;
}
#[must_use]
pub fn config(&self) -> &LifecycleConfig {
&self.config
}
}
impl std::fmt::Debug for Sidecar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Sidecar")
.field("program", &self.program)
.field("running", &self.child.is_some())
.field("restarts", &self.restart_count)
.finish_non_exhaustive()
}
}