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
19pub struct Unconfigured;
24
25pub struct Built;
27
28pub struct Started;
30
31pub struct Stopped;
33
34pub 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 #[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 #[must_use]
63 pub fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
64 self.graceful_timeout = timeout;
65 self
66 }
67
68 #[must_use]
70 pub fn with_component(mut self, component: Arc<dyn Component>) -> Self {
71 self.components.push(component);
72 self
73 }
74
75 #[must_use]
77 pub fn with_container(mut self, container: Arc<Container>) -> Self {
78 self.container = container;
79 self
80 }
81
82 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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
177pub 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 pub fn builder(config: C) -> AppBuilder<C> {
200 AppBuilder::new(config)
201 }
202}
203
204impl<C> App<Built, C> {
205 #[must_use]
207 pub fn config(&self) -> Arc<C> {
208 Arc::clone(&self.config)
209 }
210
211 #[must_use]
213 pub fn container(&self) -> Arc<Container> {
214 Arc::clone(&self.container)
215 }
216
217 #[must_use]
219 pub fn lifecycle_event_bus(&self) -> EventBus<LifecycleEvent> {
220 self.lifecycle_events.clone()
221 }
222
223 #[must_use]
225 pub fn shutdown_token(&self) -> CancellationToken {
226 self.shutdown_token.clone()
227 }
228
229 #[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 #[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 #[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 #[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 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 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 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 #[must_use]
407 pub fn config(&self) -> Arc<C> {
408 Arc::clone(&self.config)
409 }
410
411 #[must_use]
413 pub fn container(&self) -> Arc<Container> {
414 Arc::clone(&self.container)
415 }
416
417 #[must_use]
419 pub fn lifecycle_event_bus(&self) -> EventBus<LifecycleEvent> {
420 self.lifecycle_events.clone()
421 }
422
423 #[must_use]
425 pub fn shutdown_token(&self) -> CancellationToken {
426 self.shutdown_token.clone()
427 }
428
429 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 #[must_use]
499 pub fn config(&self) -> Arc<C> {
500 Arc::clone(&self.config)
501 }
502
503 #[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}