Skip to main content

forge_core/daemon/
traits.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::str::FromStr;
4use std::time::Duration;
5
6use crate::error::Result;
7
8use super::context::DaemonContext;
9
10/// Trait for FORGE daemon handlers.
11///
12/// Daemons are long-running singleton tasks that run continuously in the background.
13/// They support leader election (only one instance in cluster), automatic restart
14/// on panic, and graceful shutdown.
15pub trait ForgeDaemon: Send + Sync + 'static {
16    /// Get daemon metadata.
17    fn info() -> DaemonInfo;
18
19    /// Execute the daemon.
20    ///
21    /// The daemon should run in a loop and check `ctx.shutdown_signal()` to handle
22    /// graceful shutdown. Example:
23    ///
24    /// ```ignore
25    /// loop {
26    ///     // Do work
27    ///     tokio::select! {
28    ///         _ = tokio::time::sleep(Duration::from_secs(60)) => {}
29    ///         _ = ctx.shutdown_signal() => break,
30    ///     }
31    /// }
32    /// ```
33    fn execute(ctx: &DaemonContext) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
34}
35
36/// Daemon metadata.
37#[derive(Debug, Clone)]
38pub struct DaemonInfo {
39    /// Daemon name (used for identification and leader election).
40    pub name: &'static str,
41    /// Whether only one instance should run across the cluster.
42    pub leader_elected: bool,
43    /// Whether to restart the daemon if it panics.
44    pub restart_on_panic: bool,
45    /// Delay before restarting after a failure.
46    pub restart_delay: Duration,
47    /// Delay before first execution after startup.
48    pub startup_delay: Duration,
49    /// Maximum number of restarts (None = unlimited).
50    pub max_restarts: Option<u32>,
51}
52
53impl Default for DaemonInfo {
54    fn default() -> Self {
55        Self {
56            name: "",
57            leader_elected: true,
58            restart_on_panic: true,
59            restart_delay: Duration::from_secs(5),
60            startup_delay: Duration::from_secs(0),
61            max_restarts: None,
62        }
63    }
64}
65
66/// Daemon status in the cluster.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum DaemonStatus {
69    /// Waiting to start (startup delay).
70    Pending,
71    /// Acquiring leader lock.
72    Acquiring,
73    /// Currently running.
74    Running,
75    /// Stopped gracefully.
76    Stopped,
77    /// Stopped due to failure.
78    Failed,
79    /// Waiting to restart after failure.
80    Restarting,
81}
82
83impl DaemonStatus {
84    /// Convert to database string.
85    pub fn as_str(&self) -> &'static str {
86        match self {
87            Self::Pending => "pending",
88            Self::Acquiring => "acquiring",
89            Self::Running => "running",
90            Self::Stopped => "stopped",
91            Self::Failed => "failed",
92            Self::Restarting => "restarting",
93        }
94    }
95}
96
97impl FromStr for DaemonStatus {
98    type Err = std::convert::Infallible;
99
100    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
101        Ok(match s {
102            "pending" => Self::Pending,
103            "acquiring" => Self::Acquiring,
104            "running" => Self::Running,
105            "stopped" => Self::Stopped,
106            "failed" => Self::Failed,
107            "restarting" => Self::Restarting,
108            _ => Self::Pending,
109        })
110    }
111}
112
113#[cfg(test)]
114#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_default_daemon_info() {
120        let info = DaemonInfo::default();
121        assert!(info.leader_elected);
122        assert!(info.restart_on_panic);
123        assert_eq!(info.restart_delay, Duration::from_secs(5));
124        assert_eq!(info.startup_delay, Duration::from_secs(0));
125        assert!(info.max_restarts.is_none());
126    }
127
128    #[test]
129    fn test_status_conversion() {
130        assert_eq!(DaemonStatus::Running.as_str(), "running");
131        assert_eq!("running".parse::<DaemonStatus>(), Ok(DaemonStatus::Running));
132        assert_eq!(DaemonStatus::Failed.as_str(), "failed");
133        assert_eq!("failed".parse::<DaemonStatus>(), Ok(DaemonStatus::Failed));
134    }
135}