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    /// Default timeout for outbound HTTP requests made by the daemon.
50    pub http_timeout: Option<Duration>,
51    /// Maximum number of restarts (None = unlimited).
52    pub max_restarts: Option<u32>,
53}
54
55impl Default for DaemonInfo {
56    fn default() -> Self {
57        Self {
58            name: "",
59            leader_elected: true,
60            restart_on_panic: true,
61            restart_delay: Duration::from_secs(5),
62            startup_delay: Duration::from_secs(0),
63            http_timeout: None,
64            max_restarts: None,
65        }
66    }
67}
68
69/// Daemon status in the cluster.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum DaemonStatus {
72    /// Waiting to start (startup delay).
73    Pending,
74    /// Acquiring leader lock.
75    Acquiring,
76    /// Currently running.
77    Running,
78    /// Stopped gracefully.
79    Stopped,
80    /// Stopped due to failure.
81    Failed,
82    /// Waiting to restart after failure.
83    Restarting,
84}
85
86impl DaemonStatus {
87    /// Convert to database string.
88    pub fn as_str(&self) -> &'static str {
89        match self {
90            Self::Pending => "pending",
91            Self::Acquiring => "acquiring",
92            Self::Running => "running",
93            Self::Stopped => "stopped",
94            Self::Failed => "failed",
95            Self::Restarting => "restarting",
96        }
97    }
98}
99
100impl FromStr for DaemonStatus {
101    type Err = std::convert::Infallible;
102
103    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
104        Ok(match s {
105            "pending" => Self::Pending,
106            "acquiring" => Self::Acquiring,
107            "running" => Self::Running,
108            "stopped" => Self::Stopped,
109            "failed" => Self::Failed,
110            "restarting" => Self::Restarting,
111            _ => Self::Pending,
112        })
113    }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_default_daemon_info() {
123        let info = DaemonInfo::default();
124        assert!(info.leader_elected);
125        assert!(info.restart_on_panic);
126        assert_eq!(info.restart_delay, Duration::from_secs(5));
127        assert_eq!(info.startup_delay, Duration::from_secs(0));
128        assert_eq!(info.http_timeout, None);
129        assert!(info.max_restarts.is_none());
130    }
131
132    #[test]
133    fn test_status_conversion() {
134        assert_eq!(DaemonStatus::Running.as_str(), "running");
135        assert_eq!("running".parse::<DaemonStatus>(), Ok(DaemonStatus::Running));
136        assert_eq!(DaemonStatus::Failed.as_str(), "failed");
137        assert_eq!("failed".parse::<DaemonStatus>(), Ok(DaemonStatus::Failed));
138    }
139}