Skip to main content

actionqueue_core/
actor.rs

1//! Actor domain types for remote actor registration and heartbeat coordination.
2//!
3//! Remote actors are Caelum Vessels (or other clients) that register with the
4//! Org ActionQueue hub, claim tasks by capability, and report execution results.
5//! This module defines the pure domain types; storage, routing, and heartbeat
6//! monitoring logic lives in `actionqueue-actor`.
7
8use crate::ids::{ActorId, DepartmentId, TenantId};
9
10/// Declared capabilities of a remote actor.
11///
12/// Capabilities are free-form strings (e.g. `"compute"`, `"review"`,
13/// `"approve"`). The dispatch loop uses capability intersection to decide
14/// which actors are eligible to claim a given task.
15///
16/// # Invariants
17///
18/// - The capability list must be non-empty.
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct ActorCapabilities {
22    capabilities: Vec<String>,
23}
24
25impl ActorCapabilities {
26    /// Creates a validated `ActorCapabilities` from a list of capability strings.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error string if the list is empty or any entry is empty.
31    pub fn new(capabilities: Vec<String>) -> Result<Self, String> {
32        if capabilities.is_empty() {
33            return Err("actor must declare at least one capability".to_string());
34        }
35        for cap in &capabilities {
36            if cap.is_empty() {
37                return Err("capability string must be non-empty".to_string());
38            }
39        }
40        Ok(ActorCapabilities { capabilities })
41    }
42
43    /// Returns the capability strings.
44    pub fn as_slice(&self) -> &[String] {
45        &self.capabilities
46    }
47
48    /// Returns `true` if this set contains all capabilities in `required`.
49    pub fn satisfies(&self, required: &[String]) -> bool {
50        required.iter().all(|r| self.capabilities.iter().any(|c| c == r))
51    }
52}
53
54/// Actor registration record.
55///
56/// Represents a remote actor's registration with the Org ActionQueue hub.
57/// All fields are private with validated constructors.
58#[derive(Debug, Clone, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60pub struct ActorRegistration {
61    actor_id: ActorId,
62    /// Human-readable identity string used as the WAL lease owner.
63    identity: String,
64    capabilities: ActorCapabilities,
65    department: Option<DepartmentId>,
66    heartbeat_interval_secs: u64,
67    tenant_id: Option<TenantId>,
68}
69
70impl ActorRegistration {
71    /// Creates a new actor registration with required fields.
72    ///
73    /// Optional fields (`department`, `tenant_id`) can be set via builder methods.
74    ///
75    /// # Panics
76    ///
77    /// Panics if `identity` is empty or `heartbeat_interval_secs` is 0.
78    pub fn new(
79        actor_id: ActorId,
80        identity: impl Into<String>,
81        capabilities: ActorCapabilities,
82        heartbeat_interval_secs: u64,
83    ) -> Self {
84        let identity = identity.into();
85        assert!(!identity.is_empty(), "actor identity must be non-empty");
86        assert!(heartbeat_interval_secs > 0, "heartbeat_interval_secs must be > 0");
87        ActorRegistration {
88            actor_id,
89            identity,
90            capabilities,
91            department: None,
92            heartbeat_interval_secs,
93            tenant_id: None,
94        }
95    }
96
97    /// Attaches a department identifier, returning the modified registration.
98    pub fn with_department(mut self, department: DepartmentId) -> Self {
99        self.department = Some(department);
100        self
101    }
102
103    /// Attaches a tenant identifier, returning the modified registration.
104    pub fn with_tenant(mut self, tenant_id: TenantId) -> Self {
105        self.tenant_id = Some(tenant_id);
106        self
107    }
108
109    /// Returns the actor identifier.
110    pub fn actor_id(&self) -> ActorId {
111        self.actor_id
112    }
113
114    /// Returns the actor identity string (used as WAL lease owner).
115    pub fn identity(&self) -> &str {
116        &self.identity
117    }
118
119    /// Returns the actor's declared capabilities.
120    pub fn capabilities(&self) -> &ActorCapabilities {
121        &self.capabilities
122    }
123
124    /// Returns the actor's department, if any.
125    pub fn department(&self) -> Option<&DepartmentId> {
126        self.department.as_ref()
127    }
128
129    /// Returns the expected heartbeat interval in seconds.
130    pub fn heartbeat_interval_secs(&self) -> u64 {
131        self.heartbeat_interval_secs
132    }
133
134    /// Returns the actor's tenant, if any.
135    pub fn tenant_id(&self) -> Option<TenantId> {
136        self.tenant_id
137    }
138}
139
140/// Heartbeat timeout policy for a remote actor.
141///
142/// Timeout = `interval_secs × timeout_multiplier`. The hub declares an actor
143/// dead when it has not received a heartbeat for this duration.
144#[derive(Debug, Clone, PartialEq, Eq)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
146pub struct HeartbeatPolicy {
147    interval_secs: u64,
148    /// Timeout multiplier (timeout = interval × multiplier). Default: 3.
149    timeout_multiplier: u32,
150}
151
152impl HeartbeatPolicy {
153    /// Default timeout multiplier (3×interval).
154    pub const DEFAULT_MULTIPLIER: u32 = 3;
155
156    /// Creates a new heartbeat policy.
157    ///
158    /// # Panics
159    ///
160    /// Panics if `interval_secs == 0` or `timeout_multiplier == 0`.
161    pub fn new(interval_secs: u64, timeout_multiplier: u32) -> Self {
162        assert!(interval_secs > 0, "heartbeat interval_secs must be > 0");
163        assert!(timeout_multiplier > 0, "timeout_multiplier must be > 0");
164        HeartbeatPolicy { interval_secs, timeout_multiplier }
165    }
166
167    /// Creates a heartbeat policy with the default 3× multiplier.
168    pub fn with_default_multiplier(interval_secs: u64) -> Self {
169        Self::new(interval_secs, Self::DEFAULT_MULTIPLIER)
170    }
171
172    /// Returns the heartbeat interval in seconds.
173    pub fn interval_secs(&self) -> u64 {
174        self.interval_secs
175    }
176
177    /// Returns the timeout multiplier.
178    pub fn timeout_multiplier(&self) -> u32 {
179        self.timeout_multiplier
180    }
181
182    /// Returns the effective timeout duration: `interval_secs × timeout_multiplier`.
183    pub fn timeout_secs(&self) -> u64 {
184        self.interval_secs.saturating_mul(self.timeout_multiplier as u64)
185    }
186}
187
188impl Default for HeartbeatPolicy {
189    fn default() -> Self {
190        Self::new(30, Self::DEFAULT_MULTIPLIER)
191    }
192}