rskit_workload/
component.rs1use std::sync::Arc;
8
9use async_trait::async_trait;
10use parking_lot::Mutex;
11use rskit_component::{Component, Health};
12use rskit_errors::AppResult;
13use tracing::{debug, info};
14
15use crate::config::WorkloadConfig;
16use crate::manager::Manager;
17use crate::registry::WorkloadRegistry;
18
19pub struct WorkloadComponent {
28 config: WorkloadConfig,
29 providers: WorkloadRegistry,
30 manager: Mutex<Option<Arc<dyn Manager>>>,
31}
32
33impl WorkloadComponent {
34 #[must_use]
39 pub fn new(config: WorkloadConfig) -> Self {
40 Self::with_registry(config, WorkloadRegistry::new())
41 }
42
43 #[must_use]
45 pub fn with_registry(config: WorkloadConfig, providers: WorkloadRegistry) -> Self {
46 Self {
47 config,
48 providers,
49 manager: Mutex::new(None),
50 }
51 }
52
53 #[must_use]
55 pub fn manager(&self) -> Option<Arc<dyn Manager>> {
56 self.manager.lock().clone()
57 }
58}
59
60#[async_trait]
61impl Component for WorkloadComponent {
62 #[allow(clippy::unnecessary_literal_bound)] fn name(&self) -> &str {
64 "workload"
65 }
66
67 async fn start(&self) -> AppResult<()> {
68 let mut config = self.config.clone();
69 config.apply_defaults();
70
71 if !config.enabled {
72 debug!("workload component disabled — skipping backend init");
73 return Ok(());
74 }
75
76 let manager = self.providers.build(&config).await?;
77 manager.health_check().await?;
78 info!(provider = %config.provider, "workload manager initialized");
79 *self.manager.lock() = Some(manager);
80 Ok(())
81 }
82
83 async fn stop(&self) -> AppResult<()> {
84 debug!("workload component stopping");
85 *self.manager.lock() = None;
86 Ok(())
87 }
88
89 fn health(&self) -> Health {
90 if !self.config.enabled {
91 return Health::healthy("workload (disabled)");
92 }
93 if self.manager.lock().is_none() {
94 return Health::unhealthy("workload", "manager not initialized");
95 }
96 Health::healthy("workload")
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::test_support::{FailingFactory, FakeFactory, UnhealthyFactory};
104 use rskit_errors::ErrorCode;
105
106 fn enabled_registry(
107 factory: Arc<dyn crate::registry::ManagerFactory>,
108 ) -> (WorkloadConfig, WorkloadRegistry) {
109 let mut registry = WorkloadRegistry::new();
110 registry.register("docker", factory).unwrap();
111 let config = WorkloadConfig {
112 enabled: true,
113 provider: "docker".to_string(),
114 ..Default::default()
115 };
116 (config, registry)
117 }
118
119 #[tokio::test]
120 async fn disabled_component_starts_healthy_without_manager() {
121 let component = WorkloadComponent::new(WorkloadConfig::default());
122 assert_eq!(component.name(), "workload");
123 component.start().await.unwrap();
124 assert!(component.manager().is_none());
125 assert!(component.health().is_healthy());
126 component.stop().await.unwrap();
127 }
128
129 #[tokio::test]
130 async fn enabled_component_builds_manager_and_releases_on_stop() {
131 let (config, registry) = enabled_registry(Arc::new(FakeFactory));
132 let component = WorkloadComponent::with_registry(config, registry);
133
134 component.start().await.unwrap();
135 assert!(component.manager().is_some());
136 assert!(component.health().is_healthy());
137
138 component.stop().await.unwrap();
139 assert!(component.manager().is_none());
140 assert!(!component.health().is_healthy());
141 }
142
143 #[tokio::test]
144 async fn enabled_component_before_start_is_unhealthy() {
145 let (config, registry) = enabled_registry(Arc::new(FakeFactory));
146 let component = WorkloadComponent::with_registry(config, registry);
147 assert!(!component.health().is_healthy());
148 }
149
150 #[tokio::test]
151 async fn start_propagates_backend_build_failure() {
152 let (config, registry) = enabled_registry(Arc::new(FailingFactory));
153 let component = WorkloadComponent::with_registry(config, registry);
154 let err = component.start().await.unwrap_err();
155 assert_eq!(err.code(), ErrorCode::Internal);
156 assert!(component.manager().is_none());
157 }
158
159 #[tokio::test]
160 async fn start_fails_when_backend_health_check_fails() {
161 let (config, registry) = enabled_registry(Arc::new(UnhealthyFactory));
162 let component = WorkloadComponent::with_registry(config, registry);
163 let err = component.start().await.unwrap_err();
164 assert_eq!(err.code(), ErrorCode::ServiceUnavailable);
165 assert!(component.manager().is_none());
166 }
167
168 #[tokio::test]
169 async fn enabled_component_with_unregistered_provider_fails_to_start() {
170 let config = WorkloadConfig {
171 enabled: true,
172 provider: "podman".to_string(),
173 ..Default::default()
174 };
175 let component = WorkloadComponent::with_registry(config, WorkloadRegistry::new());
176 assert_eq!(
177 component.start().await.unwrap_err().code(),
178 ErrorCode::NotFound
179 );
180 }
181}