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 = 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                    // A component may have partially initialized before its
336                    // start future yielded. Bound that in-progress start by
337                    // both lifecycle budgets, then include it in rollback.
338                    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    /// Registers both listeners synchronously before any component starts.
553    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    /// Registers both listeners synchronously before any component starts.
579    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}