1use std::{
2 future::Future,
3 sync::Arc,
4 sync::atomic::{AtomicBool, Ordering},
5 time::{Duration, Instant},
6};
7
8use saddle_core::{ComponentLifecycle, ErrorKind, Result, SaddleError};
9
10use crate::RequestLifecycle;
11
12static RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
13const WORKER_THREADS: usize = 2;
14const MAX_IO_EVENTS_PER_TICK: usize = 5;
15const DEFAULT_START_TIMEOUT: Duration = Duration::from_secs(30);
16const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct LifecycleTimeouts {
21 start: Duration,
22 shutdown: Duration,
23}
24
25impl LifecycleTimeouts {
26 pub fn from_millis(start_ms: u64, shutdown_ms: u64) -> Option<Self> {
27 if start_ms == 0 || shutdown_ms == 0 {
28 return None;
29 }
30 Some(Self {
31 start: Duration::from_millis(start_ms),
32 shutdown: Duration::from_millis(shutdown_ms),
33 })
34 }
35}
36
37impl Default for LifecycleTimeouts {
38 fn default() -> Self {
39 Self {
40 start: DEFAULT_START_TIMEOUT,
41 shutdown: DEFAULT_SHUTDOWN_TIMEOUT,
42 }
43 }
44}
45
46pub struct Application {
51 components: Vec<Arc<dyn ComponentLifecycle>>,
52 requests: RequestLifecycle,
53 deployment_resource_budget: Option<saddle_admission::DeploymentResourceBudget>,
54 ingress_bridge_issued: AtomicBool,
55 lifecycle_timeouts: LifecycleTimeouts,
56 shutdown_deadline: Arc<std::sync::Mutex<Option<Instant>>>,
57 lifecycle_observer: Option<(saddle_observability::Observer, String)>,
58 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
59 pending_driver_finalizer: crate::post_driver::PendingDriverFinalizerSlot,
60}
61
62impl Application {
63 pub fn new() -> Self {
65 Self {
66 components: Vec::new(),
67 requests: RequestLifecycle::new(),
68 deployment_resource_budget: None,
69 ingress_bridge_issued: AtomicBool::new(false),
70 lifecycle_timeouts: LifecycleTimeouts::default(),
71 shutdown_deadline: Arc::new(std::sync::Mutex::new(None)),
72 lifecycle_observer: None,
73 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
74 pending_driver_finalizer: crate::post_driver::PendingDriverFinalizerSlot::new(),
75 }
76 }
77
78 #[doc(hidden)]
80 pub fn set_lifecycle_timeouts(&mut self, timeouts: LifecycleTimeouts) {
81 self.lifecycle_timeouts = timeouts;
82 }
83
84 #[doc(hidden)]
85 pub fn install_lifecycle_observer(
86 &mut self,
87 observer: saddle_observability::Observer,
88 application: &str,
89 ) {
90 self.lifecycle_observer = Some((observer, application.to_owned()));
91 }
92
93 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
94 pub(crate) fn install_prevalidated_components(
95 &mut self,
96 components: Vec<Arc<dyn ComponentLifecycle>>,
97 ) {
98 debug_assert!(self.components.is_empty());
99 self.components = components;
100 }
101
102 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
103 #[doc(hidden)]
104 pub fn pending_driver_finalizer(&self) -> crate::post_driver::PendingDriverFinalizerSlot {
105 self.pending_driver_finalizer.clone()
106 }
107
108 #[cfg(all(test, target_arch = "x86_64", target_os = "linux"))]
109 pub(crate) fn post_driver_is_unarmed_for_test(&self) -> bool {
110 self.pending_driver_finalizer.is_unarmed_for_test()
111 }
112
113 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
114 #[doc(hidden)]
115 #[allow(clippy::result_large_err)]
116 pub fn commit_post_driver_install(
117 &self,
118 binding: saddle_admission::VerifiedPostDriverInstallBinding,
119 ) {
120 self.pending_driver_finalizer
121 .commit_verified_install(binding)
122 }
123
124 pub(crate) fn reserved_post_driver_submit(
125 &self,
126 ) -> crate::post_driver::MustSubmitDriverFinalizer {
127 self.pending_driver_finalizer.reserved_submit_handle()
128 }
129
130 pub fn request_lifecycle(&self) -> RequestLifecycle {
132 self.requests.clone()
133 }
134
135 pub fn health(&self) -> crate::ApplicationHealth {
138 self.requests.health()
139 }
140
141 #[doc(hidden)]
144 pub fn managed_ingress_bridge(
145 &self,
146 capacity: usize,
147 ) -> Option<crate::alpha1_ingress::ManagedIngressBridge> {
148 if capacity == 0 {
149 return None;
150 }
151 if self
152 .ingress_bridge_issued
153 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
154 .is_err()
155 {
156 return None;
157 }
158 crate::alpha1_ingress::ManagedIngressBridge::new(self.requests.clone(), capacity)
159 }
160
161 pub fn register<C>(&mut self, component: C) -> Result<()>
165 where
166 C: ComponentLifecycle + 'static,
167 {
168 self.register_shared(Arc::new(component))
169 }
170
171 pub fn register_shared(&mut self, component: Arc<dyn ComponentLifecycle>) -> Result<()> {
173 if self
174 .components
175 .iter()
176 .any(|registered| registered.name() == component.name())
177 {
178 return Err(SaddleError::new(
179 ErrorKind::Conflict,
180 "runtime.duplicate_component",
181 format!("component '{}' is already registered", component.name()),
182 ));
183 }
184 self.components.push(component);
185 Ok(())
186 }
187
188 pub fn run(self) -> Result<()> {
194 Self::run_with(|| async move { Ok(self) })
195 }
196
197 pub fn run_with<F, Fut>(bootstrap: F) -> Result<()>
204 where
205 F: FnOnce() -> Fut + Send + 'static,
206 Fut: Future<Output = Result<Self>> + Send + 'static,
207 {
208 if RUNTIME_STARTED
209 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
210 .is_err()
211 {
212 return Err(SaddleError::new(
213 ErrorKind::Conflict,
214 "runtime.already_started",
215 "the Saddle runtime has already started in this process",
216 ));
217 }
218
219 let runtime = build_runtime()?;
220
221 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
222 {
223 Self::run_with_owned_runtime(runtime, bootstrap)
224 }
225
226 #[cfg(not(all(target_arch = "x86_64", target_os = "linux")))]
227 runtime.block_on(async {
228 let signal = ShutdownSignal::register()?;
229 bootstrap_and_run(bootstrap, signal.wait()).await
230 })
231 }
232
233 #[doc(hidden)]
236 pub fn run_with_deployment_resource_budget<F, Fut>(
237 budget: saddle_admission::DeploymentResourceBudget,
238 bootstrap: F,
239 ) -> Result<()>
240 where
241 F: FnOnce() -> Fut + Send + 'static,
242 Fut: Future<Output = Result<Self>> + Send + 'static,
243 {
244 Self::run_with(move || async move {
245 let mut application = bootstrap().await?;
246 if application.deployment_resource_budget.is_some() {
247 return Err(SaddleError::new(
248 ErrorKind::Conflict,
249 "runtime.deployment_resource_budget_already_installed",
250 "the deployment resource budget was already installed",
251 ));
252 }
253 application.deployment_resource_budget = Some(budget);
254 Ok(application)
255 })
256 }
257
258 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
259 pub(crate) fn claim_process_runtime() -> Result<()> {
260 if RUNTIME_STARTED
261 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
262 .is_err()
263 {
264 return Err(SaddleError::new(
265 ErrorKind::Conflict,
266 "runtime.already_started",
267 "the Saddle runtime has already started in this process",
268 ));
269 }
270 Ok(())
271 }
272
273 #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
274 pub(crate) fn run_with_owned_runtime<F, Fut>(
275 runtime: tokio::runtime::Runtime,
276 bootstrap: F,
277 ) -> Result<()>
278 where
279 F: FnOnce() -> Fut,
280 Fut: Future<Output = Result<Self>>,
281 {
282 let outcome = runtime.block_on(async {
283 let signal = ShutdownSignal::register()?;
284 let application = bootstrap().await?;
285 let finalizer = application.pending_driver_finalizer();
286 let shutdown_deadline = Arc::clone(&application.shutdown_deadline);
287 let lifecycle_observer = application.lifecycle_observer_handle();
288 let result = application.run_until_shutdown(signal.wait()).await;
289 Ok::<_, SaddleError>((finalizer, shutdown_deadline, lifecycle_observer, result))
290 });
291 match outcome {
292 Ok((finalizer, shutdown_deadline, lifecycle_observer, result)) => {
293 let deadline = *shutdown_deadline
294 .lock()
295 .unwrap_or_else(|poisoned| poisoned.into_inner());
296 finalizer.finish(runtime, result, deadline, lifecycle_observer)
297 }
298 Err(error) => {
299 drop(runtime);
300 Err(error)
301 }
302 }
303 }
304
305 pub(crate) async fn run_until_shutdown<F>(self, shutdown: F) -> Result<()>
306 where
307 F: Future<Output = Result<()>>,
308 {
309 tokio::pin!(shutdown);
310 let signal_before_start = tokio::select! {
311 biased;
312 signal_result = &mut shutdown => Some(signal_result),
313 _ = std::future::ready(()) => None,
314 };
315 if let Some(signal_result) = signal_before_start {
316 self.requests.begin_draining();
317 self.requests.wait_until_drained().await;
318 self.requests.mark_stopped();
319 return signal_result;
320 }
321
322 let mut started = 0;
323
324 for component in &self.components {
325 let start_started = Instant::now();
326 let start = component.start();
327 tokio::pin!(start);
328 let mut shutdown_during_start = None;
329 let start_result = tokio::select! {
330 biased;
331 signal_result = &mut shutdown => {
332 let deadline = Instant::now() + self.lifecycle_timeouts.shutdown;
333 self.set_shutdown_deadline(deadline);
334 shutdown_during_start = Some((signal_result, deadline));
335 let start_deadline = std::cmp::min(
339 start_started + self.lifecycle_timeouts.start,
340 deadline,
341 );
342 match tokio::time::timeout_at(start_deadline.into(), start).await {
343 Ok(result) => result,
344 Err(_) => Err(lifecycle_timeout_error("component_start")),
345 }
346 }
347 start_result = tokio::time::timeout(self.lifecycle_timeouts.start, &mut start) => {
348 start_result.unwrap_or_else(|_| Err(lifecycle_timeout_error("component_start")))
349 },
350 };
351
352 if let Err(error) = start_result {
353 self.record_timeout(&error, start_started.elapsed());
354 self.requests.begin_draining();
355 let cleanup_count = if error.code() == "runtime.lifecycle_timeout.component_start" {
356 started + 1
357 } else {
358 started
359 };
360 let deadline = shutdown_during_start
361 .as_ref()
362 .map(|(_, deadline)| *deadline)
363 .unwrap_or_else(|| Instant::now() + self.lifecycle_timeouts.shutdown);
364 self.set_shutdown_deadline(deadline);
365 let drain_result = timeout_at(
366 deadline,
367 self.requests.wait_until_drained(),
368 "request_drain",
369 )
370 .await;
371 if drain_result.is_ok() {
372 let _ = self.shutdown_components(cleanup_count, deadline).await;
373 }
374 self.requests.mark_stopped();
375 return Err(error);
376 }
377 started += 1;
378
379 if let Some((signal_result, deadline)) = shutdown_during_start {
380 self.requests.begin_draining();
381 let drain_result = timeout_at(
382 deadline,
383 self.requests.wait_until_drained(),
384 "request_drain",
385 )
386 .await;
387 let shutdown_result = if drain_result.is_ok() {
388 self.shutdown_components(started, deadline).await
389 } else {
390 drain_result
391 };
392 self.requests.mark_stopped();
393 return signal_result.and(shutdown_result);
394 }
395 }
396
397 self.requests.mark_ready();
398 let signal_result = shutdown.await;
399 let deadline = Instant::now() + self.lifecycle_timeouts.shutdown;
400 self.set_shutdown_deadline(deadline);
401 self.requests.begin_draining();
402 let drain_result = timeout_at(
403 deadline,
404 self.requests.wait_until_drained(),
405 "request_drain",
406 )
407 .await;
408 if let Err(error) = &drain_result {
409 self.record_timeout(error, self.lifecycle_timeouts.shutdown);
410 }
411 let shutdown_result = if drain_result.is_ok() {
412 self.shutdown_components(started, deadline).await
413 } else {
414 drain_result
415 };
416 self.requests.mark_stopped();
417
418 signal_result.and(shutdown_result)
419 }
420
421 fn set_shutdown_deadline(&self, deadline: Instant) {
422 *self
423 .shutdown_deadline
424 .lock()
425 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(deadline);
426 }
427
428 pub(crate) fn shutdown_deadline_handle(&self) -> Arc<std::sync::Mutex<Option<Instant>>> {
429 Arc::clone(&self.shutdown_deadline)
430 }
431
432 pub(crate) fn lifecycle_observer_handle(
433 &self,
434 ) -> Option<(saddle_observability::Observer, String)> {
435 self.lifecycle_observer.clone()
436 }
437
438 fn record_timeout(&self, error: &SaddleError, elapsed: Duration) {
439 let stage = match error.code() {
440 "runtime.lifecycle_timeout.component_start" => {
441 saddle_observability::LifecycleTimeoutStage::ComponentStart
442 }
443 "runtime.lifecycle_timeout.request_drain" => {
444 saddle_observability::LifecycleTimeoutStage::RequestDrain
445 }
446 "runtime.lifecycle_timeout.component_shutdown" => {
447 saddle_observability::LifecycleTimeoutStage::ComponentShutdown
448 }
449 _ => return,
450 };
451 if let Some((observer, application)) = &self.lifecycle_observer {
452 observer.record_lifecycle_timeout(
453 application.as_str(),
454 stage,
455 u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX),
456 );
457 }
458 }
459
460 async fn shutdown_components(&self, started: usize, deadline: Instant) -> Result<()> {
461 let mut first_error = None;
462 for component in self.components[..started].iter().rev() {
463 let result =
464 match timeout_at(deadline, component.shutdown(), "component_shutdown").await {
465 Ok(result) => result,
466 Err(error) => Err(error),
467 };
468 if let Err(error) = result {
469 self.record_timeout(&error, self.lifecycle_timeouts.shutdown);
470 if first_error.is_none() {
471 first_error = Some(error);
472 }
473 if Instant::now() >= deadline {
474 break;
475 }
476 }
477 }
478 first_error.map_or(Ok(()), Err)
479 }
480}
481
482async fn timeout_at<T>(
483 deadline: Instant,
484 future: impl Future<Output = T>,
485 stage: &'static str,
486) -> Result<T> {
487 let remaining = deadline.saturating_duration_since(Instant::now());
488 tokio::time::timeout(remaining, future)
489 .await
490 .map_err(|_| lifecycle_timeout_error(stage))
491}
492
493fn lifecycle_timeout_error(stage: &'static str) -> SaddleError {
494 SaddleError::new(
495 ErrorKind::Infrastructure,
496 match stage {
497 "component_start" => "runtime.lifecycle_timeout.component_start",
498 "request_drain" => "runtime.lifecycle_timeout.request_drain",
499 "component_shutdown" => "runtime.lifecycle_timeout.component_shutdown",
500 _ => "runtime.lifecycle_timeout.post_driver",
501 },
502 format!("managed lifecycle stage '{stage}' exceeded its wall-clock deadline"),
503 )
504}
505
506#[cfg(any(test, not(all(target_arch = "x86_64", target_os = "linux"))))]
507async fn bootstrap_and_run<F, Fut, S>(bootstrap: F, shutdown: S) -> Result<()>
508where
509 F: FnOnce() -> Fut,
510 Fut: Future<Output = Result<Application>>,
511 S: Future<Output = Result<()>>,
512{
513 let application = bootstrap().await?;
514 application.run_until_shutdown(shutdown).await
515}
516
517impl Default for Application {
518 fn default() -> Self {
519 Self::new()
520 }
521}
522
523fn build_runtime() -> Result<tokio::runtime::Runtime> {
524 tokio::runtime::Builder::new_multi_thread()
525 .worker_threads(WORKER_THREADS)
526 .max_io_events_per_tick(MAX_IO_EVENTS_PER_TICK)
527 .enable_all()
528 .build()
529 .map_err(|_| {
530 SaddleError::new(
531 ErrorKind::Infrastructure,
532 "runtime.initialization_failed",
533 "failed to initialize the Saddle async runtime",
534 )
535 })
536}
537
538#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
539pub(crate) fn claim_owned_runtime() -> Result<tokio::runtime::Runtime> {
540 Application::claim_process_runtime()?;
541 build_runtime()
542}
543
544#[cfg(unix)]
545pub(crate) struct ShutdownSignal {
546 interrupt: tokio::signal::unix::Signal,
547 terminate: tokio::signal::unix::Signal,
548}
549
550#[cfg(unix)]
551impl ShutdownSignal {
552 pub(crate) fn register() -> Result<Self> {
554 Ok(Self {
555 interrupt: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
556 .map_err(|_| signal_error())?,
557 terminate: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
558 .map_err(|_| signal_error())?,
559 })
560 }
561
562 pub(crate) async fn wait(mut self) -> Result<()> {
563 tokio::select! {
564 _ = self.interrupt.recv() => Ok(()),
565 _ = self.terminate.recv() => Ok(()),
566 }
567 }
568}
569
570#[cfg(windows)]
571struct ShutdownSignal {
572 ctrl_c: tokio::signal::windows::CtrlC,
573 ctrl_break: tokio::signal::windows::CtrlBreak,
574}
575
576#[cfg(windows)]
577impl ShutdownSignal {
578 fn register() -> Result<Self> {
580 Ok(Self {
581 ctrl_c: tokio::signal::windows::ctrl_c().map_err(|_| signal_error())?,
582 ctrl_break: tokio::signal::windows::ctrl_break().map_err(|_| signal_error())?,
583 })
584 }
585
586 async fn wait(mut self) -> Result<()> {
587 tokio::select! {
588 _ = self.ctrl_c.recv() => Ok(()),
589 _ = self.ctrl_break.recv() => Ok(()),
590 }
591 }
592}
593
594fn signal_error() -> SaddleError {
595 SaddleError::new(
596 ErrorKind::Infrastructure,
597 "runtime.signal_registration_failed",
598 "failed to register the application shutdown signal",
599 )
600}
601
602#[cfg(test)]
603mod tests {
604 use std::sync::Mutex;
605
606 use saddle_core::LifecycleFuture;
607
608 use super::*;
609 use crate::ApplicationPhase;
610
611 struct RecordingComponent {
612 name: &'static str,
613 events: Arc<Mutex<Vec<String>>>,
614 start_error: bool,
615 shutdown_error: bool,
616 }
617
618 struct BlockingStartComponent {
619 events: Arc<Mutex<Vec<String>>>,
620 started: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
621 release: Mutex<Option<tokio::sync::oneshot::Receiver<()>>>,
622 }
623
624 struct BlockingShutdownComponent {
625 events: Arc<Mutex<Vec<String>>>,
626 }
627
628 struct HealthAwareListener {
629 health: crate::ApplicationHealth,
630 events: Arc<Mutex<Vec<String>>>,
631 }
632
633 impl ComponentLifecycle for RecordingComponent {
634 fn name(&self) -> &'static str {
635 self.name
636 }
637
638 fn start(&self) -> LifecycleFuture<'_> {
639 Box::pin(async move {
640 self.events
641 .lock()
642 .unwrap()
643 .push(format!("start:{}", self.name));
644 if self.start_error {
645 Err(test_error("start failed"))
646 } else {
647 Ok(())
648 }
649 })
650 }
651
652 fn shutdown(&self) -> LifecycleFuture<'_> {
653 Box::pin(async move {
654 self.events
655 .lock()
656 .unwrap()
657 .push(format!("shutdown:{}", self.name));
658 if self.shutdown_error {
659 Err(test_error("shutdown failed"))
660 } else {
661 Ok(())
662 }
663 })
664 }
665 }
666
667 impl ComponentLifecycle for BlockingStartComponent {
668 fn name(&self) -> &'static str {
669 "blocking"
670 }
671
672 fn start(&self) -> LifecycleFuture<'_> {
673 Box::pin(async move {
674 self.events
675 .lock()
676 .unwrap()
677 .push("start:blocking".to_owned());
678 let started = self.started.lock().unwrap().take().unwrap();
679 let release = self.release.lock().unwrap().take().unwrap();
680 started.send(()).unwrap();
681 release.await.unwrap();
682 Ok(())
683 })
684 }
685
686 fn shutdown(&self) -> LifecycleFuture<'_> {
687 Box::pin(async move {
688 self.events
689 .lock()
690 .unwrap()
691 .push("shutdown:blocking".to_owned());
692 Ok(())
693 })
694 }
695 }
696
697 impl ComponentLifecycle for BlockingShutdownComponent {
698 fn name(&self) -> &'static str {
699 "blocking-shutdown"
700 }
701
702 fn start(&self) -> LifecycleFuture<'_> {
703 Box::pin(async move {
704 self.events
705 .lock()
706 .unwrap()
707 .push("start:blocking-shutdown".into());
708 Ok(())
709 })
710 }
711
712 fn shutdown(&self) -> LifecycleFuture<'_> {
713 Box::pin(async move {
714 self.events
715 .lock()
716 .unwrap()
717 .push("shutdown:blocking-shutdown".into());
718 std::future::pending().await
719 })
720 }
721 }
722
723 impl ComponentLifecycle for HealthAwareListener {
724 fn name(&self) -> &'static str {
725 "health-aware-listener"
726 }
727
728 fn start(&self) -> LifecycleFuture<'_> {
729 Box::pin(async move {
730 let snapshot = self.health.snapshot();
731 assert!(snapshot.is_live());
732 assert!(!snapshot.is_ready());
733 assert_eq!(snapshot.phase(), ApplicationPhase::Starting);
734 self.events
735 .lock()
736 .unwrap()
737 .push("listener:accepting".into());
738 Ok(())
739 })
740 }
741
742 fn shutdown(&self) -> LifecycleFuture<'_> {
743 Box::pin(async move {
744 let snapshot = self.health.snapshot();
745 assert!(snapshot.is_live());
746 assert!(!snapshot.is_ready());
747 assert_eq!(snapshot.phase(), ApplicationPhase::Draining);
748 self.events.lock().unwrap().push("listener:stopped".into());
749 Ok(())
750 })
751 }
752 }
753
754 fn component(name: &'static str, events: &Arc<Mutex<Vec<String>>>) -> RecordingComponent {
755 RecordingComponent {
756 name,
757 events: Arc::clone(events),
758 start_error: false,
759 shutdown_error: false,
760 }
761 }
762
763 fn test_error(message: &'static str) -> SaddleError {
764 SaddleError::new(ErrorKind::Infrastructure, "test.failure", message)
765 }
766
767 fn test_runtime() -> tokio::runtime::Runtime {
768 tokio::runtime::Builder::new_current_thread()
769 .enable_time()
770 .build()
771 .expect("test runtime must build")
772 }
773
774 async fn shutdown_when_ready(requests: RequestLifecycle) -> Result<()> {
775 while requests.phase() != crate::ApplicationPhase::Ready {
776 tokio::task::yield_now().await;
777 }
778 Ok(())
779 }
780
781 #[test]
782 fn components_start_in_order_and_shutdown_in_reverse() {
783 let events = Arc::new(Mutex::new(Vec::new()));
784 let mut application = Application::new();
785 application.register(component("db", &events)).unwrap();
786 application.register(component("service", &events)).unwrap();
787 let shutdown = shutdown_when_ready(application.request_lifecycle());
788
789 test_runtime()
790 .block_on(application.run_until_shutdown(shutdown))
791 .unwrap();
792
793 assert_eq!(
794 *events.lock().unwrap(),
795 [
796 "start:db",
797 "start:service",
798 "shutdown:service",
799 "shutdown:db"
800 ]
801 );
802 }
803
804 #[test]
805 fn health_uses_the_unique_lifecycle_and_clears_ready_before_listener_shutdown() {
806 test_runtime().block_on(async {
807 let events = Arc::new(Mutex::new(Vec::new()));
808 let mut application = Application::new();
809 let health = application.health();
810 let initial = health.snapshot();
811 assert!(initial.is_live());
812 assert!(!initial.is_ready());
813 assert_eq!(initial.phase(), ApplicationPhase::Starting);
814
815 application
816 .register(HealthAwareListener {
817 health: health.clone(),
818 events: Arc::clone(&events),
819 })
820 .unwrap();
821 let shutdown_health = health.clone();
822 application
823 .run_until_shutdown(async move {
824 loop {
825 let snapshot = shutdown_health.snapshot();
826 if snapshot.is_ready() {
827 assert!(snapshot.is_live());
828 assert_eq!(snapshot.phase(), ApplicationPhase::Ready);
829 return Ok(());
830 }
831 tokio::task::yield_now().await;
832 }
833 })
834 .await
835 .unwrap();
836
837 let stopped = health.snapshot();
838 assert!(!stopped.is_live());
839 assert!(!stopped.is_ready());
840 assert_eq!(stopped.phase(), ApplicationPhase::Stopped);
841 assert_eq!(
842 *events.lock().unwrap(),
843 ["listener:accepting", "listener:stopped"]
844 );
845 });
846 }
847
848 #[test]
849 fn async_bootstrap_runs_before_early_shutdown_prevents_component_start() {
850 let events = Arc::new(Mutex::new(Vec::new()));
851 let bootstrap_events = Arc::clone(&events);
852
853 test_runtime()
854 .block_on(bootstrap_and_run(
855 move || async move {
856 bootstrap_events
857 .lock()
858 .unwrap()
859 .push("bootstrap".to_owned());
860 let mut application = Application::new();
861 application.register(component("component", &bootstrap_events))?;
862 Ok(application)
863 },
864 std::future::ready(Ok(())),
865 ))
866 .unwrap();
867
868 assert_eq!(*events.lock().unwrap(), ["bootstrap"]);
869 }
870
871 #[test]
872 fn failed_async_bootstrap_does_not_start_components() {
873 let error = test_runtime()
874 .block_on(bootstrap_and_run(
875 || async { Err(test_error("bootstrap failed")) },
876 std::future::pending(),
877 ))
878 .unwrap_err();
879 assert_eq!(error.message(), "bootstrap failed");
880 }
881
882 #[test]
883 fn startup_failure_rolls_back_only_started_components() {
884 let events = Arc::new(Mutex::new(Vec::new()));
885 let mut application = Application::new();
886 application.register(component("first", &events)).unwrap();
887 let mut failing = component("failing", &events);
888 failing.start_error = true;
889 application.register(failing).unwrap();
890 application.register(component("never", &events)).unwrap();
891 let shutdown = shutdown_when_ready(application.request_lifecycle());
892
893 let error = test_runtime()
894 .block_on(application.run_until_shutdown(shutdown))
895 .unwrap_err();
896
897 assert_eq!(error.message(), "start failed");
898 assert_eq!(
899 *events.lock().unwrap(),
900 ["start:first", "start:failing", "shutdown:first"]
901 );
902 }
903
904 #[test]
905 fn shutdown_continues_after_a_component_error() {
906 let events = Arc::new(Mutex::new(Vec::new()));
907 let mut application = Application::new();
908 application.register(component("first", &events)).unwrap();
909 let mut failing = component("second", &events);
910 failing.shutdown_error = true;
911 application.register(failing).unwrap();
912 let shutdown = shutdown_when_ready(application.request_lifecycle());
913
914 let error = test_runtime()
915 .block_on(application.run_until_shutdown(shutdown))
916 .unwrap_err();
917
918 assert_eq!(error.message(), "shutdown failed");
919 assert_eq!(
920 *events.lock().unwrap(),
921 [
922 "start:first",
923 "start:second",
924 "shutdown:second",
925 "shutdown:first"
926 ]
927 );
928 }
929
930 #[test]
931 fn duplicate_component_names_are_rejected() {
932 let events = Arc::new(Mutex::new(Vec::new()));
933 let mut application = Application::new();
934 application.register(component("db", &events)).unwrap();
935
936 let error = application.register(component("db", &events)).unwrap_err();
937 assert_eq!(error.code(), "runtime.duplicate_component");
938 }
939
940 #[test]
941 fn application_shutdown_waits_for_an_admitted_request() {
942 test_runtime().block_on(async {
943 let application = Application::new();
944 let requests = application.request_lifecycle();
945 let (release, released) = tokio::sync::oneshot::channel();
946
947 let shutdown = async move {
948 shutdown_when_ready(requests.clone()).await?;
949 let request = requests
950 .try_accept()
951 .expect("application is ready before waiting for shutdown");
952 tokio::spawn(async move {
953 released.await.unwrap();
954 drop(request);
955 });
956 Ok(())
957 };
958 let running = tokio::spawn(application.run_until_shutdown(shutdown));
959
960 tokio::task::yield_now().await;
961 assert!(!running.is_finished());
962 release.send(()).unwrap();
963 running.await.unwrap().unwrap();
964 });
965 }
966
967 #[test]
968 fn signal_failure_before_start_prevents_component_startup() {
969 let events = Arc::new(Mutex::new(Vec::new()));
970 let mut application = Application::new();
971 application.register(component("service", &events)).unwrap();
972
973 let error = test_runtime()
974 .block_on(application.run_until_shutdown(async { Err(signal_error()) }))
975 .unwrap_err();
976
977 assert_eq!(error.code(), "runtime.signal_registration_failed");
978 assert!(events.lock().unwrap().is_empty());
979 }
980
981 #[test]
982 fn shutdown_during_startup_stops_starting_and_rolls_back() {
983 test_runtime().block_on(async {
984 let events = Arc::new(Mutex::new(Vec::new()));
985 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
986 let (release_tx, release_rx) = tokio::sync::oneshot::channel();
987 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
988 let mut application = Application::new();
989 application
990 .register(BlockingStartComponent {
991 events: Arc::clone(&events),
992 started: Mutex::new(Some(started_tx)),
993 release: Mutex::new(Some(release_rx)),
994 })
995 .unwrap();
996 application.register(component("never", &events)).unwrap();
997
998 let running = tokio::spawn(application.run_until_shutdown(async move {
999 shutdown_rx.await.unwrap();
1000 Ok(())
1001 }));
1002 started_rx.await.unwrap();
1003 shutdown_tx.send(()).unwrap();
1004 tokio::task::yield_now().await;
1005 release_tx.send(()).unwrap();
1006
1007 running.await.unwrap().unwrap();
1008 assert_eq!(
1009 *events.lock().unwrap(),
1010 ["start:blocking", "shutdown:blocking"]
1011 );
1012 });
1013 }
1014
1015 #[test]
1016 fn managed_runtime_provides_an_async_io_driver() {
1017 build_runtime()
1018 .unwrap()
1019 .block_on(async {
1020 tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).await
1021 })
1022 .expect("service listeners require the managed async I/O driver");
1023 }
1024
1025 #[test]
1026 fn blocked_component_start_times_out_and_rolls_back_started_components() {
1027 test_runtime().block_on(async {
1028 let events = Arc::new(Mutex::new(Vec::new()));
1029 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1030 let (_release_tx, release_rx) = tokio::sync::oneshot::channel();
1031 let mut application = Application::new();
1032 application.set_lifecycle_timeouts(LifecycleTimeouts::from_millis(10, 100).unwrap());
1033 application.register(component("first", &events)).unwrap();
1034 application
1035 .register(BlockingStartComponent {
1036 events: Arc::clone(&events),
1037 started: Mutex::new(Some(started_tx)),
1038 release: Mutex::new(Some(release_rx)),
1039 })
1040 .unwrap();
1041
1042 let running = tokio::spawn(application.run_until_shutdown(std::future::pending()));
1043 started_rx.await.unwrap();
1044 let error = running.await.unwrap().unwrap_err();
1045 assert_eq!(error.code(), "runtime.lifecycle_timeout.component_start");
1046 assert_eq!(
1047 *events.lock().unwrap(),
1048 [
1049 "start:first",
1050 "start:blocking",
1051 "shutdown:blocking",
1052 "shutdown:first"
1053 ]
1054 );
1055 });
1056 }
1057
1058 #[test]
1059 fn blocked_component_shutdown_uses_one_total_deadline_and_is_not_clean() {
1060 test_runtime().block_on(async {
1061 let events = Arc::new(Mutex::new(Vec::new()));
1062 let mut application = Application::new();
1063 application.set_lifecycle_timeouts(LifecycleTimeouts::from_millis(100, 10).unwrap());
1064 application
1065 .register(BlockingShutdownComponent {
1066 events: Arc::clone(&events),
1067 })
1068 .unwrap();
1069 let shutdown = shutdown_when_ready(application.request_lifecycle());
1070 let error = application.run_until_shutdown(shutdown).await.unwrap_err();
1071 assert_eq!(error.code(), "runtime.lifecycle_timeout.component_shutdown");
1072 assert_eq!(
1073 *events.lock().unwrap(),
1074 ["start:blocking-shutdown", "shutdown:blocking-shutdown"]
1075 );
1076 });
1077 }
1078}