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 = crate::diagnostics::task(
327 async { component.start().await },
328 saddle_core::DiagnosticStage::StartupListener,
329 "runtime.component_start",
330 );
331 tokio::pin!(start);
332 let mut shutdown_during_start = None;
333 let start_result = tokio::select! {
334 biased;
335 signal_result = &mut shutdown => {
336 let deadline = Instant::now() + self.lifecycle_timeouts.shutdown;
337 self.set_shutdown_deadline(deadline);
338 shutdown_during_start = Some((signal_result, deadline));
339 let start_deadline = std::cmp::min(
343 start_started + self.lifecycle_timeouts.start,
344 deadline,
345 );
346 match tokio::time::timeout_at(start_deadline.into(), start).await {
347 Ok(result) => result,
348 Err(_) => Err(lifecycle_timeout_error("component_start")),
349 }
350 }
351 start_result = tokio::time::timeout(self.lifecycle_timeouts.start, &mut start) => {
352 start_result.unwrap_or_else(|_| Err(lifecycle_timeout_error("component_start")))
353 },
354 };
355
356 if let Err(error) = start_result {
357 let deadline = shutdown_during_start
359 .as_ref()
360 .map(|(_, deadline)| *deadline)
361 .unwrap_or_else(|| Instant::now() + self.lifecycle_timeouts.shutdown);
362 self.set_shutdown_deadline(deadline);
363 let error = crate::diagnostics::attach(
364 error,
365 saddle_core::DiagnosticStage::StartupListener,
366 "runtime.component_start_failed",
367 );
368 crate::diagnostics::report(&error);
369 self.record_timeout(&error, start_started.elapsed());
370 self.requests.begin_draining();
371 let cleanup_count = if error.code() == "runtime.lifecycle_timeout.component_start" {
372 started + 1
373 } else {
374 started
375 };
376 let drain_result = timeout_at(
377 deadline,
378 self.requests.wait_until_drained(),
379 "request_drain",
380 )
381 .await;
382 if drain_result.is_ok() {
383 let _ = self
384 .shutdown_components(cleanup_count, deadline, Some(&error))
385 .await;
386 } else if let Err(cleanup) = drain_result {
387 crate::diagnostics::cleanup(
388 cleanup,
389 Some(&error),
390 saddle_core::DiagnosticStage::ShutdownComponent,
391 );
392 }
393 self.requests.mark_stopped();
394 return Err(error);
395 }
396 started += 1;
397
398 if let Some((signal_result, deadline)) = shutdown_during_start {
399 self.requests.begin_draining();
400 let drain_result = timeout_at(
401 deadline,
402 self.requests.wait_until_drained(),
403 "request_drain",
404 )
405 .await;
406 let signal_result = signal_result.map_err(|e| {
407 let e = crate::diagnostics::attach(
408 e,
409 saddle_core::DiagnosticStage::ShutdownComponent,
410 "runtime.shutdown_signal_failed",
411 );
412 crate::diagnostics::report(&e);
413 e
414 });
415 let shutdown_result = if drain_result.is_ok() {
416 self.shutdown_components(started, deadline, signal_result.as_ref().err())
417 .await
418 } else {
419 drain_result.map_err(|e| {
420 crate::diagnostics::cleanup(
421 e,
422 signal_result.as_ref().err(),
423 saddle_core::DiagnosticStage::ShutdownComponent,
424 )
425 })
426 };
427 self.requests.mark_stopped();
428 return signal_result.and(shutdown_result);
429 }
430 }
431
432 self.requests.mark_ready();
433 let signal_result = shutdown.await;
434 let deadline = Instant::now() + self.lifecycle_timeouts.shutdown;
435 self.set_shutdown_deadline(deadline);
436 self.requests.begin_draining();
437 let signal_result = signal_result.map_err(|e| {
438 let e = crate::diagnostics::attach(
439 e,
440 saddle_core::DiagnosticStage::ShutdownComponent,
441 "runtime.shutdown_signal_failed",
442 );
443 crate::diagnostics::report(&e);
444 e
445 });
446 let drain_result = timeout_at(
447 deadline,
448 self.requests.wait_until_drained(),
449 "request_drain",
450 )
451 .await;
452 if let Err(error) = &drain_result {
453 self.record_timeout(error, self.lifecycle_timeouts.shutdown);
454 }
455 let shutdown_result = if drain_result.is_ok() {
456 self.shutdown_components(started, deadline, signal_result.as_ref().err())
457 .await
458 } else {
459 drain_result.map_err(|e| {
460 crate::diagnostics::cleanup(
461 e,
462 signal_result.as_ref().err(),
463 saddle_core::DiagnosticStage::ShutdownComponent,
464 )
465 })
466 };
467 self.requests.mark_stopped();
468
469 signal_result.and(shutdown_result)
470 }
471
472 fn set_shutdown_deadline(&self, deadline: Instant) {
473 *self
474 .shutdown_deadline
475 .lock()
476 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(deadline);
477 }
478
479 pub(crate) fn shutdown_deadline_handle(&self) -> Arc<std::sync::Mutex<Option<Instant>>> {
480 Arc::clone(&self.shutdown_deadline)
481 }
482
483 pub(crate) fn lifecycle_observer_handle(
484 &self,
485 ) -> Option<(saddle_observability::Observer, String)> {
486 self.lifecycle_observer.clone()
487 }
488
489 fn record_timeout(&self, error: &SaddleError, elapsed: Duration) {
490 let stage = match error.code() {
491 "runtime.lifecycle_timeout.component_start" => {
492 saddle_observability::LifecycleTimeoutStage::ComponentStart
493 }
494 "runtime.lifecycle_timeout.request_drain" => {
495 saddle_observability::LifecycleTimeoutStage::RequestDrain
496 }
497 "runtime.lifecycle_timeout.component_shutdown" => {
498 saddle_observability::LifecycleTimeoutStage::ComponentShutdown
499 }
500 _ => return,
501 };
502 if let Some((observer, application)) = &self.lifecycle_observer {
503 observer.record_lifecycle_timeout(
504 application.as_str(),
505 stage,
506 u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX),
507 );
508 }
509 }
510
511 async fn shutdown_components(
512 &self,
513 started: usize,
514 deadline: Instant,
515 primary: Option<&SaddleError>,
516 ) -> Result<()> {
517 let mut first_error = None;
518 for component in self.components[..started].iter().rev() {
519 let result = match timeout_at(
520 deadline,
521 crate::diagnostics::task(
522 async { component.shutdown().await },
523 saddle_core::DiagnosticStage::ShutdownComponent,
524 "runtime.component_shutdown",
525 ),
526 "component_shutdown",
527 )
528 .await
529 {
530 Ok(result) => result,
531 Err(error) => Err(error),
532 };
533 if let Err(error) = result {
534 let error = crate::diagnostics::cleanup(
535 error,
536 primary.or(first_error.as_ref()),
537 saddle_core::DiagnosticStage::ShutdownComponent,
538 );
539 self.record_timeout(&error, self.lifecycle_timeouts.shutdown);
540 if first_error.is_none() {
541 first_error = Some(error);
542 }
543 if Instant::now() >= deadline {
544 break;
545 }
546 }
547 }
548 first_error.map_or(Ok(()), Err)
549 }
550}
551
552async fn timeout_at<T>(
553 deadline: Instant,
554 future: impl Future<Output = T>,
555 stage: &'static str,
556) -> Result<T> {
557 let remaining = deadline.saturating_duration_since(Instant::now());
558 tokio::time::timeout(remaining, future)
559 .await
560 .map_err(|_| lifecycle_timeout_error(stage))
561}
562
563fn lifecycle_timeout_error(stage: &'static str) -> SaddleError {
564 SaddleError::new(
565 ErrorKind::Infrastructure,
566 match stage {
567 "component_start" => "runtime.lifecycle_timeout.component_start",
568 "request_drain" => "runtime.lifecycle_timeout.request_drain",
569 "component_shutdown" => "runtime.lifecycle_timeout.component_shutdown",
570 _ => "runtime.lifecycle_timeout.post_driver",
571 },
572 format!("managed lifecycle stage '{stage}' exceeded its wall-clock deadline"),
573 )
574}
575
576#[cfg(any(test, not(all(target_arch = "x86_64", target_os = "linux"))))]
577async fn bootstrap_and_run<F, Fut, S>(bootstrap: F, shutdown: S) -> Result<()>
578where
579 F: FnOnce() -> Fut,
580 Fut: Future<Output = Result<Application>>,
581 S: Future<Output = Result<()>>,
582{
583 let application = bootstrap().await?;
584 application.run_until_shutdown(shutdown).await
585}
586
587impl Default for Application {
588 fn default() -> Self {
589 Self::new()
590 }
591}
592
593fn build_runtime() -> Result<tokio::runtime::Runtime> {
594 tokio::runtime::Builder::new_multi_thread()
595 .worker_threads(WORKER_THREADS)
596 .max_io_events_per_tick(MAX_IO_EVENTS_PER_TICK)
597 .enable_all()
598 .build()
599 .map_err(|_| {
600 SaddleError::new(
601 ErrorKind::Infrastructure,
602 "runtime.initialization_failed",
603 "failed to initialize the Saddle async runtime",
604 )
605 })
606}
607
608#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
609pub(crate) fn claim_owned_runtime() -> Result<tokio::runtime::Runtime> {
610 Application::claim_process_runtime()?;
611 build_runtime()
612}
613
614#[cfg(unix)]
615pub(crate) struct ShutdownSignal {
616 interrupt: tokio::signal::unix::Signal,
617 terminate: tokio::signal::unix::Signal,
618}
619
620#[cfg(unix)]
621impl ShutdownSignal {
622 pub(crate) fn register() -> Result<Self> {
624 Ok(Self {
625 interrupt: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
626 .map_err(|_| signal_error())?,
627 terminate: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
628 .map_err(|_| signal_error())?,
629 })
630 }
631
632 pub(crate) async fn wait(mut self) -> Result<()> {
633 tokio::select! {
634 _ = self.interrupt.recv() => Ok(()),
635 _ = self.terminate.recv() => Ok(()),
636 }
637 }
638}
639
640#[cfg(windows)]
641struct ShutdownSignal {
642 ctrl_c: tokio::signal::windows::CtrlC,
643 ctrl_break: tokio::signal::windows::CtrlBreak,
644}
645
646#[cfg(windows)]
647impl ShutdownSignal {
648 fn register() -> Result<Self> {
650 Ok(Self {
651 ctrl_c: tokio::signal::windows::ctrl_c().map_err(|_| signal_error())?,
652 ctrl_break: tokio::signal::windows::ctrl_break().map_err(|_| signal_error())?,
653 })
654 }
655
656 async fn wait(mut self) -> Result<()> {
657 tokio::select! {
658 _ = self.ctrl_c.recv() => Ok(()),
659 _ = self.ctrl_break.recv() => Ok(()),
660 }
661 }
662}
663
664fn signal_error() -> SaddleError {
665 SaddleError::new(
666 ErrorKind::Infrastructure,
667 "runtime.signal_registration_failed",
668 "failed to register the application shutdown signal",
669 )
670}
671
672#[cfg(test)]
673mod tests {
674 use std::sync::Mutex;
675
676 use saddle_core::LifecycleFuture;
677
678 use super::*;
679 use crate::ApplicationPhase;
680
681 struct RecordingComponent {
682 name: &'static str,
683 events: Arc<Mutex<Vec<String>>>,
684 start_error: bool,
685 shutdown_error: bool,
686 }
687
688 struct BlockingStartComponent {
689 events: Arc<Mutex<Vec<String>>>,
690 started: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
691 release: Mutex<Option<tokio::sync::oneshot::Receiver<()>>>,
692 }
693
694 struct BlockingShutdownComponent {
695 events: Arc<Mutex<Vec<String>>>,
696 }
697
698 struct HealthAwareListener {
699 health: crate::ApplicationHealth,
700 events: Arc<Mutex<Vec<String>>>,
701 }
702
703 impl ComponentLifecycle for RecordingComponent {
704 fn name(&self) -> &'static str {
705 self.name
706 }
707
708 fn start(&self) -> LifecycleFuture<'_> {
709 Box::pin(async move {
710 self.events
711 .lock()
712 .unwrap()
713 .push(format!("start:{}", self.name));
714 if self.start_error {
715 Err(test_error("start failed"))
716 } else {
717 Ok(())
718 }
719 })
720 }
721
722 fn shutdown(&self) -> LifecycleFuture<'_> {
723 Box::pin(async move {
724 self.events
725 .lock()
726 .unwrap()
727 .push(format!("shutdown:{}", self.name));
728 if self.shutdown_error {
729 Err(test_error("shutdown failed"))
730 } else {
731 Ok(())
732 }
733 })
734 }
735 }
736
737 impl ComponentLifecycle for BlockingStartComponent {
738 fn name(&self) -> &'static str {
739 "blocking"
740 }
741
742 fn start(&self) -> LifecycleFuture<'_> {
743 Box::pin(async move {
744 self.events
745 .lock()
746 .unwrap()
747 .push("start:blocking".to_owned());
748 let started = self.started.lock().unwrap().take().unwrap();
749 let release = self.release.lock().unwrap().take().unwrap();
750 started.send(()).unwrap();
751 release.await.unwrap();
752 Ok(())
753 })
754 }
755
756 fn shutdown(&self) -> LifecycleFuture<'_> {
757 Box::pin(async move {
758 self.events
759 .lock()
760 .unwrap()
761 .push("shutdown:blocking".to_owned());
762 Ok(())
763 })
764 }
765 }
766
767 impl ComponentLifecycle for BlockingShutdownComponent {
768 fn name(&self) -> &'static str {
769 "blocking-shutdown"
770 }
771
772 fn start(&self) -> LifecycleFuture<'_> {
773 Box::pin(async move {
774 self.events
775 .lock()
776 .unwrap()
777 .push("start:blocking-shutdown".into());
778 Ok(())
779 })
780 }
781
782 fn shutdown(&self) -> LifecycleFuture<'_> {
783 Box::pin(async move {
784 self.events
785 .lock()
786 .unwrap()
787 .push("shutdown:blocking-shutdown".into());
788 std::future::pending().await
789 })
790 }
791 }
792
793 impl ComponentLifecycle for HealthAwareListener {
794 fn name(&self) -> &'static str {
795 "health-aware-listener"
796 }
797
798 fn start(&self) -> LifecycleFuture<'_> {
799 Box::pin(async move {
800 let snapshot = self.health.snapshot();
801 assert!(snapshot.is_live());
802 assert!(!snapshot.is_ready());
803 assert_eq!(snapshot.phase(), ApplicationPhase::Starting);
804 self.events
805 .lock()
806 .unwrap()
807 .push("listener:accepting".into());
808 Ok(())
809 })
810 }
811
812 fn shutdown(&self) -> LifecycleFuture<'_> {
813 Box::pin(async move {
814 let snapshot = self.health.snapshot();
815 assert!(snapshot.is_live());
816 assert!(!snapshot.is_ready());
817 assert_eq!(snapshot.phase(), ApplicationPhase::Draining);
818 self.events.lock().unwrap().push("listener:stopped".into());
819 Ok(())
820 })
821 }
822 }
823
824 fn component(name: &'static str, events: &Arc<Mutex<Vec<String>>>) -> RecordingComponent {
825 RecordingComponent {
826 name,
827 events: Arc::clone(events),
828 start_error: false,
829 shutdown_error: false,
830 }
831 }
832
833 fn test_error(message: &'static str) -> SaddleError {
834 SaddleError::new(ErrorKind::Infrastructure, "test.failure", message)
835 }
836
837 fn test_runtime() -> tokio::runtime::Runtime {
838 tokio::runtime::Builder::new_current_thread()
839 .enable_time()
840 .build()
841 .expect("test runtime must build")
842 }
843
844 async fn shutdown_when_ready(requests: RequestLifecycle) -> Result<()> {
845 while requests.phase() != crate::ApplicationPhase::Ready {
846 tokio::task::yield_now().await;
847 }
848 Ok(())
849 }
850
851 #[test]
852 fn components_start_in_order_and_shutdown_in_reverse() {
853 let events = Arc::new(Mutex::new(Vec::new()));
854 let mut application = Application::new();
855 application.register(component("db", &events)).unwrap();
856 application.register(component("service", &events)).unwrap();
857 let shutdown = shutdown_when_ready(application.request_lifecycle());
858
859 test_runtime()
860 .block_on(application.run_until_shutdown(shutdown))
861 .unwrap();
862
863 assert_eq!(
864 *events.lock().unwrap(),
865 [
866 "start:db",
867 "start:service",
868 "shutdown:service",
869 "shutdown:db"
870 ]
871 );
872 }
873
874 #[test]
875 fn health_uses_the_unique_lifecycle_and_clears_ready_before_listener_shutdown() {
876 test_runtime().block_on(async {
877 let events = Arc::new(Mutex::new(Vec::new()));
878 let mut application = Application::new();
879 let health = application.health();
880 let initial = health.snapshot();
881 assert!(initial.is_live());
882 assert!(!initial.is_ready());
883 assert_eq!(initial.phase(), ApplicationPhase::Starting);
884
885 application
886 .register(HealthAwareListener {
887 health: health.clone(),
888 events: Arc::clone(&events),
889 })
890 .unwrap();
891 let shutdown_health = health.clone();
892 application
893 .run_until_shutdown(async move {
894 loop {
895 let snapshot = shutdown_health.snapshot();
896 if snapshot.is_ready() {
897 assert!(snapshot.is_live());
898 assert_eq!(snapshot.phase(), ApplicationPhase::Ready);
899 return Ok(());
900 }
901 tokio::task::yield_now().await;
902 }
903 })
904 .await
905 .unwrap();
906
907 let stopped = health.snapshot();
908 assert!(!stopped.is_live());
909 assert!(!stopped.is_ready());
910 assert_eq!(stopped.phase(), ApplicationPhase::Stopped);
911 assert_eq!(
912 *events.lock().unwrap(),
913 ["listener:accepting", "listener:stopped"]
914 );
915 });
916 }
917
918 #[test]
919 fn async_bootstrap_runs_before_early_shutdown_prevents_component_start() {
920 let events = Arc::new(Mutex::new(Vec::new()));
921 let bootstrap_events = Arc::clone(&events);
922
923 test_runtime()
924 .block_on(bootstrap_and_run(
925 move || async move {
926 bootstrap_events
927 .lock()
928 .unwrap()
929 .push("bootstrap".to_owned());
930 let mut application = Application::new();
931 application.register(component("component", &bootstrap_events))?;
932 Ok(application)
933 },
934 std::future::ready(Ok(())),
935 ))
936 .unwrap();
937
938 assert_eq!(*events.lock().unwrap(), ["bootstrap"]);
939 }
940
941 #[test]
942 fn failed_async_bootstrap_does_not_start_components() {
943 let error = test_runtime()
944 .block_on(bootstrap_and_run(
945 || async { Err(test_error("bootstrap failed")) },
946 std::future::pending(),
947 ))
948 .unwrap_err();
949 assert_eq!(error.message(), "bootstrap failed");
950 }
951
952 #[test]
953 fn startup_failure_rolls_back_only_started_components() {
954 let events = Arc::new(Mutex::new(Vec::new()));
955 let mut application = Application::new();
956 application.register(component("first", &events)).unwrap();
957 let mut failing = component("failing", &events);
958 failing.start_error = true;
959 application.register(failing).unwrap();
960 application.register(component("never", &events)).unwrap();
961 let shutdown = shutdown_when_ready(application.request_lifecycle());
962
963 let error = test_runtime()
964 .block_on(application.run_until_shutdown(shutdown))
965 .unwrap_err();
966
967 assert_eq!(error.message(), "start failed");
968 assert_eq!(
969 *events.lock().unwrap(),
970 ["start:first", "start:failing", "shutdown:first"]
971 );
972 }
973
974 #[test]
975 fn shutdown_continues_after_a_component_error() {
976 let events = Arc::new(Mutex::new(Vec::new()));
977 let mut application = Application::new();
978 application.register(component("first", &events)).unwrap();
979 let mut failing = component("second", &events);
980 failing.shutdown_error = true;
981 application.register(failing).unwrap();
982 let shutdown = shutdown_when_ready(application.request_lifecycle());
983
984 let error = test_runtime()
985 .block_on(application.run_until_shutdown(shutdown))
986 .unwrap_err();
987
988 assert_eq!(error.message(), "shutdown failed");
989 assert_eq!(
990 *events.lock().unwrap(),
991 [
992 "start:first",
993 "start:second",
994 "shutdown:second",
995 "shutdown:first"
996 ]
997 );
998 }
999
1000 #[test]
1001 fn duplicate_component_names_are_rejected() {
1002 let events = Arc::new(Mutex::new(Vec::new()));
1003 let mut application = Application::new();
1004 application.register(component("db", &events)).unwrap();
1005
1006 let error = application.register(component("db", &events)).unwrap_err();
1007 assert_eq!(error.code(), "runtime.duplicate_component");
1008 }
1009
1010 #[test]
1011 fn application_shutdown_waits_for_an_admitted_request() {
1012 test_runtime().block_on(async {
1013 let application = Application::new();
1014 let requests = application.request_lifecycle();
1015 let (release, released) = tokio::sync::oneshot::channel();
1016
1017 let shutdown = async move {
1018 shutdown_when_ready(requests.clone()).await?;
1019 let request = requests
1020 .try_accept()
1021 .expect("application is ready before waiting for shutdown");
1022 tokio::spawn(async move {
1023 released.await.unwrap();
1024 drop(request);
1025 });
1026 Ok(())
1027 };
1028 let running = tokio::spawn(application.run_until_shutdown(shutdown));
1029
1030 tokio::task::yield_now().await;
1031 assert!(!running.is_finished());
1032 release.send(()).unwrap();
1033 running.await.unwrap().unwrap();
1034 });
1035 }
1036
1037 #[test]
1038 fn signal_failure_before_start_prevents_component_startup() {
1039 let events = Arc::new(Mutex::new(Vec::new()));
1040 let mut application = Application::new();
1041 application.register(component("service", &events)).unwrap();
1042
1043 let error = test_runtime()
1044 .block_on(application.run_until_shutdown(async { Err(signal_error()) }))
1045 .unwrap_err();
1046
1047 assert_eq!(error.code(), "runtime.signal_registration_failed");
1048 assert!(events.lock().unwrap().is_empty());
1049 }
1050
1051 #[test]
1052 fn shutdown_during_startup_stops_starting_and_rolls_back() {
1053 test_runtime().block_on(async {
1054 let events = Arc::new(Mutex::new(Vec::new()));
1055 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1056 let (release_tx, release_rx) = tokio::sync::oneshot::channel();
1057 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
1058 let mut application = Application::new();
1059 application
1060 .register(BlockingStartComponent {
1061 events: Arc::clone(&events),
1062 started: Mutex::new(Some(started_tx)),
1063 release: Mutex::new(Some(release_rx)),
1064 })
1065 .unwrap();
1066 application.register(component("never", &events)).unwrap();
1067
1068 let running = tokio::spawn(application.run_until_shutdown(async move {
1069 shutdown_rx.await.unwrap();
1070 Ok(())
1071 }));
1072 started_rx.await.unwrap();
1073 shutdown_tx.send(()).unwrap();
1074 tokio::task::yield_now().await;
1075 release_tx.send(()).unwrap();
1076
1077 running.await.unwrap().unwrap();
1078 assert_eq!(
1079 *events.lock().unwrap(),
1080 ["start:blocking", "shutdown:blocking"]
1081 );
1082 });
1083 }
1084
1085 #[test]
1086 fn managed_runtime_provides_an_async_io_driver() {
1087 build_runtime()
1088 .unwrap()
1089 .block_on(async {
1090 tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).await
1091 })
1092 .expect("service listeners require the managed async I/O driver");
1093 }
1094
1095 #[test]
1096 fn blocked_component_start_times_out_and_rolls_back_started_components() {
1097 test_runtime().block_on(async {
1098 let events = Arc::new(Mutex::new(Vec::new()));
1099 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1100 let (_release_tx, release_rx) = tokio::sync::oneshot::channel();
1101 let mut application = Application::new();
1102 application.set_lifecycle_timeouts(LifecycleTimeouts::from_millis(10, 100).unwrap());
1103 application.register(component("first", &events)).unwrap();
1104 application
1105 .register(BlockingStartComponent {
1106 events: Arc::clone(&events),
1107 started: Mutex::new(Some(started_tx)),
1108 release: Mutex::new(Some(release_rx)),
1109 })
1110 .unwrap();
1111
1112 let running = tokio::spawn(application.run_until_shutdown(std::future::pending()));
1113 started_rx.await.unwrap();
1114 let error = running.await.unwrap().unwrap_err();
1115 assert_eq!(error.code(), "runtime.lifecycle_timeout.component_start");
1116 assert_eq!(
1117 *events.lock().unwrap(),
1118 [
1119 "start:first",
1120 "start:blocking",
1121 "shutdown:blocking",
1122 "shutdown:first"
1123 ]
1124 );
1125 });
1126 }
1127
1128 #[test]
1129 fn blocked_component_shutdown_uses_one_total_deadline_and_is_not_clean() {
1130 test_runtime().block_on(async {
1131 let events = Arc::new(Mutex::new(Vec::new()));
1132 let mut application = Application::new();
1133 application.set_lifecycle_timeouts(LifecycleTimeouts::from_millis(100, 10).unwrap());
1134 application
1135 .register(BlockingShutdownComponent {
1136 events: Arc::clone(&events),
1137 })
1138 .unwrap();
1139 let shutdown = shutdown_when_ready(application.request_lifecycle());
1140 let error = application.run_until_shutdown(shutdown).await.unwrap_err();
1141 assert_eq!(error.code(), "runtime.lifecycle_timeout.component_shutdown");
1142 assert_eq!(
1143 *events.lock().unwrap(),
1144 ["start:blocking-shutdown", "shutdown:blocking-shutdown"]
1145 );
1146 });
1147 }
1148}