Skip to main content

rskit_discovery/
server.rs

1//! Discovery-integrated server component.
2//!
3//! Wraps any server implementing the [`rskit_bootstrap::Component`] trait to automatically register with service discovery on start
4//! and deregister on stop.
5
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use rskit_bootstrap::{Component, Health};
10use rskit_errors::{AppError, AppResult, ErrorCode};
11
12use crate::{instance::ServiceInstance, traits::Registry};
13
14/// Wraps a server component with service discovery auto-registration.
15///
16/// On `start()`, the inner server starts first, then the instance is registered. On `stop()`,
17/// the instance is deregistered first, then the inner server stops.
18pub struct DiscoveryServer<S: Component + ?Sized> {
19    inner: Arc<S>,
20    registry: Arc<dyn Registry>,
21    instance: ServiceInstance,
22    name: String,
23}
24
25impl<S: Component + ?Sized> DiscoveryServer<S> {
26    /// Create a new discovery-integrated server.
27    ///
28    /// # Arguments
29    ///
30    /// * `name` - Component identifier (e.g., "discovery-grpc-server")
31    /// * `inner` - The server component to wrap
32    /// * `registry` - The service registry for registration/deregistration
33    /// * `instance` - The service instance configuration
34    pub fn new(
35        name: String,
36        inner: Arc<S>,
37        registry: Arc<dyn Registry>,
38        instance: ServiceInstance,
39    ) -> Self {
40        Self {
41            inner,
42            registry,
43            instance,
44            name,
45        }
46    }
47
48    /// Returns a reference to the wrapped server component.
49    pub fn inner(&self) -> &S {
50        &self.inner
51    }
52
53    /// Returns the service instance being registered.
54    pub fn instance(&self) -> &ServiceInstance {
55        &self.instance
56    }
57}
58
59#[async_trait]
60impl<S: Component + ?Sized + 'static> Component for DiscoveryServer<S> {
61    fn name(&self) -> &str {
62        &self.name
63    }
64
65    async fn start(&self) -> AppResult<()> {
66        // Start the inner server first
67        tracing::debug!(
68            component = %self.name,
69            "Starting inner server component"
70        );
71        self.inner.start().await.map_err(|e| {
72            AppError::new(
73                ErrorCode::Internal,
74                format!("failed to start inner server: {}", e),
75            )
76        })?;
77
78        // Then register with discovery
79        tracing::debug!(
80            component = %self.name,
81            service_id = %self.instance.id,
82            service_name = %self.instance.name,
83            address = %self.instance.address,
84            port = %self.instance.port,
85            "Registering with service discovery"
86        );
87
88        if let Err(err) = self.registry.register(&self.instance).await {
89            // Log and attempt to stop the server if registration fails
90            tracing::error!(
91                component = %self.name,
92                error = %err,
93                "Registration failed, stopping inner server"
94            );
95            let inner_clone = self.inner.clone();
96            tokio::spawn(async move {
97                if let Err(e) = inner_clone.stop().await {
98                    tracing::warn!(
99                        "Failed to stop inner server after registration failure: {}",
100                        e
101                    );
102                }
103            });
104            return Err(AppError::new(
105                ErrorCode::Internal,
106                format!("failed to register with discovery: {}", err),
107            ));
108        }
109
110        tracing::debug!(
111            component = %self.name,
112            service_id = %self.instance.id,
113            "Service registered successfully"
114        );
115        Ok(())
116    }
117
118    async fn stop(&self) -> AppResult<()> {
119        tracing::debug!(
120            component = %self.name,
121            service_id = %self.instance.id,
122            "Stopping discovery-server component"
123        );
124
125        // Deregister from discovery first
126        if let Err(e) = self.registry.deregister(&self.instance.id).await {
127            tracing::warn!(
128                component = %self.name,
129                service_id = %self.instance.id,
130                error = %e,
131                "Failed to deregister from discovery"
132            );
133            // Continue to stop the server even if deregistration fails
134        }
135
136        // Then stop the inner server
137        self.inner.stop().await.map_err(|e| {
138            AppError::new(
139                ErrorCode::Internal,
140                format!("failed to stop inner server: {}", e),
141            )
142        })?;
143
144        tracing::debug!(
145            component = %self.name,
146            "Discovery-server component stopped"
147        );
148        Ok(())
149    }
150
151    fn health(&self) -> Health {
152        // Report the inner component's health, tagged with the registration status.
153        let inner_health = self.inner.health();
154        if inner_health.is_healthy() {
155            Health::healthy(format!("{} (registered)", self.name))
156        } else {
157            Health::unhealthy(
158                format!("{} (inner unhealthy)", self.name),
159                "inner component is unhealthy",
160            )
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::sync::Arc;
169
170    use rskit_errors::AppResult;
171
172    /// Mock server for testing
173    struct MockServer {
174        started: parking_lot::Mutex<bool>,
175        stopped: parking_lot::Mutex<bool>,
176    }
177
178    impl MockServer {
179        fn new() -> Self {
180            Self {
181                started: parking_lot::Mutex::new(false),
182                stopped: parking_lot::Mutex::new(false),
183            }
184        }
185    }
186
187    #[async_trait]
188    impl Component for MockServer {
189        fn name(&self) -> &str {
190            "mock-server"
191        }
192
193        async fn start(&self) -> AppResult<()> {
194            *self.started.lock() = true;
195            Ok(())
196        }
197
198        async fn stop(&self) -> AppResult<()> {
199            *self.stopped.lock() = true;
200            Ok(())
201        }
202
203        fn health(&self) -> Health {
204            Health::healthy("mock-server")
205        }
206    }
207
208    struct ErrorServer {
209        fail_start: bool,
210        fail_stop: bool,
211        healthy: bool,
212    }
213
214    #[async_trait]
215    impl Component for ErrorServer {
216        fn name(&self) -> &str {
217            "error-server"
218        }
219
220        async fn start(&self) -> AppResult<()> {
221            if self.fail_start {
222                Err(AppError::new(ErrorCode::Internal, "start failed"))
223            } else {
224                Ok(())
225            }
226        }
227
228        async fn stop(&self) -> AppResult<()> {
229            if self.fail_stop {
230                Err(AppError::new(ErrorCode::Internal, "stop failed"))
231            } else {
232                Ok(())
233            }
234        }
235
236        fn health(&self) -> Health {
237            if self.healthy {
238                Health::healthy("error-server")
239            } else {
240                Health::unhealthy("error-server", "down")
241            }
242        }
243    }
244
245    /// Mock registry for testing
246    struct MockRegistry {
247        registered: parking_lot::Mutex<Vec<ServiceInstance>>,
248        deregistered: parking_lot::Mutex<Vec<String>>,
249        register_error: parking_lot::Mutex<Option<String>>,
250        deregister_error: parking_lot::Mutex<Option<String>>,
251    }
252
253    impl MockRegistry {
254        fn new() -> Arc<Self> {
255            Arc::new(Self {
256                registered: parking_lot::Mutex::new(Vec::new()),
257                deregistered: parking_lot::Mutex::new(Vec::new()),
258                register_error: parking_lot::Mutex::new(None),
259                deregister_error: parking_lot::Mutex::new(None),
260            })
261        }
262
263        fn set_register_error(&self, error: Option<String>) {
264            *self.register_error.lock() = error;
265        }
266
267        fn set_deregister_error(&self, error: Option<String>) {
268            *self.deregister_error.lock() = error;
269        }
270
271        fn registered_instances(&self) -> Vec<ServiceInstance> {
272            self.registered.lock().clone()
273        }
274
275        fn deregistered_ids(&self) -> Vec<String> {
276            self.deregistered.lock().clone()
277        }
278    }
279
280    #[async_trait]
281    impl Registry for MockRegistry {
282        async fn register(&self, instance: &ServiceInstance) -> AppResult<()> {
283            if let Some(err) = &*self.register_error.lock() {
284                return Err(AppError::new(ErrorCode::Internal, err.clone()));
285            }
286            self.registered.lock().push(instance.clone());
287            Ok(())
288        }
289
290        async fn deregister(&self, id: &str) -> AppResult<()> {
291            if let Some(err) = &*self.deregister_error.lock() {
292                return Err(AppError::new(ErrorCode::Internal, err.clone()));
293            }
294            self.deregistered.lock().push(id.to_string());
295            Ok(())
296        }
297    }
298
299    #[tokio::test]
300    async fn test_lifecycle_success() {
301        let server = Arc::new(MockServer::new());
302        let registry = MockRegistry::new();
303        let instance = ServiceInstance {
304            id: "test-1".to_string(),
305            name: "test-service".to_string(),
306            address: "127.0.0.1".to_string(),
307            port: 8080,
308            healthy: true,
309            weight: 1,
310            tags: vec!["test".to_string()],
311            metadata: Default::default(),
312        };
313
314        let discovery_server = DiscoveryServer::new(
315            "discovery-test".to_string(),
316            server.clone(),
317            registry.clone(),
318            instance.clone(),
319        );
320
321        // Start should start inner server and register
322        discovery_server.start().await.unwrap();
323        assert!(*server.started.lock());
324        assert_eq!(registry.registered_instances().len(), 1);
325        assert_eq!(registry.registered_instances()[0].id, "test-1");
326
327        // Stop should deregister and stop inner server
328        discovery_server.stop().await.unwrap();
329        assert!(*server.stopped.lock());
330        assert_eq!(registry.deregistered_ids().len(), 1);
331        assert_eq!(registry.deregistered_ids()[0], "test-1");
332    }
333
334    #[tokio::test]
335    async fn test_registration_failure_stops_server() {
336        let server = Arc::new(MockServer::new());
337        let registry = MockRegistry::new();
338        registry.set_register_error(Some("service unavailable".to_string()));
339
340        let instance = ServiceInstance {
341            id: "test-2".to_string(),
342            name: "test-service".to_string(),
343            address: "127.0.0.1".to_string(),
344            port: 8081,
345            healthy: true,
346            weight: 1,
347            tags: vec![],
348            metadata: Default::default(),
349        };
350
351        let discovery_server = DiscoveryServer::new(
352            "discovery-test".to_string(),
353            server.clone(),
354            registry.clone(),
355            instance,
356        );
357
358        // Start should fail
359        let result = discovery_server.start().await;
360        assert!(result.is_err());
361
362        // Server should have started (even though registration failed)
363        assert!(*server.started.lock());
364
365        // After a short delay, we should see the stop called
366        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
367        assert!(*server.stopped.lock());
368    }
369
370    #[tokio::test]
371    async fn test_deregistration_failure_doesnt_prevent_stop() {
372        let server = Arc::new(MockServer::new());
373        let registry = MockRegistry::new();
374        let instance = ServiceInstance {
375            id: "test-3".to_string(),
376            name: "test-service".to_string(),
377            address: "127.0.0.1".to_string(),
378            port: 8082,
379            healthy: true,
380            weight: 1,
381            tags: vec![],
382            metadata: Default::default(),
383        };
384
385        let discovery_server = DiscoveryServer::new(
386            "discovery-test".to_string(),
387            server.clone(),
388            registry.clone(),
389            instance,
390        );
391
392        // Start successfully
393        discovery_server.start().await.unwrap();
394        assert_eq!(registry.registered_instances().len(), 1);
395
396        // Set deregistration error
397        registry.set_deregister_error(Some("registry error".to_string()));
398
399        // Stop should still succeed
400        let result = discovery_server.stop().await;
401        assert!(result.is_ok());
402
403        // Server should be stopped
404        assert!(*server.stopped.lock());
405    }
406
407    #[test]
408    fn test_component_name_and_accessors() {
409        let server = Arc::new(MockServer::new());
410        let registry = MockRegistry::new();
411        let instance = ServiceInstance {
412            id: "test-4".to_string(),
413            name: "my-service".to_string(),
414            address: "192.168.1.1".to_string(),
415            port: 9000,
416            healthy: true,
417            weight: 1,
418            tags: vec!["prod".to_string()],
419            metadata: Default::default(),
420        };
421
422        let discovery_server = DiscoveryServer::new(
423            "my-discovery-server".to_string(),
424            server,
425            registry,
426            instance.clone(),
427        );
428
429        assert_eq!(discovery_server.name(), "my-discovery-server");
430        assert_eq!(discovery_server.inner().name(), "mock-server");
431        assert_eq!(discovery_server.instance().id, "test-4");
432        assert_eq!(discovery_server.instance().name, "my-service");
433        assert_eq!(discovery_server.instance().port, 9000);
434    }
435
436    #[tokio::test]
437    async fn inner_start_failure_prevents_registration() {
438        let server = Arc::new(ErrorServer {
439            fail_start: true,
440            fail_stop: false,
441            healthy: true,
442        });
443        let registry = MockRegistry::new();
444        let discovery_server = DiscoveryServer::new(
445            "discovery-test".to_string(),
446            server,
447            registry.clone(),
448            test_instance("test-5"),
449        );
450
451        let err = discovery_server.start().await.unwrap_err();
452
453        assert!(err.to_string().contains("failed to start inner server"));
454        assert!(registry.registered_instances().is_empty());
455    }
456
457    #[tokio::test]
458    async fn inner_stop_failure_is_returned_after_deregistering() {
459        let server = Arc::new(ErrorServer {
460            fail_start: false,
461            fail_stop: true,
462            healthy: true,
463        });
464        let registry = MockRegistry::new();
465        let discovery_server = DiscoveryServer::new(
466            "discovery-test".to_string(),
467            server,
468            registry.clone(),
469            test_instance("test-6"),
470        );
471        discovery_server.start().await.unwrap();
472
473        let err = discovery_server.stop().await.unwrap_err();
474
475        assert!(err.to_string().contains("failed to stop inner server"));
476        assert_eq!(registry.deregistered_ids(), vec!["test-6".to_string()]);
477    }
478
479    #[test]
480    fn health_reflects_inner_component_state() {
481        let registry = MockRegistry::new();
482        let healthy = DiscoveryServer::new(
483            "discovery-test".to_string(),
484            Arc::new(ErrorServer {
485                fail_start: false,
486                fail_stop: false,
487                healthy: true,
488            }),
489            registry.clone(),
490            test_instance("test-7"),
491        );
492        let unhealthy = DiscoveryServer::new(
493            "discovery-test".to_string(),
494            Arc::new(ErrorServer {
495                fail_start: false,
496                fail_stop: false,
497                healthy: false,
498            }),
499            registry,
500            test_instance("test-8"),
501        );
502
503        assert!(healthy.health().is_healthy());
504        assert!(!unhealthy.health().is_healthy());
505    }
506
507    fn test_instance(id: &str) -> ServiceInstance {
508        ServiceInstance {
509            id: id.to_string(),
510            name: "test-service".to_string(),
511            address: "127.0.0.1".to_string(),
512            port: 8080,
513            healthy: true,
514            weight: 1,
515            tags: Vec::new(),
516            metadata: Default::default(),
517        }
518    }
519}