1use std::sync::Arc;
8
9use async_trait::async_trait;
10use parking_lot::Mutex;
11use rskit_bootstrap::{Component, Health};
12use rskit_errors::{AppError, AppResult, ErrorCode};
13use rskit_resilience::{ConstantBackoff, Policy, RetryPolicy};
14use tracing::{debug, info, warn};
15
16use crate::config::DiscoveryConfig;
17use crate::factory::DiscoveryRegistry;
18use crate::traits::{Discovery, Registry};
19
20pub struct DiscoveryComponent {
26 config: DiscoveryConfig,
27 providers: DiscoveryRegistry,
28 registry: Mutex<Option<Arc<dyn Registry>>>,
29 discovery: Mutex<Option<Arc<dyn Discovery>>>,
30 instance_id: Mutex<Option<String>>,
31}
32
33impl DiscoveryComponent {
34 pub fn new(config: DiscoveryConfig) -> Self {
36 Self {
37 config,
38 providers: DiscoveryRegistry::builtins(),
39 registry: Mutex::new(None),
40 discovery: Mutex::new(None),
41 instance_id: Mutex::new(None),
42 }
43 }
44
45 #[must_use]
47 pub fn with_registry(config: DiscoveryConfig, providers: DiscoveryRegistry) -> Self {
48 Self {
49 config,
50 providers,
51 registry: Mutex::new(None),
52 discovery: Mutex::new(None),
53 instance_id: Mutex::new(None),
54 }
55 }
56
57 pub fn registry(&self) -> Option<Arc<dyn Registry>> {
59 self.registry.lock().clone()
60 }
61
62 pub fn discovery(&self) -> Option<Arc<dyn Discovery>> {
64 self.discovery.lock().clone()
65 }
66}
67
68#[async_trait]
69impl Component for DiscoveryComponent {
70 fn name(&self) -> &str {
71 "discovery"
72 }
73
74 async fn start(&self) -> AppResult<()> {
75 let mut config = self.config.clone();
76 config.apply_defaults();
77
78 if !config.enabled {
79 debug!("Discovery disabled — using static provider");
80 let cfg_static = DiscoveryConfig {
81 provider: "static".to_string(),
82 ..config
83 };
84 let (reg, disc) = self.providers.create(&cfg_static)?;
85 *self.registry.lock() = Some(reg);
86 *self.discovery.lock() = Some(disc);
87 return Ok(());
88 }
89
90 config.validate()?;
91
92 let (reg, disc) = self.providers.create(&config)?;
93 *self.registry.lock() = Some(reg.clone());
94 *self.discovery.lock() = Some(disc);
95
96 if config.registration.enabled {
98 let mut instance = config.build_instance();
99
100 if config.health.enabled {
102 let addr = if instance.address.is_empty() {
103 "localhost".to_string()
104 } else {
105 instance.address.clone()
106 };
107 let health_url = format!("http://{}:{}{}", addr, instance.port, config.health.path);
108 instance
109 .metadata
110 .insert("health_url".to_string(), health_url);
111 }
112
113 info!(
114 id = %instance.id,
115 name = %instance.name,
116 address = %instance.address,
117 port = instance.port,
118 "Registering with service discovery"
119 );
120
121 let instance_id = instance.id.clone();
122 let max_retries = config.registration.max_retries.max(1) as usize;
123 let retry_policy = RetryPolicy::new()
124 .with_max_attempts(max_retries)
125 .with_constant_backoff(ConstantBackoff::new(config.registration.retry_duration()?))
126 .with_jitter(false)
127 .with_on_retry({
128 let instance_id = instance_id.clone();
129 move |attempt, error| {
130 warn!(
131 error = %error,
132 service_id = %instance_id,
133 attempt,
134 max_retries,
135 "failed to register service"
136 );
137 }
138 });
139 let registration_policy = Policy::new().with_retry(retry_policy);
140
141 let registration = registration_policy
142 .execute(|| async { reg.register(&instance).await })
143 .await;
144
145 if let Err(err) = registration {
146 if config.registration.required {
147 return Err(AppError::new(
148 ErrorCode::Internal,
149 format!("discovery: register self after {max_retries} retries: {err}"),
150 ));
151 }
152 warn!(
153 service_id = %instance_id,
154 "failed to register with discovery — continuing in degraded mode"
155 );
156 } else {
157 *self.instance_id.lock() = Some(instance_id.clone());
158 }
159 }
160
161 debug!(provider = %config.provider, "Discovery component started");
162 Ok(())
163 }
164
165 async fn stop(&self) -> AppResult<()> {
166 debug!("Discovery component stopping");
167
168 let instance_id = self.instance_id.lock().take();
169 if let Some(id) = instance_id
170 && let Some(reg) = self.registry()
171 && let Err(e) = reg.deregister(&id).await
172 {
173 warn!(error = %e, id = %id, "Failed to deregister on stop");
174 }
175
176 Ok(())
177 }
178
179 fn health(&self) -> Health {
180 if self.discovery.lock().is_none() {
181 return Health::unhealthy("discovery", "not initialized");
182 }
183 if !self.config.enabled {
184 return Health::healthy("discovery (static)");
185 }
186 if self.instance_id.lock().is_some() {
187 Health::healthy("discovery")
188 } else {
189 Health::degraded("discovery", "no services registered")
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::instance::ServiceInstance;
198 use async_trait::async_trait;
199
200 struct FailingRegistry {
201 fail_register: bool,
202 deregistered: Arc<Mutex<Vec<String>>>,
203 }
204
205 #[async_trait]
206 impl Registry for FailingRegistry {
207 async fn register(&self, _instance: &ServiceInstance) -> AppResult<()> {
208 if self.fail_register {
209 Err(AppError::new(ErrorCode::Internal, "registration failed"))
210 } else {
211 Ok(())
212 }
213 }
214
215 async fn deregister(&self, id: &str) -> AppResult<()> {
216 self.deregistered.lock().push(id.to_string());
217 Ok(())
218 }
219 }
220
221 #[async_trait]
222 impl Discovery for FailingRegistry {
223 async fn resolve(&self, _service: &str) -> AppResult<Vec<ServiceInstance>> {
224 Ok(Vec::new())
225 }
226 }
227
228 #[tokio::test]
229 async fn disabled_component_uses_static() {
230 let config = DiscoveryConfig {
231 enabled: false,
232 ..Default::default()
233 };
234 let comp = DiscoveryComponent::new(config);
235 comp.start().await.unwrap();
236 assert!(comp.discovery().is_some());
237 assert!(comp.registry().is_some());
238 assert!(comp.health().is_healthy());
239 comp.stop().await.unwrap();
240 }
241
242 #[tokio::test]
243 async fn static_provider_with_registration() {
244 let config = DiscoveryConfig {
245 enabled: true,
246 provider: "static".to_string(),
247 registration: crate::config::RegistrationConfig {
248 enabled: true,
249 service_name: "test-svc".to_string(),
250 service_id: "test-svc-1".to_string(),
251 service_address: "127.0.0.1".to_string(),
252 service_port: 8080,
253 ..Default::default()
254 },
255 ..Default::default()
256 };
257 let comp = DiscoveryComponent::new(config);
258 comp.start().await.unwrap();
259 assert!(comp.instance_id.lock().is_some());
260 comp.stop().await.unwrap();
261 assert!(comp.instance_id.lock().is_none());
262 }
263
264 #[test]
265 fn with_registry_initial_health_is_unhealthy_and_accessors_are_empty() {
266 let comp =
267 DiscoveryComponent::with_registry(DiscoveryConfig::default(), DiscoveryRegistry::new());
268
269 assert_eq!(comp.name(), "discovery");
270 assert!(comp.registry().is_none());
271 assert!(comp.discovery().is_none());
272 assert!(!comp.health().is_healthy());
273 }
274
275 #[tokio::test]
276 async fn optional_registration_failure_starts_degraded_without_instance_id() {
277 let provider = Arc::new(FailingRegistry {
278 fail_register: true,
279 deregistered: Arc::new(Mutex::new(Vec::new())),
280 });
281 let mut providers = DiscoveryRegistry::new();
282 providers.register(
283 "fake",
284 Box::new({
285 let provider = provider.clone();
286 move |_| Ok((provider.clone(), provider.clone()))
287 }),
288 );
289 let comp = DiscoveryComponent::with_registry(
290 DiscoveryConfig {
291 enabled: true,
292 provider: "fake".to_string(),
293 registration: crate::config::RegistrationConfig {
294 enabled: true,
295 required: false,
296 service_name: "svc".to_string(),
297 service_port: 8080,
298 max_retries: 1,
299 retry_interval: "1ms".to_string(),
300 ..Default::default()
301 },
302 ..Default::default()
303 },
304 providers,
305 );
306
307 comp.start().await.unwrap();
308
309 assert!(comp.registry().is_some());
310 assert!(comp.discovery().is_some());
311 assert!(comp.instance_id.lock().is_none());
312 assert!(!comp.health().is_healthy());
313 }
314
315 #[tokio::test]
316 async fn required_registration_failure_returns_start_error() {
317 let provider = Arc::new(FailingRegistry {
318 fail_register: true,
319 deregistered: Arc::new(Mutex::new(Vec::new())),
320 });
321 let mut providers = DiscoveryRegistry::new();
322 providers.register(
323 "fake",
324 Box::new({
325 let provider = provider.clone();
326 move |_| Ok((provider.clone(), provider.clone()))
327 }),
328 );
329 let comp = DiscoveryComponent::with_registry(
330 DiscoveryConfig {
331 enabled: true,
332 provider: "fake".to_string(),
333 registration: crate::config::RegistrationConfig {
334 enabled: true,
335 required: true,
336 service_name: "svc".to_string(),
337 service_port: 8080,
338 max_retries: 1,
339 retry_interval: "1ms".to_string(),
340 ..Default::default()
341 },
342 ..Default::default()
343 },
344 providers,
345 );
346
347 let err = comp.start().await.unwrap_err();
348 assert_eq!(err.code(), ErrorCode::Internal);
349 assert!(err.to_string().contains("register self"));
350 }
351
352 #[tokio::test]
353 async fn successful_registration_adds_health_url_and_deregisters_on_stop() {
354 let deregistered = Arc::new(Mutex::new(Vec::new()));
355 let provider = Arc::new(FailingRegistry {
356 fail_register: false,
357 deregistered: deregistered.clone(),
358 });
359 let mut providers = DiscoveryRegistry::new();
360 providers.register(
361 "fake",
362 Box::new({
363 let provider = provider.clone();
364 move |_| Ok((provider.clone(), provider.clone()))
365 }),
366 );
367 let comp = DiscoveryComponent::with_registry(
368 DiscoveryConfig {
369 enabled: true,
370 provider: "fake".to_string(),
371 registration: crate::config::RegistrationConfig {
372 enabled: true,
373 service_name: "svc".to_string(),
374 service_id: "svc-1".to_string(),
375 service_port: 8080,
376 max_retries: 1,
377 retry_interval: "1ms".to_string(),
378 ..Default::default()
379 },
380 ..Default::default()
381 },
382 providers,
383 );
384
385 comp.start().await.unwrap();
386 assert!(comp.health().is_healthy());
387 comp.stop().await.unwrap();
388
389 assert_eq!(*deregistered.lock(), vec!["svc-1".to_string()]);
390 }
391}