Skip to main content

saddle_runtime/
application.rs

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/// Fixed wall-clock limits for the managed component lifecycle.
19#[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
46/// A complete Saddle application hosted by the process-wide async runtime.
47///
48/// This is an assembly API, not a general-purpose async executor: it exposes no
49/// Tokio handle, task spawning, runtime configuration, or arbitrary `block_on`.
50pub 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    /// Creates an empty application assembly.
64    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    /// Installs the frozen process lifecycle policy before component startup.
79    #[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    /// Returns the request lifecycle shared with Saddle's Service adapter.
131    pub fn request_lifecycle(&self) -> RequestLifecycle {
132        self.requests.clone()
133    }
134
135    /// Returns a read-only observer of the framework's unique lifecycle
136    /// state. The observer cannot admit requests or mutate readiness.
137    pub fn health(&self) -> crate::ApplicationHealth {
138        self.requests.health()
139    }
140
141    /// Reserves the fixed alpha.1 Ingress execution bridge attached to this
142    /// application's existing 0.2 request lifecycle.
143    #[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    /// Registers a framework component for managed startup and shutdown.
162    ///
163    /// Components start in registration order and stop in reverse order.
164    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    /// Registers an already shared framework component.
172    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    /// Runs the application on Saddle's single process-wide async runtime.
189    ///
190    /// The call blocks the process entry thread until SIGINT or, on Unix,
191    /// SIGTERM. Shutdown first closes request admission, then waits for every
192    /// admitted request, and finally stops components in reverse order.
193    pub fn run(self) -> Result<()> {
194        Self::run_with(|| async move { Ok(self) })
195    }
196
197    /// Creates the application inside Saddle's process-wide async runtime and
198    /// then runs it until shutdown.
199    ///
200    /// This is the framework assembly path for components whose initialization
201    /// performs async I/O. Business code is not given a runtime handle or an
202    /// executor through this API.
203    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    /// Runs the formal process while retaining its one frozen deployment
234    /// budget inside Runtime assembly. No read or replacement surface escapes.
235    #[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                    // A component may have partially initialized before its
340                    // start future yielded. Bound that in-progress start by
341                    // both lifecycle budgets, then include it in rollback.
342                    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                // Fix cleanup's budget BEFORE any diagnostic capture/submit.
358                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    /// Registers both listeners synchronously before any component starts.
623    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    /// Registers both listeners synchronously before any component starts.
649    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}