Skip to main content

rainier_container/
application.rs

1//! The [`Application`] — the container plus a lifecycle.
2//!
3//! Laravel's `Illuminate\Foundation\Application` *is* a container with extra
4//! responsibilities, and this mirrors that: `Application` derefs to
5//! [`Container`], so `app.singleton(..)` and `app.resolve::<T>()` work directly,
6//! while the application adds the parts a bare registry has no opinion about —
7//! the two-phase provider lifecycle, the environment, the base paths, and the
8//! lifecycle **hooks**.
9//!
10//! ```
11//! # use rainier_container::{Application, Container};
12//! # #[tokio::main] async fn main() -> rainier_support::Result<()> {
13//! let app = Application::new(".");
14//! app.instance(42u32);
15//! app.booted(|_| println!("ready"));
16//! app.boot().await?;
17//! assert_eq!(*app.resolve::<u32>()?, 42);
18//! # Ok(()) }
19//! ```
20
21use std::path::{Path, PathBuf};
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::{Arc, Mutex, RwLock};
24
25use rainier_support::Result;
26
27use crate::container::Container;
28use crate::provider::ServiceProvider;
29
30/// A callback invoked at a point in the application lifecycle.
31pub type LifecycleHook = Box<dyn Fn(&Application) + Send + Sync>;
32
33/// The application: a [`Container`] with a boot lifecycle, an environment, and
34/// a filesystem layout.
35pub struct Application {
36    container: Arc<Container>,
37    providers: Mutex<Vec<Arc<dyn ServiceProvider>>>,
38    /// How many entries of `providers` have already been booted. Providers
39    /// registered after boot are booted by the next `boot()` call.
40    booted_upto: Mutex<usize>,
41    booting_hooks: Mutex<Vec<LifecycleHook>>,
42    booted_hooks: Mutex<Vec<LifecycleHook>>,
43    terminating_hooks: Mutex<Vec<LifecycleHook>>,
44    environment: RwLock<String>,
45    base_path: PathBuf,
46    booted: AtomicBool,
47}
48
49impl Application {
50    /// A new application rooted at `base_path`, in the `production`
51    /// environment until told otherwise.
52    pub fn new(base_path: impl Into<PathBuf>) -> Self {
53        Self {
54            container: Arc::new(Container::new()),
55            providers: Mutex::new(Vec::new()),
56            booted_upto: Mutex::new(0),
57            booting_hooks: Mutex::new(Vec::new()),
58            booted_hooks: Mutex::new(Vec::new()),
59            terminating_hooks: Mutex::new(Vec::new()),
60            environment: RwLock::new("production".to_string()),
61            base_path: base_path.into(),
62            booted: AtomicBool::new(false),
63        }
64    }
65
66    /// Builder form of [`set_environment`](Self::set_environment).
67    pub fn with_environment(self, environment: impl Into<String>) -> Self {
68        self.set_environment(environment);
69        self
70    }
71
72    /// The shared container. Hand this to anything that needs to resolve
73    /// services without holding the whole application.
74    pub fn container(&self) -> &Arc<Container> {
75        &self.container
76    }
77
78    // --- providers ---------------------------------------------------------
79
80    /// Register a provider, running its
81    /// [`register`](ServiceProvider::register) pass immediately.
82    ///
83    /// Booting is deliberately *not* done here: a provider's
84    /// [`boot`](ServiceProvider::boot) may resolve services that a
85    /// later-registered provider binds, so booting has to wait until every
86    /// provider has registered. Register everything, then call
87    /// [`boot`](Self::boot).
88    pub fn register(&self, provider: impl ServiceProvider) -> Result<()> {
89        self.register_arc(Arc::new(provider))
90    }
91
92    /// [`register`](Self::register) for a provider that is already shared.
93    pub fn register_arc(&self, provider: Arc<dyn ServiceProvider>) -> Result<()> {
94        provider.register(self)?;
95        self.providers.lock().expect("providers lock poisoned").push(provider);
96        Ok(())
97    }
98
99    /// The names of every registered provider, in registration order.
100    pub fn provider_names(&self) -> Vec<&'static str> {
101        self.providers.lock().expect("providers lock poisoned").iter().map(|p| p.name()).collect()
102    }
103
104    // --- lifecycle ---------------------------------------------------------
105
106    /// Boot every provider that has not booted yet, running the `booting`
107    /// hooks first and the `booted` hooks after.
108    ///
109    /// Idempotent, and safe to call again after registering more providers —
110    /// only the new ones boot.
111    pub async fn boot(&self) -> Result<()> {
112        let first_boot = !self.booted.load(Ordering::SeqCst);
113        if first_boot {
114            self.run_hooks(&self.booting_hooks);
115        }
116
117        loop {
118            // Take one provider at a time rather than snapshotting the list:
119            // a provider's `boot` is allowed to register further providers,
120            // and those must boot in this same pass.
121            let next = {
122                let providers = self.providers.lock().expect("providers lock poisoned");
123                let mut upto = self.booted_upto.lock().expect("providers lock poisoned");
124                if *upto >= providers.len() {
125                    break;
126                }
127                let provider = Arc::clone(&providers[*upto]);
128                *upto += 1;
129                provider
130            };
131
132            next.boot(self).await.map_err(|e| {
133                rainier_support::Error::internal(format!(
134                    "service provider `{}` failed to boot: {e}",
135                    next.name()
136                ))
137            })?;
138        }
139
140        self.booted.store(true, Ordering::SeqCst);
141        if first_boot {
142            self.run_hooks(&self.booted_hooks);
143        }
144        Ok(())
145    }
146
147    /// Whether [`boot`](Self::boot) has completed at least once.
148    pub fn is_booted(&self) -> bool {
149        self.booted.load(Ordering::SeqCst)
150    }
151
152    /// Run `hook` immediately before providers boot.
153    ///
154    /// Ignored if the application has already booted — "before boot" has
155    /// passed, and silently running it late would be worse than not running it.
156    pub fn booting(&self, hook: impl Fn(&Application) + Send + Sync + 'static) {
157        if self.is_booted() {
158            tracing::warn!("`booting` hook registered after the application booted; ignoring");
159            return;
160        }
161        self.booting_hooks.lock().expect("hooks lock poisoned").push(Box::new(hook));
162    }
163
164    /// Run `hook` once the application has booted — or immediately, if it
165    /// already has. The immediate call is what makes this usable from code
166    /// that cannot know whether boot has happened yet.
167    pub fn booted(&self, hook: impl Fn(&Application) + Send + Sync + 'static) {
168        if self.is_booted() {
169            hook(self);
170            return;
171        }
172        self.booted_hooks.lock().expect("hooks lock poisoned").push(Box::new(hook));
173    }
174
175    /// Run `hook` when [`terminate`](Self::terminate) is called — after a
176    /// response has been sent, for work that should not delay it.
177    pub fn terminating(&self, hook: impl Fn(&Application) + Send + Sync + 'static) {
178        self.terminating_hooks.lock().expect("hooks lock poisoned").push(Box::new(hook));
179    }
180
181    /// Run the `terminating` hooks. The HTTP kernel calls this after flushing
182    /// a response; a console command calls it before exiting.
183    pub fn terminate(&self) {
184        self.run_hooks(&self.terminating_hooks);
185    }
186
187    fn run_hooks(&self, hooks: &Mutex<Vec<LifecycleHook>>) {
188        // Drain into a local first: a hook is free to register another hook,
189        // and holding the lock across the callback would deadlock.
190        let drained: Vec<LifecycleHook> =
191            hooks.lock().expect("hooks lock poisoned").drain(..).collect();
192        for hook in drained {
193            hook(self);
194        }
195    }
196
197    // --- environment -------------------------------------------------------
198
199    /// The current environment name (`local`, `testing`, `production`, …).
200    pub fn environment(&self) -> String {
201        self.environment.read().expect("environment lock poisoned").clone()
202    }
203
204    /// Set the environment name.
205    pub fn set_environment(&self, environment: impl Into<String>) {
206        *self.environment.write().expect("environment lock poisoned") = environment.into();
207    }
208
209    /// Whether the environment is any of `names`.
210    pub fn environment_is(&self, names: &[&str]) -> bool {
211        let current = self.environment();
212        names.iter().any(|n| *n == current)
213    }
214
215    /// Whether the environment is `local`.
216    pub fn is_local(&self) -> bool {
217        self.environment_is(&["local"])
218    }
219
220    /// Whether the environment is `testing`.
221    pub fn is_testing(&self) -> bool {
222        self.environment_is(&["testing", "test"])
223    }
224
225    /// Whether the environment is `production`.
226    pub fn is_production(&self) -> bool {
227        self.environment_is(&["production", "prod"])
228    }
229
230    // --- paths -------------------------------------------------------------
231
232    /// The application root.
233    pub fn base_path(&self) -> &Path {
234        &self.base_path
235    }
236
237    /// A path relative to the application root.
238    pub fn path(&self, relative: impl AsRef<Path>) -> PathBuf {
239        self.base_path.join(relative)
240    }
241
242    /// `<base>/config`.
243    pub fn config_path(&self) -> PathBuf {
244        self.path("config")
245    }
246
247    /// `<base>/storage`.
248    pub fn storage_path(&self) -> PathBuf {
249        self.path("storage")
250    }
251
252    /// `<base>/resources` — where views live.
253    pub fn resource_path(&self) -> PathBuf {
254        self.path("resources")
255    }
256
257    /// `<base>/database`.
258    pub fn database_path(&self) -> PathBuf {
259        self.path("database")
260    }
261}
262
263impl std::ops::Deref for Application {
264    type Target = Container;
265
266    fn deref(&self) -> &Self::Target {
267        &self.container
268    }
269}
270
271impl std::fmt::Debug for Application {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        f.debug_struct("Application")
274            .field("environment", &self.environment())
275            .field("base_path", &self.base_path)
276            .field("booted", &self.is_booted())
277            .field("providers", &self.provider_names().len())
278            .finish()
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use rainier_support::BoxFuture;
286    use std::sync::atomic::AtomicUsize;
287
288    /// The providers below record their lifecycle steps into one process-wide
289    /// log, so the tests that assert on ordering must not interleave. Each one
290    /// takes this lock for its duration and starts from an empty log.
291    static SERIAL: Mutex<()> = Mutex::new(());
292    static ORDER: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
293
294    struct Recording(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
295
296    impl Recording {
297        fn start() -> Self {
298            let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
299            ORDER.lock().unwrap().clear();
300            Self(guard)
301        }
302
303        fn steps(&self) -> Vec<&'static str> {
304            ORDER.lock().unwrap().clone()
305        }
306    }
307
308    fn record(step: &'static str) {
309        ORDER.lock().unwrap().push(step);
310    }
311
312    struct First;
313    struct Second;
314    struct Marker(u32);
315
316    impl ServiceProvider for First {
317        fn register(&self, app: &Application) -> Result<()> {
318            record("register:first");
319            app.instance(Marker(1));
320            Ok(())
321        }
322        fn boot<'a>(&'a self, _app: &'a Application) -> BoxFuture<'a, Result<()>> {
323            Box::pin(async {
324                record("boot:first");
325                Ok(())
326            })
327        }
328    }
329
330    impl ServiceProvider for Second {
331        fn register(&self, _app: &Application) -> Result<()> {
332            record("register:second");
333            Ok(())
334        }
335        fn boot<'a>(&'a self, app: &'a Application) -> BoxFuture<'a, Result<()>> {
336            Box::pin(async move {
337                record("boot:second");
338                // Legal here and nowhere earlier: every provider has registered.
339                assert_eq!(app.resolve::<Marker>()?.0, 1);
340                Ok(())
341            })
342        }
343    }
344
345    #[tokio::test]
346    async fn every_provider_registers_before_any_boots() {
347        let log = Recording::start();
348        let app = Application::new(".");
349        app.register(First).unwrap();
350        app.register(Second).unwrap();
351        app.boot().await.unwrap();
352
353        assert_eq!(
354            log.steps(),
355            vec!["register:first", "register:second", "boot:first", "boot:second"]
356        );
357    }
358
359    #[tokio::test]
360    async fn boot_is_idempotent() {
361        let log = Recording::start();
362        let app = Application::new(".");
363        app.register(First).unwrap();
364        app.boot().await.unwrap();
365        app.boot().await.unwrap();
366
367        assert_eq!(log.steps().iter().filter(|s| **s == "boot:first").count(), 1);
368    }
369
370    #[tokio::test]
371    async fn providers_registered_after_boot_are_booted_by_the_next_boot() {
372        let log = Recording::start();
373        let app = Application::new(".");
374        app.boot().await.unwrap();
375        app.register(First).unwrap();
376        app.boot().await.unwrap();
377
378        assert!(log.steps().contains(&"boot:first"));
379    }
380
381    #[tokio::test]
382    async fn hooks_run_around_booting() {
383        let app = Application::new(".");
384        let seen = Arc::new(Mutex::new(Vec::new()));
385
386        let s = Arc::clone(&seen);
387        app.booting(move |_| s.lock().unwrap().push("booting"));
388        let s = Arc::clone(&seen);
389        app.booted(move |_| s.lock().unwrap().push("booted"));
390        let s = Arc::clone(&seen);
391        app.terminating(move |_| s.lock().unwrap().push("terminating"));
392
393        app.boot().await.unwrap();
394        assert_eq!(*seen.lock().unwrap(), vec!["booting", "booted"]);
395
396        app.terminate();
397        assert_eq!(*seen.lock().unwrap(), vec!["booting", "booted", "terminating"]);
398    }
399
400    #[tokio::test]
401    async fn a_booted_hook_added_late_fires_immediately() {
402        let app = Application::new(".");
403        app.boot().await.unwrap();
404
405        let fired = Arc::new(AtomicUsize::new(0));
406        let f = Arc::clone(&fired);
407        app.booted(move |_| {
408            f.fetch_add(1, Ordering::SeqCst);
409        });
410        assert_eq!(fired.load(Ordering::SeqCst), 1);
411    }
412
413    #[tokio::test]
414    async fn a_failing_provider_names_itself() {
415        struct Broken;
416        impl ServiceProvider for Broken {
417            fn name(&self) -> &'static str {
418                "BrokenProvider"
419            }
420            fn boot<'a>(&'a self, _: &'a Application) -> BoxFuture<'a, Result<()>> {
421                Box::pin(async { Err(rainier_support::Error::internal("no socket")) })
422            }
423        }
424
425        let app = Application::new(".");
426        app.register(Broken).unwrap();
427        let err = app.boot().await.unwrap_err();
428        assert!(err.message().contains("BrokenProvider"), "{}", err.message());
429        assert!(err.message().contains("no socket"), "{}", err.message());
430    }
431
432    #[test]
433    fn the_application_derefs_to_its_container() {
434        let app = Application::new(".");
435        app.instance(Marker(9));
436        assert_eq!(app.resolve::<Marker>().unwrap().0, 9);
437    }
438
439    #[test]
440    fn environment_predicates() {
441        let app = Application::new(".").with_environment("local");
442        assert!(app.is_local());
443        assert!(!app.is_production());
444        app.set_environment("testing");
445        assert!(app.is_testing());
446    }
447
448    #[test]
449    fn paths_hang_off_the_base_path() {
450        let app = Application::new("/srv/app");
451        assert_eq!(app.config_path(), PathBuf::from("/srv/app/config"));
452        assert_eq!(app.path("routes/web.rs"), PathBuf::from("/srv/app/routes/web.rs"));
453    }
454}