Skip to main content

rskit_bootstrap/
app.rs

1use std::marker::PhantomData;
2use std::sync::Arc;
3use std::time::Duration;
4
5use rskit_component::{Component, Registry};
6use rskit_config::AppConfig;
7use rskit_di::Container;
8use rskit_errors::{AppError, AppResult, ErrorCode};
9use rskit_hook::{EventBus, EventBusConfig};
10use rskit_provider::Provider;
11use tokio_util::sync::CancellationToken;
12
13use crate::hooks::LifecycleEvent;
14use crate::lifecycle::{
15    AsyncHook, LifecycleHooks, LifecyclePhase, make_hook, record_stop_error, run_hooks,
16    wait_for_signal,
17};
18
19/// Typestate marker retained only as the unbuildable initial marker.
20///
21/// Applications are constructed through [`AppBuilder`], which validates config
22/// and returns [`App<Built, C>`].
23pub struct Unconfigured;
24
25/// Typestate marker for a built application whose components are not running.
26pub struct Built;
27
28/// Typestate marker for an application with started components.
29pub struct Started;
30
31/// Typestate marker for an application that has completed shutdown.
32pub struct Stopped;
33
34/// Builder for [`App`].
35///
36/// The builder is the composition root for configuration validation, component registration,
37/// dependency injection, provider registration, and lifecycle hook ordering.
38pub struct AppBuilder<C: AppConfig> {
39    config: C,
40    graceful_timeout: Duration,
41    components: Vec<Arc<dyn Component>>,
42    container: Arc<Container>,
43    lifecycle_events: EventBus<LifecycleEvent>,
44    hooks: LifecycleHooks,
45}
46
47impl<C: AppConfig> AppBuilder<C> {
48    /// Create a new builder with the given application configuration.
49    #[must_use]
50    pub fn new(config: C) -> Self {
51        Self {
52            config,
53            graceful_timeout: Duration::from_secs(30),
54            components: Vec::new(),
55            container: Arc::new(Container::new()),
56            lifecycle_events: EventBus::new(EventBusConfig::default()),
57            hooks: LifecycleHooks::default(),
58        }
59    }
60
61    /// Set the graceful shutdown timeout.
62    #[must_use]
63    pub fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
64        self.graceful_timeout = timeout;
65        self
66    }
67
68    /// Register a component to be started and stopped by the app lifecycle.
69    #[must_use]
70    pub fn with_component(mut self, component: Arc<dyn Component>) -> Self {
71        self.components.push(component);
72        self
73    }
74
75    /// Use an existing typed DI container as the application container.
76    #[must_use]
77    pub fn with_container(mut self, container: Arc<Container>) -> Self {
78        self.container = container;
79        self
80    }
81
82    /// Use an existing bounded lifecycle event bus.
83    #[must_use]
84    pub fn with_lifecycle_event_bus(mut self, lifecycle_events: EventBus<LifecycleEvent>) -> Self {
85        self.lifecycle_events = lifecycle_events;
86        self
87    }
88
89    /// Register a typed dependency in the application DI container.
90    #[must_use]
91    pub fn with_dependency<T>(self, dependency: Arc<T>) -> Self
92    where
93        T: Send + Sync + 'static,
94    {
95        self.container.register(dependency);
96        self
97    }
98
99    /// Register a provider implementation in the application DI container.
100    #[must_use]
101    pub fn with_provider<P>(self, provider: Arc<P>) -> Self
102    where
103        P: Provider + Send + Sync + 'static,
104    {
105        self.container.register(provider);
106        self
107    }
108
109    /// Register a hook called before components start.
110    #[must_use]
111    pub fn before_start<F, Fut>(mut self, hook: F) -> Self
112    where
113        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
114        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
115    {
116        self.hooks.before_start.push(make_hook(hook));
117        self
118    }
119
120    /// Register a hook called after components start and readiness checks pass.
121    #[must_use]
122    pub fn after_start<F, Fut>(mut self, hook: F) -> Self
123    where
124        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
125        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
126    {
127        self.hooks.after_start.push(make_hook(hook));
128        self
129    }
130
131    /// Register a hook called before components stop.
132    #[must_use]
133    pub fn before_stop<F, Fut>(mut self, hook: F) -> Self
134    where
135        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
136        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
137    {
138        self.hooks.before_stop.push(make_hook(hook));
139        self
140    }
141
142    /// Register a hook called after components stop.
143    #[must_use]
144    pub fn after_stop<F, Fut>(mut self, hook: F) -> Self
145    where
146        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
147        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
148    {
149        self.hooks.after_stop.push(make_hook(hook));
150        self
151    }
152
153    /// Validate config and build the application in the [`Built`] state.
154    pub fn build(self) -> AppResult<App<Built, C>> {
155        self.config
156            .validate()
157            .map_err(|error| AppError::invalid_input("config", error.to_string()))?;
158
159        let mut registry = Registry::new();
160        for component in self.components {
161            registry.register(component);
162        }
163
164        Ok(App {
165            _state: PhantomData,
166            config: Arc::new(self.config),
167            container: self.container,
168            registry,
169            lifecycle_events: self.lifecycle_events,
170            hooks: self.hooks,
171            graceful_timeout: self.graceful_timeout,
172            shutdown_token: CancellationToken::new(),
173        })
174    }
175}
176
177/// Application orchestrator with typestate lifecycle.
178///
179/// Valid transitions are compile-time scoped:
180///
181/// ```text
182/// AppBuilder::build() -> App<Built, C>
183/// App<Built, C>::start() -> App<Started, C>
184/// App<Started, C>::stop() -> App<Stopped, C>
185/// ```
186pub struct App<S, C> {
187    _state: PhantomData<S>,
188    config: Arc<C>,
189    container: Arc<Container>,
190    registry: Registry,
191    lifecycle_events: EventBus<LifecycleEvent>,
192    hooks: LifecycleHooks,
193    graceful_timeout: Duration,
194    shutdown_token: CancellationToken,
195}
196
197impl<C: AppConfig> App<Built, C> {
198    /// Create an [`AppBuilder`].
199    pub fn builder(config: C) -> AppBuilder<C> {
200        AppBuilder::new(config)
201    }
202}
203
204impl<C> App<Built, C> {
205    /// Return a clone of the shared application configuration.
206    #[must_use]
207    pub fn config(&self) -> Arc<C> {
208        Arc::clone(&self.config)
209    }
210
211    /// Return the typed application DI container.
212    #[must_use]
213    pub fn container(&self) -> Arc<Container> {
214        Arc::clone(&self.container)
215    }
216
217    /// Return the bounded lifecycle event bus used by this app.
218    #[must_use]
219    pub fn lifecycle_event_bus(&self) -> EventBus<LifecycleEvent> {
220        self.lifecycle_events.clone()
221    }
222
223    /// Clone the shutdown token to share with long-running tasks.
224    #[must_use]
225    pub fn shutdown_token(&self) -> CancellationToken {
226        self.shutdown_token.clone()
227    }
228
229    /// Register a hook called before components start.
230    #[must_use]
231    pub fn before_start<F, Fut>(mut self, hook: F) -> Self
232    where
233        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
234        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
235    {
236        self.hooks.before_start.push(make_hook(hook));
237        self
238    }
239
240    /// Register a hook called after components start and readiness checks pass.
241    #[must_use]
242    pub fn after_start<F, Fut>(mut self, hook: F) -> Self
243    where
244        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
245        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
246    {
247        self.hooks.after_start.push(make_hook(hook));
248        self
249    }
250
251    /// Register a hook called before components stop.
252    #[must_use]
253    pub fn before_stop<F, Fut>(mut self, hook: F) -> Self
254    where
255        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
256        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
257    {
258        self.hooks.before_stop.push(make_hook(hook));
259        self
260    }
261
262    /// Register a hook called after components stop.
263    #[must_use]
264    pub fn after_stop<F, Fut>(mut self, hook: F) -> Self
265    where
266        F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
267        Fut: std::future::Future<Output = AppResult<()>> + Send + 'static,
268    {
269        self.hooks.after_stop.push(make_hook(hook));
270        self
271    }
272}
273
274impl<C: AppConfig> App<Built, C> {
275    /// Start components and transition to [`Started`].
276    pub async fn start(self) -> AppResult<App<Started, C>> {
277        crate::summary::print_startup(self.config.service_config(), self.registry.len());
278        self.run_hooks(LifecyclePhase::BeforeStart, &self.hooks.before_start)
279            .await?;
280
281        if let Err(error) = self.registry.start_all().await {
282            return Err(self
283                .rollback_startup_failure(error.context("component startup failed"), false)
284                .await);
285        }
286
287        if let Err(error) = self.ready_check() {
288            return Err(self
289                .rollback_startup_failure(error.context("component readiness failed"), true)
290                .await);
291        }
292
293        if let Err(error) = self
294            .run_hooks(LifecyclePhase::AfterStart, &self.hooks.after_start)
295            .await
296        {
297            return Err(self
298                .rollback_startup_failure(error.context("after_start hooks failed"), true)
299                .await);
300        }
301
302        Ok(App {
303            _state: PhantomData,
304            config: self.config,
305            container: self.container,
306            registry: self.registry,
307            lifecycle_events: self.lifecycle_events,
308            hooks: self.hooks,
309            graceful_timeout: self.graceful_timeout,
310            shutdown_token: self.shutdown_token,
311        })
312    }
313
314    /// Run as a long-running service until a signal or shutdown token fires.
315    pub async fn run(self) -> AppResult<()> {
316        let started = self.start().await?;
317        wait_for_signal(started.shutdown_token.clone()).await;
318        started.stop().await.map(|_| ())
319    }
320
321    /// Run a finite task after startup, then stop the app.
322    pub async fn run_task<F, Fut>(self, task: F) -> AppResult<()>
323    where
324        F: FnOnce(Arc<C>, CancellationToken) -> Fut,
325        Fut: std::future::Future<Output = AppResult<()>>,
326    {
327        let started = self.start().await?;
328        let task_result = tokio::select! {
329            result = task(Arc::clone(&started.config), started.shutdown_token.clone()) => result,
330            _ = wait_for_signal_owned(started.shutdown_token.clone()) => {
331                Ok(())
332            }
333        };
334        let stop_result = started.stop().await.map(|_| ());
335        match (task_result, stop_result) {
336            (Ok(()), Ok(())) => Ok(()),
337            (Ok(()), Err(stop_error)) => Err(stop_error),
338            (Err(task_error), Ok(())) => Err(task_error),
339            (Err(task_error), Err(stop_error)) => Err(task_error
340                .context("application shutdown also failed after task error")
341                .with_detail("shutdown_error", stop_error.to_string())),
342        }
343    }
344
345    fn ready_check(&self) -> AppResult<()> {
346        let unhealthy = self
347            .registry
348            .health_all()
349            .into_iter()
350            .filter(|health| !health.is_healthy())
351            .map(|health| match health.message {
352                Some(message) => format!("{}={}({message})", health.name, health.status),
353                None => format!("{}={}", health.name, health.status),
354            })
355            .collect::<Vec<_>>();
356
357        if unhealthy.is_empty() {
358            Ok(())
359        } else {
360            Err(AppError::service_unavailable(format!(
361                "unhealthy components: {}",
362                unhealthy.join(", ")
363            )))
364        }
365    }
366
367    async fn rollback_startup_failure(
368        &self,
369        mut error: AppError,
370        stop_components: bool,
371    ) -> AppError {
372        if let Err(stop_hook_error) = self
373            .run_hooks(LifecyclePhase::BeforeStop, &self.hooks.before_stop)
374            .await
375        {
376            tracing::warn!(error = %stop_hook_error, "before_stop hook failed during startup rollback");
377            error = error.context(format!(
378                "startup rollback before_stop hooks failed: {stop_hook_error}"
379            ));
380        }
381
382        if stop_components && let Err(stop_error) = self.registry.stop_all().await {
383            tracing::warn!(error = %stop_error, "component rollback error");
384            error = error.context(format!(
385                "startup rollback component stop failed: {stop_error}"
386            ));
387        }
388        error
389    }
390}
391
392impl<C> App<Built, C> {
393    async fn run_hooks(&self, phase: LifecyclePhase, hooks: &[AsyncHook]) -> AppResult<()> {
394        run_hooks(
395            phase,
396            hooks,
397            self.shutdown_token.clone(),
398            &self.lifecycle_events,
399        )
400        .await
401    }
402}
403
404impl<C> App<Started, C> {
405    /// Return a clone of the shared application configuration.
406    #[must_use]
407    pub fn config(&self) -> Arc<C> {
408        Arc::clone(&self.config)
409    }
410
411    /// Return the typed application DI container.
412    #[must_use]
413    pub fn container(&self) -> Arc<Container> {
414        Arc::clone(&self.container)
415    }
416
417    /// Return the bounded lifecycle event bus used by this app.
418    #[must_use]
419    pub fn lifecycle_event_bus(&self) -> EventBus<LifecycleEvent> {
420        self.lifecycle_events.clone()
421    }
422
423    /// Clone the shutdown token to share with long-running tasks.
424    #[must_use]
425    pub fn shutdown_token(&self) -> CancellationToken {
426        self.shutdown_token.clone()
427    }
428
429    /// Stop components and transition to [`Stopped`].
430    pub async fn stop(self) -> AppResult<App<Stopped, C>> {
431        let mut stop_error: Option<AppError> = None;
432        let stop_result = tokio::time::timeout(
433            self.graceful_timeout,
434            run_hooks(
435                LifecyclePhase::BeforeStop,
436                &self.hooks.before_stop,
437                self.shutdown_token.clone(),
438                &self.lifecycle_events,
439            ),
440        )
441        .await;
442
443        match stop_result {
444            Ok(Ok(())) => {}
445            Ok(Err(error)) => {
446                tracing::warn!(error = %error, "before_stop hook error");
447                stop_error = Some(error);
448            }
449            Err(_) => {
450                tracing::warn!("graceful shutdown timeout elapsed during before_stop hooks");
451                stop_error = Some(AppError::new(
452                    ErrorCode::Timeout,
453                    "graceful shutdown timeout elapsed during before_stop hooks",
454                ));
455            }
456        }
457
458        if let Err(error) = self.registry.stop_all().await {
459            tracing::warn!(error = %error, "component shutdown error");
460            record_stop_error(&mut stop_error, error, "component shutdown failed");
461        }
462
463        if let Err(error) = run_hooks(
464            LifecyclePhase::AfterStop,
465            &self.hooks.after_stop,
466            self.shutdown_token.clone(),
467            &self.lifecycle_events,
468        )
469        .await
470        {
471            record_stop_error(&mut stop_error, error, "after_stop hooks failed");
472        }
473
474        if let Err(error) = self.container.close().await {
475            record_stop_error(&mut stop_error, error, "container close failed");
476        }
477        tracing::info!("shutdown complete");
478
479        if let Some(error) = stop_error {
480            return Err(error);
481        }
482
483        Ok(App {
484            _state: PhantomData,
485            config: self.config,
486            container: self.container,
487            registry: self.registry,
488            lifecycle_events: self.lifecycle_events,
489            hooks: self.hooks,
490            graceful_timeout: self.graceful_timeout,
491            shutdown_token: self.shutdown_token,
492        })
493    }
494}
495
496impl<C> App<Stopped, C> {
497    /// Return a clone of the shared application configuration.
498    #[must_use]
499    pub fn config(&self) -> Arc<C> {
500        Arc::clone(&self.config)
501    }
502
503    /// Return the bounded lifecycle event bus used by this app.
504    #[must_use]
505    pub fn lifecycle_event_bus(&self) -> EventBus<LifecycleEvent> {
506        self.lifecycle_events.clone()
507    }
508}
509
510async fn wait_for_signal_owned(token: CancellationToken) {
511    wait_for_signal(token).await;
512}
513
514#[cfg(test)]
515mod tests {
516    use std::sync::Arc;
517    use std::sync::atomic::{AtomicUsize, Ordering};
518
519    use parking_lot::Mutex;
520    use rskit_config::{AppConfig as _, ServiceConfig};
521    use rskit_provider::Provider;
522    use tokio_util::sync::CancellationToken;
523
524    use super::AppBuilder;
525
526    #[derive(Debug, Default, serde::Deserialize)]
527    struct TestCfg {
528        #[serde(default)]
529        service: rskit_config::ServiceConfig,
530    }
531
532    impl rskit_validation::Validate for TestCfg {
533        fn validate(&self) -> Result<(), validator::ValidationErrors> {
534            rskit_validation::Validate::validate(&self.service)
535        }
536    }
537
538    impl rskit_config::AppConfig for TestCfg {
539        fn apply_defaults(&mut self) {}
540
541        fn service_config(&self) -> &ServiceConfig {
542            &self.service
543        }
544    }
545
546    struct TestProvider;
547
548    impl Provider for TestProvider {
549        fn name(&self) -> &'static str {
550            "test-provider"
551        }
552    }
553
554    #[tokio::test]
555    async fn app_builder_builds_successfully() {
556        let result = AppBuilder::new(TestCfg::default()).build();
557        assert!(result.is_ok());
558    }
559
560    #[tokio::test]
561    async fn lifecycle_hooks_run_in_order() {
562        let order = Arc::new(Mutex::new(Vec::new()));
563        let before_start = Arc::clone(&order);
564        let after_start = Arc::clone(&order);
565        let before_stop = Arc::clone(&order);
566        let after_stop = Arc::clone(&order);
567
568        let app = AppBuilder::new(TestCfg::default())
569            .before_start(move |_token| {
570                let order = Arc::clone(&before_start);
571                async move {
572                    order.lock().push("before_start");
573                    Ok(())
574                }
575            })
576            .after_start(move |_token| {
577                let order = Arc::clone(&after_start);
578                async move {
579                    order.lock().push("after_start");
580                    Ok(())
581                }
582            })
583            .before_stop(move |_token| {
584                let order = Arc::clone(&before_stop);
585                async move {
586                    order.lock().push("before_stop");
587                    Ok(())
588                }
589            })
590            .after_stop(move |_token| {
591                let order = Arc::clone(&after_stop);
592                async move {
593                    order.lock().push("after_stop");
594                    Ok(())
595                }
596            })
597            .build()
598            .expect("build should succeed");
599
600        app.run_task(|_cfg: Arc<TestCfg>, _cancel: CancellationToken| async move { Ok(()) })
601            .await
602            .expect("run_task should complete");
603
604        assert_eq!(
605            *order.lock(),
606            vec!["before_start", "after_start", "before_stop", "after_stop"]
607        );
608    }
609
610    #[tokio::test]
611    async fn lifecycle_events_are_published() {
612        let app = AppBuilder::new(TestCfg::default())
613            .build()
614            .expect("build should succeed");
615        let mut subscriber = app.lifecycle_event_bus().subscribe();
616
617        app.run_task(|_cfg: Arc<TestCfg>, _cancel: CancellationToken| async move { Ok(()) })
618            .await
619            .expect("run_task should complete");
620
621        let before_start = subscriber.recv().await.expect("before_start event");
622        let after_start = subscriber.recv().await.expect("after_start event");
623        let before_stop = subscriber.recv().await.expect("before_stop event");
624        let after_stop = subscriber.recv().await.expect("after_stop event");
625        assert_eq!(before_start.kind(), crate::LifecycleEventType::BeforeStart);
626        assert_eq!(after_start.kind(), crate::LifecycleEventType::AfterStart);
627        assert_eq!(before_stop.kind(), crate::LifecycleEventType::BeforeStop);
628        assert_eq!(after_stop.kind(), crate::LifecycleEventType::AfterStop);
629    }
630
631    #[tokio::test]
632    async fn builder_registers_provider_dependency_and_started_stopped_accessors() {
633        let dependency = Arc::new(7_usize);
634        let provider = Arc::new(TestProvider);
635        let app = AppBuilder::new(TestCfg::default())
636            .with_dependency(Arc::clone(&dependency))
637            .with_provider(Arc::clone(&provider))
638            .after_stop(|_| async { Ok(()) })
639            .build()
640            .expect("build should succeed");
641
642        assert_eq!(*app.container().resolve::<usize>().unwrap(), 7);
643        assert_eq!(
644            app.container().resolve::<TestProvider>().unwrap().name(),
645            "test-provider"
646        );
647        let _built_bus = app.lifecycle_event_bus();
648
649        let started = app.start().await.expect("start should succeed");
650        assert_eq!(started.config().service_config().name, "service");
651        assert_eq!(*started.container().resolve::<usize>().unwrap(), 7);
652        let _started_bus = started.lifecycle_event_bus();
653        let _shutdown = started.shutdown_token();
654
655        let stopped = started.stop().await.expect("stop should succeed");
656        assert_eq!(stopped.config().service_config().name, "service");
657        let _stopped_bus = stopped.lifecycle_event_bus();
658    }
659
660    #[tokio::test]
661    async fn run_task_exits_when_shutdown_token_is_cancelled() {
662        let app = AppBuilder::new(TestCfg::default())
663            .build()
664            .expect("build should succeed");
665
666        app.run_task(|_cfg: Arc<TestCfg>, cancel: CancellationToken| async move {
667            cancel.cancel();
668            std::future::pending::<rskit_errors::AppResult<()>>().await
669        })
670        .await
671        .expect("shutdown cancellation should stop run_task");
672    }
673
674    #[tokio::test]
675    async fn app_run_task_executes_and_exits() {
676        let app = AppBuilder::new(TestCfg::default())
677            .build()
678            .expect("build should succeed");
679        let runs = Arc::new(AtomicUsize::new(0));
680        let run_counter = Arc::clone(&runs);
681
682        app.run_task(move |_cfg: Arc<TestCfg>, _cancel: CancellationToken| {
683            let runs = Arc::clone(&run_counter);
684            async move {
685                runs.fetch_add(1, Ordering::SeqCst);
686                Ok::<(), rskit_errors::AppError>(())
687            }
688        })
689        .await
690        .expect("task should run");
691
692        assert_eq!(runs.load(Ordering::SeqCst), 1);
693    }
694
695    #[tokio::test]
696    async fn hook_failure_stops_startup() {
697        let app = AppBuilder::new(TestCfg::default())
698            .before_start(|_| async {
699                Err::<(), _>(rskit_errors::AppError::new(
700                    rskit_errors::ErrorCode::Internal,
701                    "hard fail",
702                ))
703            })
704            .build()
705            .expect("build should succeed");
706
707        let error = app
708            .run_task(|_cfg: Arc<TestCfg>, _cancel: CancellationToken| async move { Ok(()) })
709            .await
710            .expect_err("hook failure should fail startup");
711        assert!(error.to_string().contains("before_start hook failed"));
712    }
713
714    #[tokio::test]
715    async fn stop_hooks_run_after_shutdown_token_is_cancelled() {
716        let ran = Arc::new(AtomicUsize::new(0));
717        let before_stop_ran = Arc::clone(&ran);
718        let after_stop_ran = Arc::clone(&ran);
719
720        let app = AppBuilder::new(TestCfg::default())
721            .before_stop(move |_token| {
722                let ran = Arc::clone(&before_stop_ran);
723                async move {
724                    ran.fetch_add(1, Ordering::SeqCst);
725                    Ok(())
726                }
727            })
728            .after_stop(move |_token| {
729                let ran = Arc::clone(&after_stop_ran);
730                async move {
731                    ran.fetch_add(1, Ordering::SeqCst);
732                    Ok(())
733                }
734            })
735            .build()
736            .expect("build should succeed")
737            .start()
738            .await
739            .expect("start should succeed");
740
741        app.shutdown_token().cancel();
742        app.stop().await.expect("stop hooks should still run");
743
744        assert_eq!(ran.load(Ordering::SeqCst), 2);
745    }
746}