hyperdb_mcp/daemon/
run.rs1use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use tokio::signal;
11use tracing::{error, info, warn};
12
13use hyperdb_api::{HyperProcess, Parameters, TransportMode};
14
15use super::discovery::{self, DaemonInfo};
16use super::health::{DaemonState, HealthListener};
17use super::{DEFAULT_IDLE_TIMEOUT_SECS, ENV_IDLE_TIMEOUT};
18
19#[derive(Debug)]
21pub struct DaemonConfig {
22 pub port: u16,
23 pub idle_timeout: Duration,
24}
25
26impl DaemonConfig {
27 pub fn from_args(port: u16, idle_timeout_secs: Option<u64>) -> Self {
28 let idle_timeout_secs = idle_timeout_secs
29 .or_else(|| {
30 std::env::var(ENV_IDLE_TIMEOUT)
31 .ok()
32 .and_then(|v| v.parse().ok())
33 })
34 .unwrap_or(DEFAULT_IDLE_TIMEOUT_SECS);
35
36 Self {
37 port,
38 idle_timeout: Duration::from_secs(idle_timeout_secs),
39 }
40 }
41}
42
43pub const RESTART_WINDOW: Duration = Duration::from_secs(60);
46pub const RESTART_LIMIT: usize = 3;
47
48const HYPERD_POLL_INTERVAL: Duration = Duration::from_secs(5);
50
51struct HyperState {
54 hyper: Option<HyperProcess>,
55 restart_history: Vec<Instant>,
56}
57
58#[derive(Debug)]
59enum RestartError {
60 TooManyRestarts,
62 SpawnFailed(String),
64}
65
66pub async fn run_daemon(config: DaemonConfig) -> Result<(), Box<dyn std::error::Error>> {
72 let listener = HealthListener::bind(config.port).map_err(|e| {
74 if e.kind() == std::io::ErrorKind::AddrInUse {
75 format!(
76 "Another hyperdb daemon is already running on port {}. \
77 Use `hyperdb-mcp daemon status` to check or `hyperdb-mcp daemon stop` to stop it.",
78 config.port
79 )
80 } else {
81 format!("Failed to bind health port {}: {e}", config.port)
82 }
83 })?;
84 let bound_port = listener.port;
85 info!(port = bound_port, "daemon health listener bound");
86
87 let hyper = HyperProcess::new(None, Some(&build_params()?))?;
89 let endpoint = hyper
90 .endpoint()
91 .ok_or("hyperd did not report an endpoint")?
92 .to_string();
93 info!(endpoint = %endpoint, "hyperd started");
94
95 let info = DaemonInfo {
97 pid: std::process::id(),
98 hyperd_endpoint: endpoint.clone(),
99 health_port: bound_port,
100 started_at: chrono::Utc::now().to_rfc3339(),
101 version: env!("CARGO_PKG_VERSION").to_string(),
102 };
103 discovery::write_discovery_file(&info)?;
104 info!(path = %discovery::discovery_file_path()?.display(), "discovery file written");
105
106 let state = Arc::new(DaemonState::new());
111 let info_arc = Arc::new(Mutex::new(info));
112 let hyper_state = Arc::new(Mutex::new(HyperState {
113 hyper: Some(hyper),
114 restart_history: Vec::new(),
115 }));
116
117 let health_state = Arc::clone(&state);
119 let health_info = Arc::clone(&info_arc);
120 let health_handle = std::thread::spawn(move || {
121 listener.run(health_state, health_info);
122 });
123
124 tokio::select! {
127 () = idle_monitor(Arc::clone(&state), config.idle_timeout) => {}
128 () = hyperd_monitor(Arc::clone(&state), Arc::clone(&hyper_state), Arc::clone(&info_arc)) => {}
129 () = shutdown_signal() => {
130 info!("received shutdown signal");
131 }
132 }
133 state.request_shutdown();
134
135 info!("shutting down daemon");
143 discovery::remove_discovery_file();
144 let _ = health_handle.join();
145 drop(hyper_state); Ok(())
148}
149
150#[derive(Debug, PartialEq, Eq)]
152pub enum RestartAttempt {
153 Recorded,
155 LimitExceeded,
157}
158
159pub fn try_record_restart_attempt(history: &mut Vec<Instant>, now: Instant) -> RestartAttempt {
166 history.retain(|t| now.duration_since(*t) < RESTART_WINDOW);
167 if history.len() >= RESTART_LIMIT {
168 return RestartAttempt::LimitExceeded;
169 }
170 history.push(now);
171 RestartAttempt::Recorded
172}
173
174fn build_params() -> std::io::Result<Parameters> {
176 let log_dir = discovery::state_dir()?.join("logs");
177 std::fs::create_dir_all(&log_dir)?;
178
179 let mut params = Parameters::new();
180 params.set("log_file_max_count", "2");
181 params.set("log_file_size_limit", "100M");
182 params.set("log_dir", log_dir.to_string_lossy().as_ref());
183 params.set_transport_mode(TransportMode::Tcp);
184 Ok(params)
185}
186
187async fn idle_monitor(state: Arc<DaemonState>, idle_timeout: Duration) {
190 loop {
191 tokio::time::sleep(Duration::from_secs(10)).await;
192 if state.should_shutdown() {
193 return;
194 }
195 if state.idle_duration() >= idle_timeout {
196 info!(
197 idle_secs = idle_timeout.as_secs(),
198 "idle timeout reached, shutting down"
199 );
200 return;
201 }
202 }
203}
204
205async fn hyperd_monitor(
209 state: Arc<DaemonState>,
210 hyper_state: Arc<Mutex<HyperState>>,
211 info_arc: Arc<Mutex<DaemonInfo>>,
212) {
213 loop {
214 tokio::time::sleep(HYPERD_POLL_INTERVAL).await;
215 if state.should_shutdown() {
216 return;
217 }
218
219 let needs_restart = {
223 let mut guard = hyper_state.lock().expect("HyperState mutex poisoned");
224 let process_dead = guard.hyper.as_mut().map_or(true, HyperProcess::has_exited);
225 if process_dead {
228 let _ = state.consume_restart_request();
231 true
232 } else {
233 state.consume_restart_request()
234 }
235 };
236
237 if !needs_restart {
238 continue;
239 }
240
241 match try_restart_hyperd(&hyper_state, &info_arc) {
242 Ok(new_endpoint) => {
243 info!(endpoint = %new_endpoint, "hyperd restarted");
244 let _ = state.consume_restart_request();
250 }
251 Err(RestartError::TooManyRestarts) => {
252 error!(
253 limit = RESTART_LIMIT,
254 window_secs = RESTART_WINDOW.as_secs(),
255 "hyperd restart limit exceeded — daemon shutting down"
256 );
257 return;
258 }
259 Err(RestartError::SpawnFailed(e)) => {
260 warn!(error = %e, "hyperd spawn failed during restart; will retry on next tick");
261 }
262 }
263 }
264}
265
266fn try_restart_hyperd(
272 hyper_state: &Mutex<HyperState>,
273 info_arc: &Mutex<DaemonInfo>,
274) -> Result<String, RestartError> {
275 let mut guard = hyper_state.lock().expect("HyperState mutex poisoned");
276
277 if try_record_restart_attempt(&mut guard.restart_history, Instant::now())
279 == RestartAttempt::LimitExceeded
280 {
281 return Err(RestartError::TooManyRestarts);
282 }
283
284 guard.hyper = None;
287
288 let params = build_params().map_err(|e| RestartError::SpawnFailed(e.to_string()))?;
290 let new_hyper = HyperProcess::new(None, Some(¶ms))
291 .map_err(|e| RestartError::SpawnFailed(e.to_string()))?;
292 let new_endpoint = new_hyper
293 .endpoint()
294 .ok_or_else(|| RestartError::SpawnFailed("hyperd did not report endpoint".into()))?
295 .to_string();
296
297 let snapshot = {
301 let mut info_guard = info_arc.lock().expect("DaemonInfo mutex poisoned");
302 info_guard.hyperd_endpoint.clone_from(&new_endpoint);
303 info_guard.clone()
304 };
305 discovery::write_discovery_file(&snapshot)
306 .map_err(|e| RestartError::SpawnFailed(format!("discovery write: {e}")))?;
307
308 guard.hyper = Some(new_hyper);
309 Ok(new_endpoint)
310}
311
312async fn shutdown_signal() {
313 let ctrl_c = signal::ctrl_c();
314
315 #[cfg(unix)]
316 {
317 let mut sigterm =
318 signal::unix::signal(signal::unix::SignalKind::terminate()).expect("sigterm handler");
319 tokio::select! {
320 _ = ctrl_c => {}
321 _ = sigterm.recv() => {}
322 }
323 }
324
325 #[cfg(not(unix))]
326 {
327 ctrl_c.await.ok();
328 }
329}