aura_agent/runtime/services/
traits.rs1use async_trait::async_trait;
15use aura_core::effects::PhysicalTimeEffects;
16use std::fmt;
17use std::sync::Arc;
18
19use crate::runtime::TaskSupervisor;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ServiceHealth {
24 Healthy,
26 Degraded {
28 reason: String,
30 },
31 Unhealthy {
33 reason: String,
35 },
36 NotStarted,
38 Starting,
40 Stopping,
42 Stopped,
44}
45
46impl ServiceHealth {
47 pub fn is_healthy(&self) -> bool {
49 matches!(self, ServiceHealth::Healthy)
50 }
51
52 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#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ServiceErrorKind {
78 StartupFailed,
80 ShutdownFailed,
82 InvalidConfiguration,
84 DependencyUnavailable,
86 Unavailable,
88 Internal,
90 Timeout,
92}
93
94#[derive(Debug)]
96pub struct ServiceError {
97 pub service: &'static str,
99 pub kind: ServiceErrorKind,
101 pub message: String,
103 pub cause: Option<Box<dyn std::error::Error + Send + Sync>>,
105}
106
107impl ServiceError {
108 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 pub fn startup_failed(service: &'static str, message: impl Into<String>) -> Self {
120 Self::new(service, ServiceErrorKind::StartupFailed, message)
121 }
122
123 pub fn shutdown_failed(service: &'static str, message: impl Into<String>) -> Self {
125 Self::new(service, ServiceErrorKind::ShutdownFailed, message)
126 }
127
128 pub fn internal(service: &'static str, message: impl Into<String>) -> Self {
130 Self::new(service, ServiceErrorKind::Internal, message)
131 }
132
133 pub fn unavailable(service: &'static str, message: impl Into<String>) -> Self {
135 Self::new(service, ServiceErrorKind::Unavailable, message)
136 }
137
138 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#[derive(Clone)]
165pub struct RuntimeServiceContext {
166 tasks: Arc<TaskSupervisor>,
167 time_effects: Arc<dyn PhysicalTimeEffects + Send + Sync>,
168}
169
170impl RuntimeServiceContext {
171 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 pub fn tasks(&self) -> Arc<TaskSupervisor> {
184 self.tasks.clone()
185 }
186
187 pub fn time_effects(&self) -> Arc<dyn PhysicalTimeEffects + Send + Sync> {
189 self.time_effects.clone()
190 }
191}
192
193#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
231#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
232pub trait RuntimeService: Send + Sync {
233 fn name(&self) -> &'static str;
237
238 fn dependencies(&self) -> &[&'static str] {
243 &[]
244 }
245
246 async fn start(&self, context: &RuntimeServiceContext) -> Result<(), ServiceError>;
258
259 async fn stop(&self) -> Result<(), ServiceError>;
272
273 async fn health(&self) -> ServiceHealth;
278}
279
280#[allow(dead_code)] pub trait RuntimeServiceCollection {
283 fn get_service(&self, name: &str) -> Option<&dyn RuntimeService>;
285
286 fn services_in_start_order(&self) -> Vec<&dyn RuntimeService>;
288
289 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}