lspkit_sidecar/
lifecycle.rs1use std::time::Duration;
8
9use tokio::process::{Child, Command};
10
11#[non_exhaustive]
13#[derive(Debug, Clone)]
14pub struct LifecycleConfig {
15 pub ping_interval: Duration,
17 pub ping_timeout: Duration,
19 pub initial_backoff: Duration,
21 pub max_backoff: Duration,
23 pub max_restarts: u32,
25}
26
27impl Default for LifecycleConfig {
28 fn default() -> Self {
29 Self {
30 ping_interval: Duration::from_secs(5),
31 ping_timeout: Duration::from_secs(2),
32 initial_backoff: Duration::from_secs(1),
33 max_backoff: Duration::from_secs(30),
34 max_restarts: u32::MAX,
35 }
36 }
37}
38
39#[non_exhaustive]
41#[derive(Debug, thiserror::Error)]
42pub enum LifecycleError {
43 #[error("spawn failed: {0}")]
45 Spawn(#[source] std::io::Error),
46 #[error("restart budget exhausted after {0} attempts")]
48 BudgetExhausted(u32),
49}
50
51pub struct Sidecar {
53 program: String,
54 args: Vec<String>,
55 child: Option<Child>,
56 config: LifecycleConfig,
57 restart_count: u32,
58}
59
60impl Sidecar {
61 #[must_use]
63 pub fn new(program: impl Into<String>, args: Vec<String>, config: LifecycleConfig) -> Self {
64 Self {
65 program: program.into(),
66 args,
67 child: None,
68 config,
69 restart_count: 0,
70 }
71 }
72
73 pub fn spawn(&mut self) -> Result<(), LifecycleError> {
78 let child = Command::new(&self.program)
79 .args(&self.args)
80 .kill_on_drop(true)
81 .spawn()
82 .map_err(LifecycleError::Spawn)?;
83 self.child = Some(child);
84 Ok(())
85 }
86
87 pub async fn wait(&mut self) -> Option<std::process::ExitStatus> {
89 if let Some(child) = self.child.as_mut() {
90 child.wait().await.ok()
91 } else {
92 None
93 }
94 }
95
96 #[must_use]
99 pub fn next_backoff(&self) -> Duration {
100 let factor = 2_u32.saturating_pow(self.restart_count.min(20));
101 let scaled = self
102 .config
103 .initial_backoff
104 .checked_mul(factor)
105 .unwrap_or(self.config.max_backoff);
106 scaled.min(self.config.max_backoff)
107 }
108
109 pub fn record_restart(&mut self) -> u32 {
111 self.restart_count = self.restart_count.saturating_add(1);
112 self.restart_count
113 }
114
115 pub fn reset_restart_count(&mut self) {
117 self.restart_count = 0;
118 }
119
120 #[must_use]
122 pub fn config(&self) -> &LifecycleConfig {
123 &self.config
124 }
125}
126
127impl std::fmt::Debug for Sidecar {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("Sidecar")
130 .field("program", &self.program)
131 .field("running", &self.child.is_some())
132 .field("restarts", &self.restart_count)
133 .finish_non_exhaustive()
134 }
135}