Skip to main content

saddle_runtime/
application.rs

1use std::{
2    future::Future,
3    sync::Arc,
4    sync::atomic::{AtomicBool, Ordering},
5};
6
7use saddle_core::{ComponentLifecycle, ErrorKind, Result, SaddleError};
8
9use crate::RequestLifecycle;
10
11static RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
12const WORKER_THREADS: usize = 2;
13const MAX_IO_EVENTS_PER_TICK: usize = 5;
14
15/// A complete Saddle application hosted by the process-wide async runtime.
16///
17/// This is an assembly API, not a general-purpose async executor: it exposes no
18/// Tokio handle, task spawning, runtime configuration, or arbitrary `block_on`.
19pub struct Application {
20    components: Vec<Arc<dyn ComponentLifecycle>>,
21    requests: RequestLifecycle,
22    #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
23    pending_driver_finalizer: crate::post_driver::PendingDriverFinalizerSlot,
24}
25
26impl Application {
27    /// Creates an empty application assembly.
28    pub fn new() -> Self {
29        Self {
30            components: Vec::new(),
31            requests: RequestLifecycle::new(),
32            #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
33            pending_driver_finalizer: crate::post_driver::PendingDriverFinalizerSlot::new(),
34        }
35    }
36
37    #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
38    pub(crate) fn install_prevalidated_components(
39        &mut self,
40        components: Vec<Arc<dyn ComponentLifecycle>>,
41    ) {
42        debug_assert!(self.components.is_empty());
43        self.components = components;
44    }
45
46    #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
47    #[doc(hidden)]
48    pub fn pending_driver_finalizer(&self) -> crate::post_driver::PendingDriverFinalizerSlot {
49        self.pending_driver_finalizer.clone()
50    }
51
52    /// Returns the request lifecycle shared with Saddle's Service adapter.
53    pub fn request_lifecycle(&self) -> RequestLifecycle {
54        self.requests.clone()
55    }
56
57    /// Registers a framework component for managed startup and shutdown.
58    ///
59    /// Components start in registration order and stop in reverse order.
60    pub fn register<C>(&mut self, component: C) -> Result<()>
61    where
62        C: ComponentLifecycle + 'static,
63    {
64        self.register_shared(Arc::new(component))
65    }
66
67    /// Registers an already shared framework component.
68    pub fn register_shared(&mut self, component: Arc<dyn ComponentLifecycle>) -> Result<()> {
69        if self
70            .components
71            .iter()
72            .any(|registered| registered.name() == component.name())
73        {
74            return Err(SaddleError::new(
75                ErrorKind::Conflict,
76                "runtime.duplicate_component",
77                format!("component '{}' is already registered", component.name()),
78            ));
79        }
80        self.components.push(component);
81        Ok(())
82    }
83
84    /// Runs the application on Saddle's single process-wide async runtime.
85    ///
86    /// The call blocks the process entry thread until SIGINT or, on Unix,
87    /// SIGTERM. Shutdown first closes request admission, then waits for every
88    /// admitted request, and finally stops components in reverse order.
89    pub fn run(self) -> Result<()> {
90        Self::run_with(|| async move { Ok(self) })
91    }
92
93    /// Creates the application inside Saddle's process-wide async runtime and
94    /// then runs it until shutdown.
95    ///
96    /// This is the framework assembly path for components whose initialization
97    /// performs async I/O. Business code is not given a runtime handle or an
98    /// executor through this API.
99    pub fn run_with<F, Fut>(bootstrap: F) -> Result<()>
100    where
101        F: FnOnce() -> Fut + Send + 'static,
102        Fut: Future<Output = Result<Self>> + Send + 'static,
103    {
104        if RUNTIME_STARTED
105            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
106            .is_err()
107        {
108            return Err(SaddleError::new(
109                ErrorKind::Conflict,
110                "runtime.already_started",
111                "the Saddle runtime has already started in this process",
112            ));
113        }
114
115        let runtime = build_runtime()?;
116
117        #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
118        {
119            Self::run_with_owned_runtime(runtime, bootstrap)
120        }
121
122        #[cfg(not(all(target_arch = "x86_64", target_os = "linux")))]
123        runtime.block_on(async {
124            let signal = ShutdownSignal::register()?;
125            bootstrap_and_run(bootstrap, signal.wait()).await
126        })
127    }
128
129    #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
130    pub(crate) fn claim_process_runtime() -> Result<()> {
131        if RUNTIME_STARTED
132            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
133            .is_err()
134        {
135            return Err(SaddleError::new(
136                ErrorKind::Conflict,
137                "runtime.already_started",
138                "the Saddle runtime has already started in this process",
139            ));
140        }
141        Ok(())
142    }
143
144    #[cfg(all(target_arch = "x86_64", target_os = "linux"))]
145    pub(crate) fn run_with_owned_runtime<F, Fut>(
146        runtime: tokio::runtime::Runtime,
147        bootstrap: F,
148    ) -> Result<()>
149    where
150        F: FnOnce() -> Fut,
151        Fut: Future<Output = Result<Self>>,
152    {
153        let outcome = runtime.block_on(async {
154            let signal = ShutdownSignal::register()?;
155            let application = bootstrap().await?;
156            let finalizer = application.pending_driver_finalizer();
157            let result = application.run_until_shutdown(signal.wait()).await;
158            Ok::<_, SaddleError>((finalizer, result))
159        });
160        match outcome {
161            Ok((finalizer, result)) => finalizer.finish(runtime, result),
162            Err(error) => {
163                drop(runtime);
164                Err(error)
165            }
166        }
167    }
168
169    pub(crate) async fn run_until_shutdown<F>(self, shutdown: F) -> Result<()>
170    where
171        F: Future<Output = Result<()>>,
172    {
173        tokio::pin!(shutdown);
174        let signal_before_start = tokio::select! {
175            biased;
176            signal_result = &mut shutdown => Some(signal_result),
177            _ = std::future::ready(()) => None,
178        };
179        if let Some(signal_result) = signal_before_start {
180            self.requests.begin_draining();
181            self.requests.wait_until_drained().await;
182            self.requests.mark_stopped();
183            return signal_result;
184        }
185
186        let mut started = 0;
187
188        for component in &self.components {
189            let start = component.start();
190            tokio::pin!(start);
191            let mut shutdown_during_start = None;
192            let start_result = tokio::select! {
193                biased;
194                signal_result = &mut shutdown => {
195                    shutdown_during_start = Some(signal_result);
196                    // ComponentLifecycle does not define cancellation-safe
197                    // startup. Finish the in-progress start before rollback so
198                    // partially initialized resources can be shut down safely.
199                    start.await
200                }
201                start_result = &mut start => start_result,
202            };
203
204            if let Err(error) = start_result {
205                self.requests.begin_draining();
206                self.requests.wait_until_drained().await;
207                let _ = self.shutdown_components(started).await;
208                self.requests.mark_stopped();
209                return Err(error);
210            }
211            started += 1;
212
213            if let Some(signal_result) = shutdown_during_start {
214                self.requests.begin_draining();
215                self.requests.wait_until_drained().await;
216                let shutdown_result = self.shutdown_components(started).await;
217                self.requests.mark_stopped();
218                return signal_result.and(shutdown_result);
219            }
220        }
221
222        self.requests.mark_ready();
223        let signal_result = shutdown.await;
224        self.requests.begin_draining();
225        self.requests.wait_until_drained().await;
226        let shutdown_result = self.shutdown_components(started).await;
227        self.requests.mark_stopped();
228
229        signal_result.and(shutdown_result)
230    }
231
232    async fn shutdown_components(&self, started: usize) -> Result<()> {
233        let mut first_error = None;
234        for component in self.components[..started].iter().rev() {
235            if let Err(error) = component.shutdown().await {
236                if first_error.is_none() {
237                    first_error = Some(error);
238                }
239            }
240        }
241        first_error.map_or(Ok(()), Err)
242    }
243}
244
245#[cfg(any(test, not(all(target_arch = "x86_64", target_os = "linux"))))]
246async fn bootstrap_and_run<F, Fut, S>(bootstrap: F, shutdown: S) -> Result<()>
247where
248    F: FnOnce() -> Fut,
249    Fut: Future<Output = Result<Application>>,
250    S: Future<Output = Result<()>>,
251{
252    let application = bootstrap().await?;
253    application.run_until_shutdown(shutdown).await
254}
255
256impl Default for Application {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262fn build_runtime() -> Result<tokio::runtime::Runtime> {
263    tokio::runtime::Builder::new_multi_thread()
264        .worker_threads(WORKER_THREADS)
265        .max_io_events_per_tick(MAX_IO_EVENTS_PER_TICK)
266        .enable_all()
267        .build()
268        .map_err(|_| {
269            SaddleError::new(
270                ErrorKind::Infrastructure,
271                "runtime.initialization_failed",
272                "failed to initialize the Saddle async runtime",
273            )
274        })
275}
276
277#[cfg(unix)]
278pub(crate) struct ShutdownSignal {
279    interrupt: tokio::signal::unix::Signal,
280    terminate: tokio::signal::unix::Signal,
281}
282
283#[cfg(unix)]
284impl ShutdownSignal {
285    /// Registers both listeners synchronously before any component starts.
286    pub(crate) fn register() -> Result<Self> {
287        Ok(Self {
288            interrupt: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
289                .map_err(|_| signal_error())?,
290            terminate: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
291                .map_err(|_| signal_error())?,
292        })
293    }
294
295    pub(crate) async fn wait(mut self) -> Result<()> {
296        tokio::select! {
297            _ = self.interrupt.recv() => Ok(()),
298            _ = self.terminate.recv() => Ok(()),
299        }
300    }
301}
302
303#[cfg(windows)]
304struct ShutdownSignal {
305    ctrl_c: tokio::signal::windows::CtrlC,
306    ctrl_break: tokio::signal::windows::CtrlBreak,
307}
308
309#[cfg(windows)]
310impl ShutdownSignal {
311    /// Registers both listeners synchronously before any component starts.
312    fn register() -> Result<Self> {
313        Ok(Self {
314            ctrl_c: tokio::signal::windows::ctrl_c().map_err(|_| signal_error())?,
315            ctrl_break: tokio::signal::windows::ctrl_break().map_err(|_| signal_error())?,
316        })
317    }
318
319    async fn wait(mut self) -> Result<()> {
320        tokio::select! {
321            _ = self.ctrl_c.recv() => Ok(()),
322            _ = self.ctrl_break.recv() => Ok(()),
323        }
324    }
325}
326
327fn signal_error() -> SaddleError {
328    SaddleError::new(
329        ErrorKind::Infrastructure,
330        "runtime.signal_registration_failed",
331        "failed to register the application shutdown signal",
332    )
333}
334
335#[cfg(test)]
336mod tests {
337    use std::sync::Mutex;
338
339    use saddle_core::LifecycleFuture;
340
341    use super::*;
342
343    struct RecordingComponent {
344        name: &'static str,
345        events: Arc<Mutex<Vec<String>>>,
346        start_error: bool,
347        shutdown_error: bool,
348    }
349
350    struct BlockingStartComponent {
351        events: Arc<Mutex<Vec<String>>>,
352        started: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
353        release: Mutex<Option<tokio::sync::oneshot::Receiver<()>>>,
354    }
355
356    impl ComponentLifecycle for RecordingComponent {
357        fn name(&self) -> &'static str {
358            self.name
359        }
360
361        fn start(&self) -> LifecycleFuture<'_> {
362            Box::pin(async move {
363                self.events
364                    .lock()
365                    .unwrap()
366                    .push(format!("start:{}", self.name));
367                if self.start_error {
368                    Err(test_error("start failed"))
369                } else {
370                    Ok(())
371                }
372            })
373        }
374
375        fn shutdown(&self) -> LifecycleFuture<'_> {
376            Box::pin(async move {
377                self.events
378                    .lock()
379                    .unwrap()
380                    .push(format!("shutdown:{}", self.name));
381                if self.shutdown_error {
382                    Err(test_error("shutdown failed"))
383                } else {
384                    Ok(())
385                }
386            })
387        }
388    }
389
390    impl ComponentLifecycle for BlockingStartComponent {
391        fn name(&self) -> &'static str {
392            "blocking"
393        }
394
395        fn start(&self) -> LifecycleFuture<'_> {
396            Box::pin(async move {
397                self.events
398                    .lock()
399                    .unwrap()
400                    .push("start:blocking".to_owned());
401                let started = self.started.lock().unwrap().take().unwrap();
402                let release = self.release.lock().unwrap().take().unwrap();
403                started.send(()).unwrap();
404                release.await.unwrap();
405                Ok(())
406            })
407        }
408
409        fn shutdown(&self) -> LifecycleFuture<'_> {
410            Box::pin(async move {
411                self.events
412                    .lock()
413                    .unwrap()
414                    .push("shutdown:blocking".to_owned());
415                Ok(())
416            })
417        }
418    }
419
420    fn component(name: &'static str, events: &Arc<Mutex<Vec<String>>>) -> RecordingComponent {
421        RecordingComponent {
422            name,
423            events: Arc::clone(events),
424            start_error: false,
425            shutdown_error: false,
426        }
427    }
428
429    fn test_error(message: &'static str) -> SaddleError {
430        SaddleError::new(ErrorKind::Infrastructure, "test.failure", message)
431    }
432
433    fn test_runtime() -> tokio::runtime::Runtime {
434        tokio::runtime::Builder::new_current_thread()
435            .build()
436            .expect("test runtime must build")
437    }
438
439    async fn shutdown_when_ready(requests: RequestLifecycle) -> Result<()> {
440        while requests.phase() != crate::ApplicationPhase::Ready {
441            tokio::task::yield_now().await;
442        }
443        Ok(())
444    }
445
446    #[test]
447    fn components_start_in_order_and_shutdown_in_reverse() {
448        let events = Arc::new(Mutex::new(Vec::new()));
449        let mut application = Application::new();
450        application.register(component("db", &events)).unwrap();
451        application.register(component("service", &events)).unwrap();
452        let shutdown = shutdown_when_ready(application.request_lifecycle());
453
454        test_runtime()
455            .block_on(application.run_until_shutdown(shutdown))
456            .unwrap();
457
458        assert_eq!(
459            *events.lock().unwrap(),
460            [
461                "start:db",
462                "start:service",
463                "shutdown:service",
464                "shutdown:db"
465            ]
466        );
467    }
468
469    #[test]
470    fn async_bootstrap_runs_before_early_shutdown_prevents_component_start() {
471        let events = Arc::new(Mutex::new(Vec::new()));
472        let bootstrap_events = Arc::clone(&events);
473
474        test_runtime()
475            .block_on(bootstrap_and_run(
476                move || async move {
477                    bootstrap_events
478                        .lock()
479                        .unwrap()
480                        .push("bootstrap".to_owned());
481                    let mut application = Application::new();
482                    application.register(component("component", &bootstrap_events))?;
483                    Ok(application)
484                },
485                std::future::ready(Ok(())),
486            ))
487            .unwrap();
488
489        assert_eq!(*events.lock().unwrap(), ["bootstrap"]);
490    }
491
492    #[test]
493    fn failed_async_bootstrap_does_not_start_components() {
494        let error = test_runtime()
495            .block_on(bootstrap_and_run(
496                || async { Err(test_error("bootstrap failed")) },
497                std::future::pending(),
498            ))
499            .unwrap_err();
500        assert_eq!(error.message(), "bootstrap failed");
501    }
502
503    #[test]
504    fn startup_failure_rolls_back_only_started_components() {
505        let events = Arc::new(Mutex::new(Vec::new()));
506        let mut application = Application::new();
507        application.register(component("first", &events)).unwrap();
508        let mut failing = component("failing", &events);
509        failing.start_error = true;
510        application.register(failing).unwrap();
511        application.register(component("never", &events)).unwrap();
512        let shutdown = shutdown_when_ready(application.request_lifecycle());
513
514        let error = test_runtime()
515            .block_on(application.run_until_shutdown(shutdown))
516            .unwrap_err();
517
518        assert_eq!(error.message(), "start failed");
519        assert_eq!(
520            *events.lock().unwrap(),
521            ["start:first", "start:failing", "shutdown:first"]
522        );
523    }
524
525    #[test]
526    fn shutdown_continues_after_a_component_error() {
527        let events = Arc::new(Mutex::new(Vec::new()));
528        let mut application = Application::new();
529        application.register(component("first", &events)).unwrap();
530        let mut failing = component("second", &events);
531        failing.shutdown_error = true;
532        application.register(failing).unwrap();
533        let shutdown = shutdown_when_ready(application.request_lifecycle());
534
535        let error = test_runtime()
536            .block_on(application.run_until_shutdown(shutdown))
537            .unwrap_err();
538
539        assert_eq!(error.message(), "shutdown failed");
540        assert_eq!(
541            *events.lock().unwrap(),
542            [
543                "start:first",
544                "start:second",
545                "shutdown:second",
546                "shutdown:first"
547            ]
548        );
549    }
550
551    #[test]
552    fn duplicate_component_names_are_rejected() {
553        let events = Arc::new(Mutex::new(Vec::new()));
554        let mut application = Application::new();
555        application.register(component("db", &events)).unwrap();
556
557        let error = application.register(component("db", &events)).unwrap_err();
558        assert_eq!(error.code(), "runtime.duplicate_component");
559    }
560
561    #[test]
562    fn application_shutdown_waits_for_an_admitted_request() {
563        test_runtime().block_on(async {
564            let application = Application::new();
565            let requests = application.request_lifecycle();
566            let (release, released) = tokio::sync::oneshot::channel();
567
568            let shutdown = async move {
569                shutdown_when_ready(requests.clone()).await?;
570                let request = requests
571                    .try_accept()
572                    .expect("application is ready before waiting for shutdown");
573                tokio::spawn(async move {
574                    released.await.unwrap();
575                    drop(request);
576                });
577                Ok(())
578            };
579            let running = tokio::spawn(application.run_until_shutdown(shutdown));
580
581            tokio::task::yield_now().await;
582            assert!(!running.is_finished());
583            release.send(()).unwrap();
584            running.await.unwrap().unwrap();
585        });
586    }
587
588    #[test]
589    fn signal_failure_before_start_prevents_component_startup() {
590        let events = Arc::new(Mutex::new(Vec::new()));
591        let mut application = Application::new();
592        application.register(component("service", &events)).unwrap();
593
594        let error = test_runtime()
595            .block_on(application.run_until_shutdown(async { Err(signal_error()) }))
596            .unwrap_err();
597
598        assert_eq!(error.code(), "runtime.signal_registration_failed");
599        assert!(events.lock().unwrap().is_empty());
600    }
601
602    #[test]
603    fn shutdown_during_startup_stops_starting_and_rolls_back() {
604        test_runtime().block_on(async {
605            let events = Arc::new(Mutex::new(Vec::new()));
606            let (started_tx, started_rx) = tokio::sync::oneshot::channel();
607            let (release_tx, release_rx) = tokio::sync::oneshot::channel();
608            let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
609            let mut application = Application::new();
610            application
611                .register(BlockingStartComponent {
612                    events: Arc::clone(&events),
613                    started: Mutex::new(Some(started_tx)),
614                    release: Mutex::new(Some(release_rx)),
615                })
616                .unwrap();
617            application.register(component("never", &events)).unwrap();
618
619            let running = tokio::spawn(application.run_until_shutdown(async move {
620                shutdown_rx.await.unwrap();
621                Ok(())
622            }));
623            started_rx.await.unwrap();
624            shutdown_tx.send(()).unwrap();
625            tokio::task::yield_now().await;
626            release_tx.send(()).unwrap();
627
628            running.await.unwrap().unwrap();
629            assert_eq!(
630                *events.lock().unwrap(),
631                ["start:blocking", "shutdown:blocking"]
632            );
633        });
634    }
635
636    #[test]
637    fn managed_runtime_provides_an_async_io_driver() {
638        build_runtime()
639            .unwrap()
640            .block_on(async {
641                tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).await
642            })
643            .expect("service listeners require the managed async I/O driver");
644    }
645}