Skip to main content

aura_agent/runtime/services/
traits.rs

1//! Runtime Service Traits
2//!
3//! Unified lifecycle management for runtime services. All service managers
4//! implement `RuntimeService` for consistent startup, shutdown, and health
5//! monitoring.
6//!
7//! ## Design Principles
8//!
9//! 1. **Uniform Lifecycle**: All services follow the same start/stop pattern
10//! 2. **Dependency Ordering**: Services declare dependencies for ordered startup
11//! 3. **Health Monitoring**: Consistent health check interface
12//! 4. **Graceful Shutdown**: Services can clean up resources properly
13
14use async_trait::async_trait;
15use aura_core::effects::PhysicalTimeEffects;
16use std::fmt;
17use std::sync::Arc;
18
19use crate::runtime::TaskSupervisor;
20
21/// Health status of a runtime service
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ServiceHealth {
24    /// Service is operating normally
25    Healthy,
26    /// Service is operational but experiencing issues
27    Degraded {
28        /// Reason for degraded state
29        reason: String,
30    },
31    /// Service is not operational
32    Unhealthy {
33        /// Reason for unhealthy state
34        reason: String,
35    },
36    /// Service has not been started
37    NotStarted,
38    /// Service is starting up
39    Starting,
40    /// Service is shutting down
41    Stopping,
42    /// Service has been stopped
43    Stopped,
44}
45
46impl ServiceHealth {
47    /// Returns true if the service is healthy
48    pub fn is_healthy(&self) -> bool {
49        matches!(self, ServiceHealth::Healthy)
50    }
51
52    /// Returns true if the service is operational (healthy or degraded)
53    pub fn is_operational(&self) -> bool {
54        matches!(
55            self,
56            ServiceHealth::Healthy | ServiceHealth::Degraded { .. }
57        )
58    }
59}
60
61impl fmt::Display for ServiceHealth {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            ServiceHealth::Healthy => write!(f, "healthy"),
65            ServiceHealth::Degraded { reason } => write!(f, "degraded: {}", reason),
66            ServiceHealth::Unhealthy { reason } => write!(f, "unhealthy: {}", reason),
67            ServiceHealth::NotStarted => write!(f, "not started"),
68            ServiceHealth::Starting => write!(f, "starting"),
69            ServiceHealth::Stopping => write!(f, "stopping"),
70            ServiceHealth::Stopped => write!(f, "stopped"),
71        }
72    }
73}
74
75/// Error kinds for service operations
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ServiceErrorKind {
78    /// Service failed to start
79    StartupFailed,
80    /// Service failed to stop gracefully
81    ShutdownFailed,
82    /// Service configuration is invalid
83    InvalidConfiguration,
84    /// A required dependency is not available
85    DependencyUnavailable,
86    /// Service is unavailable or disabled
87    Unavailable,
88    /// Service encountered an internal error
89    Internal,
90    /// Service operation timed out
91    Timeout,
92}
93
94/// Error from a service operation
95#[derive(Debug)]
96pub struct ServiceError {
97    /// Name of the service that encountered the error
98    pub service: &'static str,
99    /// Kind of error
100    pub kind: ServiceErrorKind,
101    /// Human-readable error message
102    pub message: String,
103    /// Optional underlying cause
104    pub cause: Option<Box<dyn std::error::Error + Send + Sync>>,
105}
106
107impl ServiceError {
108    /// Create a new service error
109    pub fn new(service: &'static str, kind: ServiceErrorKind, message: impl Into<String>) -> Self {
110        Self {
111            service,
112            kind,
113            message: message.into(),
114            cause: None,
115        }
116    }
117
118    /// Create a startup failure error
119    pub fn startup_failed(service: &'static str, message: impl Into<String>) -> Self {
120        Self::new(service, ServiceErrorKind::StartupFailed, message)
121    }
122
123    /// Create a shutdown failure error
124    pub fn shutdown_failed(service: &'static str, message: impl Into<String>) -> Self {
125        Self::new(service, ServiceErrorKind::ShutdownFailed, message)
126    }
127
128    /// Create an internal error
129    pub fn internal(service: &'static str, message: impl Into<String>) -> Self {
130        Self::new(service, ServiceErrorKind::Internal, message)
131    }
132
133    /// Create an unavailable service error
134    pub fn unavailable(service: &'static str, message: impl Into<String>) -> Self {
135        Self::new(service, ServiceErrorKind::Unavailable, message)
136    }
137
138    /// Add a cause to this error
139    pub fn with_cause(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
140        self.cause = Some(Box::new(cause));
141        self
142    }
143}
144
145impl fmt::Display for ServiceError {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "[{}] {:?}: {}", self.service, self.kind, self.message)?;
148        if let Some(cause) = &self.cause {
149            write!(f, " (caused by: {})", cause)?;
150        }
151        Ok(())
152    }
153}
154
155impl std::error::Error for ServiceError {
156    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157        self.cause
158            .as_ref()
159            .map(|c| c.as_ref() as &(dyn std::error::Error + 'static))
160    }
161}
162
163/// Shared runtime context provided to services during lifecycle operations.
164#[derive(Clone)]
165pub struct RuntimeServiceContext {
166    tasks: Arc<TaskSupervisor>,
167    time_effects: Arc<dyn PhysicalTimeEffects + Send + Sync>,
168}
169
170impl RuntimeServiceContext {
171    /// Create one runtime service context from shared runtime dependencies.
172    pub fn new(
173        tasks: Arc<TaskSupervisor>,
174        time_effects: Arc<dyn PhysicalTimeEffects + Send + Sync>,
175    ) -> Self {
176        Self {
177            tasks,
178            time_effects,
179        }
180    }
181
182    /// Borrow the shared supervised task root for service-owned child groups.
183    pub fn tasks(&self) -> Arc<TaskSupervisor> {
184        self.tasks.clone()
185    }
186
187    /// Borrow physical time effects for service startup and maintenance work.
188    pub fn time_effects(&self) -> Arc<dyn PhysicalTimeEffects + Send + Sync> {
189        self.time_effects.clone()
190    }
191}
192
193/// Trait for runtime services with unified lifecycle management
194///
195/// This is the only supported lifecycle API for runtime-managed services.
196///
197/// ## Example
198///
199/// ```ignore
200/// use aura_agent::runtime::services::{RuntimeService, ServiceHealth, ServiceError};
201///
202/// struct MyService { /* ... */ }
203///
204/// #[async_trait]
205/// impl RuntimeService for MyService {
206///     fn name(&self) -> &'static str {
207///         "my_service"
208///     }
209///
210///     fn dependencies(&self) -> &[&'static str] {
211///         &["indexed_journal", "transport"]
212///     }
213///
214///     async fn start(&self, ctx: &RuntimeServiceContext) -> Result<(), ServiceError> {
215///         // Initialize and start background tasks
216///         let _tasks = ctx.tasks();
217///         Ok(())
218///     }
219///
220///     async fn stop(&self) -> Result<(), ServiceError> {
221///         // Graceful shutdown
222///         Ok(())
223///     }
224///
225///     async fn health(&self) -> ServiceHealth {
226///         ServiceHealth::Healthy
227///     }
228/// }
229/// ```
230#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
231#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
232pub trait RuntimeService: Send + Sync {
233    /// Returns the unique name of this service
234    ///
235    /// Used for logging, metrics, and dependency resolution.
236    fn name(&self) -> &'static str;
237
238    /// Returns the names of services this service depends on
239    ///
240    /// Dependencies will be started before this service and stopped after.
241    /// Return an empty slice if there are no dependencies.
242    fn dependencies(&self) -> &[&'static str] {
243        &[]
244    }
245
246    /// Start the service
247    ///
248    /// Called during runtime startup. The service should initialize any
249    /// required state and spawn background tasks using the provided
250    /// runtime service context.
251    ///
252    /// # Arguments
253    /// * `context` - Runtime service context for spawning tasks and accessing time effects
254    ///
255    /// # Errors
256    /// Returns `ServiceError` if startup fails
257    async fn start(&self, context: &RuntimeServiceContext) -> Result<(), ServiceError>;
258
259    /// Stop the service gracefully
260    ///
261    /// Called during runtime shutdown. The service should:
262    /// 1. Stop accepting new work
263    /// 2. Complete or cancel in-progress operations
264    /// 3. Release resources
265    ///
266    /// Background tasks spawned via `TaskSupervisor` are automatically
267    /// cancelled, but the service may need to perform additional cleanup.
268    ///
269    /// # Errors
270    /// Returns `ServiceError` if shutdown fails
271    async fn stop(&self) -> Result<(), ServiceError>;
272
273    /// Returns the current health status of the service
274    ///
275    /// Called after startup and during health monitoring. Should reflect the
276    /// current lifecycle/actor state rather than a placeholder approximation.
277    async fn health(&self) -> ServiceHealth;
278}
279
280/// Extension trait for collections of runtime services
281#[allow(dead_code)] // Reserved for future dependency-ordered service collections.
282pub trait RuntimeServiceCollection {
283    /// Get a service by name
284    fn get_service(&self, name: &str) -> Option<&dyn RuntimeService>;
285
286    /// Get all services sorted by dependency order (dependencies first)
287    fn services_in_start_order(&self) -> Vec<&dyn RuntimeService>;
288
289    /// Get all services sorted by reverse dependency order (dependents first)
290    fn services_in_stop_order(&self) -> Vec<&dyn RuntimeService>;
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_service_health_display() {
299        assert_eq!(format!("{}", ServiceHealth::Healthy), "healthy");
300        assert_eq!(
301            format!(
302                "{}",
303                ServiceHealth::Degraded {
304                    reason: "high load".to_string()
305                }
306            ),
307            "degraded: high load"
308        );
309    }
310
311    #[test]
312    fn test_service_health_checks() {
313        assert!(ServiceHealth::Healthy.is_healthy());
314        assert!(ServiceHealth::Healthy.is_operational());
315
316        let degraded = ServiceHealth::Degraded {
317            reason: "test".to_string(),
318        };
319        assert!(!degraded.is_healthy());
320        assert!(degraded.is_operational());
321
322        let unhealthy = ServiceHealth::Unhealthy {
323            reason: "test".to_string(),
324        };
325        assert!(!unhealthy.is_healthy());
326        assert!(!unhealthy.is_operational());
327    }
328
329    #[test]
330    fn test_service_error_display() {
331        let err = ServiceError::startup_failed("test_service", "failed to connect");
332        assert!(err.to_string().contains("test_service"));
333        assert!(err.to_string().contains("StartupFailed"));
334        assert!(err.to_string().contains("failed to connect"));
335    }
336
337    #[test]
338    fn runtime_service_context_exposes_shared_dependencies() {
339        let tasks = Arc::new(TaskSupervisor::new());
340        let time_effects: Arc<dyn PhysicalTimeEffects + Send + Sync> =
341            Arc::new(aura_effects::time::PhysicalTimeHandler::new());
342        let context = RuntimeServiceContext::new(tasks.clone(), time_effects.clone());
343
344        assert!(Arc::ptr_eq(&context.tasks(), &tasks));
345        assert!(Arc::ptr_eq(&context.time_effects(), &time_effects));
346    }
347}