Skip to main content

forge_core/cluster/
roles.rs

1use std::str::FromStr;
2
3/// Node role in the cluster.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum NodeRole {
6    /// HTTP gateway for client requests.
7    Gateway,
8    /// Function executor.
9    Function,
10    /// Background job worker.
11    Worker,
12    /// Scheduler (leader-only) for crons and job assignment.
13    Scheduler,
14}
15
16impl NodeRole {
17    /// Convert to string for database storage.
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            Self::Gateway => "gateway",
21            Self::Function => "function",
22            Self::Worker => "worker",
23            Self::Scheduler => "scheduler",
24        }
25    }
26
27    /// Get all default roles.
28    pub fn all() -> Vec<Self> {
29        vec![Self::Gateway, Self::Function, Self::Worker, Self::Scheduler]
30    }
31}
32
33/// Error for parsing NodeRole from string.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ParseNodeRoleError(pub String);
36
37impl std::fmt::Display for ParseNodeRoleError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "invalid node role: {}", self.0)
40    }
41}
42
43impl std::error::Error for ParseNodeRoleError {}
44
45impl FromStr for NodeRole {
46    type Err = ParseNodeRoleError;
47
48    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
49        match s {
50            "gateway" => Ok(Self::Gateway),
51            "function" => Ok(Self::Function),
52            "worker" => Ok(Self::Worker),
53            "scheduler" => Ok(Self::Scheduler),
54            _ => Err(ParseNodeRoleError(s.to_string())),
55        }
56    }
57}
58
59impl std::fmt::Display for NodeRole {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.as_str())
62    }
63}
64
65/// Leader role for coordinated operations.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum LeaderRole {
68    /// Job assignment and cron triggering.
69    Scheduler,
70    /// Metrics aggregation.
71    MetricsAggregator,
72    /// Log compaction.
73    LogCompactor,
74}
75
76impl LeaderRole {
77    /// Get the PostgreSQL advisory lock ID for this role.
78    pub fn lock_id(&self) -> i64 {
79        // Use a unique ID based on "FORGE" + role number
80        // 0x464F524745 = "FORGE" in hex
81        match self {
82            Self::Scheduler => 0x464F_5247_0001,
83            Self::MetricsAggregator => 0x464F_5247_0002,
84            Self::LogCompactor => 0x464F_5247_0003,
85        }
86    }
87
88    /// Convert to string for database storage.
89    pub fn as_str(&self) -> &'static str {
90        match self {
91            Self::Scheduler => "scheduler",
92            Self::MetricsAggregator => "metrics_aggregator",
93            Self::LogCompactor => "log_compactor",
94        }
95    }
96}
97
98/// Error for parsing LeaderRole from string.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct ParseLeaderRoleError(pub String);
101
102impl std::fmt::Display for ParseLeaderRoleError {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "invalid leader role: {}", self.0)
105    }
106}
107
108impl std::error::Error for ParseLeaderRoleError {}
109
110impl FromStr for LeaderRole {
111    type Err = ParseLeaderRoleError;
112
113    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
114        match s {
115            "scheduler" => Ok(Self::Scheduler),
116            "metrics_aggregator" => Ok(Self::MetricsAggregator),
117            "log_compactor" => Ok(Self::LogCompactor),
118            _ => Err(ParseLeaderRoleError(s.to_string())),
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_node_role_conversion() {
129        assert_eq!("gateway".parse::<NodeRole>(), Ok(NodeRole::Gateway));
130        assert_eq!("worker".parse::<NodeRole>(), Ok(NodeRole::Worker));
131        assert!("invalid".parse::<NodeRole>().is_err());
132        assert_eq!(NodeRole::Gateway.as_str(), "gateway");
133    }
134
135    #[test]
136    fn test_all_roles() {
137        let roles = NodeRole::all();
138        assert_eq!(roles.len(), 4);
139        assert!(roles.contains(&NodeRole::Gateway));
140        assert!(roles.contains(&NodeRole::Scheduler));
141    }
142
143    #[test]
144    fn test_leader_role_lock_ids() {
145        // Each leader role should have a unique lock ID
146        let scheduler_id = LeaderRole::Scheduler.lock_id();
147        let metrics_id = LeaderRole::MetricsAggregator.lock_id();
148        let log_id = LeaderRole::LogCompactor.lock_id();
149
150        assert_ne!(scheduler_id, metrics_id);
151        assert_ne!(metrics_id, log_id);
152        assert_ne!(scheduler_id, log_id);
153    }
154
155    #[test]
156    fn test_leader_role_conversion() {
157        assert_eq!("scheduler".parse::<LeaderRole>(), Ok(LeaderRole::Scheduler));
158        assert!("invalid".parse::<LeaderRole>().is_err());
159        assert_eq!(LeaderRole::Scheduler.as_str(), "scheduler");
160    }
161}