Skip to main content

rainier_container/
container.rs

1//! The IoC [`Container`] — Rainier's service locator and the thing every
2//! component registers itself into.
3//!
4//! Laravel resolves services by class name using PHP's reflection. Rust has no
5//! reflection, so the key here is [`TypeId`] and the "constructor injection" is
6//! an explicit factory closure that receives the container and pulls out what
7//! it needs:
8//!
9//! ```
10//! # use rainier_container::Container;
11//! # use rainier_support::Result;
12//! struct Config { url: String }
13//! struct Client { url: String }
14//!
15//! let c = Container::new();
16//! c.instance(Config { url: "postgres://…".into() });
17//! c.singleton(|c: &Container| {
18//!     let config = c.resolve::<Config>()?;
19//!     Ok(Client { url: config.url.clone() })
20//! });
21//!
22//! let client = c.resolve::<Client>().unwrap();
23//! assert_eq!(client.url, "postgres://…");
24//! ```
25//!
26//! Three binding lifetimes, matching Laravel's:
27//!
28//! | Method | Lifetime |
29//! |---|---|
30//! | [`bind`](Container::bind) | a fresh value per resolution |
31//! | [`singleton`](Container::singleton) | built once, shared forever |
32//! | [`scoped`](Container::scoped) | built once per scope, dropped by [`flush_scoped`](Container::flush_scoped) |
33//! | [`instance`](Container::instance) | supplied ready-made |
34
35use std::any::{type_name, Any, TypeId};
36use std::cell::RefCell;
37use std::collections::HashMap;
38use std::sync::{Arc, Mutex, RwLock};
39
40use rainier_support::{Error, Result};
41
42/// A type-erased service instance.
43type Shared = Arc<dyn Any + Send + Sync>;
44
45/// A factory that builds a type-erased service from the container.
46type Factory = Arc<dyn Fn(&Container) -> Result<Shared> + Send + Sync>;
47
48/// How a bound type is produced, and how long the result lives.
49#[derive(Clone)]
50enum Binding {
51    /// Rebuilt on every resolution.
52    Transient(Factory),
53    /// Built at most once; the instance is memoised in the `Mutex`.
54    Shared { factory: Factory, instance: Arc<Mutex<Option<Shared>>>, scoped: bool },
55}
56
57/// A registry of services keyed by their Rust type.
58///
59/// Cheap to share: put it in an [`Arc`] and clone the handle. All methods take
60/// `&self` — bindings are registered through interior mutability, because
61/// service providers register into a container that is already shared.
62#[derive(Default)]
63pub struct Container {
64    bindings: RwLock<HashMap<TypeId, Binding>>,
65}
66
67thread_local! {
68    /// The chain of types currently being resolved on this thread, so a
69    /// dependency cycle is reported as an error instead of deadlocking on the
70    /// memoisation mutex.
71    static RESOLVING: RefCell<Vec<(TypeId, &'static str)>> = const { RefCell::new(Vec::new()) };
72}
73
74impl Container {
75    /// An empty container.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Bind a factory that runs on **every** resolution.
81    pub fn bind<T, F>(&self, factory: F)
82    where
83        T: Send + Sync + 'static,
84        F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
85    {
86        let factory: Factory = Arc::new(move |c| Ok(Arc::new(factory(c)?) as Shared));
87        self.insert::<T>(Binding::Transient(factory));
88    }
89
90    /// Bind a factory that runs **once**; every resolution afterwards hands
91    /// back the same [`Arc`].
92    pub fn singleton<T, F>(&self, factory: F)
93    where
94        T: Send + Sync + 'static,
95        F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
96    {
97        self.shared::<T, F>(factory, false);
98    }
99
100    /// Like [`singleton`](Self::singleton), but [`flush_scoped`] drops the
101    /// memoised instance so the next resolution rebuilds it.
102    ///
103    /// This is the binding a long-running worker wants for per-job state: the
104    /// queue worker flushes scoped bindings between jobs so one job cannot leak
105    /// state into the next.
106    ///
107    /// [`flush_scoped`]: Self::flush_scoped
108    pub fn scoped<T, F>(&self, factory: F)
109    where
110        T: Send + Sync + 'static,
111        F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
112    {
113        self.shared::<T, F>(factory, true);
114    }
115
116    fn shared<T, F>(&self, factory: F, scoped: bool)
117    where
118        T: Send + Sync + 'static,
119        F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
120    {
121        let factory: Factory = Arc::new(move |c| Ok(Arc::new(factory(c)?) as Shared));
122        self.insert::<T>(Binding::Shared { factory, instance: Arc::new(Mutex::new(None)), scoped });
123    }
124
125    /// Register an already-built value. The container never constructs it.
126    pub fn instance<T: Send + Sync + 'static>(&self, value: T) {
127        self.instance_arc(Arc::new(value));
128    }
129
130    /// Register an already-built value that is already behind an [`Arc`] —
131    /// useful when the caller must keep a handle to it too.
132    pub fn instance_arc<T: Send + Sync + 'static>(&self, value: Arc<T>) {
133        let instance: Shared = value;
134        self.insert::<T>(Binding::Shared {
135            // Never called: the instance slot is pre-filled.
136            factory: Arc::new(|_| Err(Error::internal("instance binding has no factory"))),
137            instance: Arc::new(Mutex::new(Some(instance))),
138            scoped: false,
139        });
140    }
141
142    fn insert<T: 'static>(&self, binding: Binding) {
143        let mut bindings = self.bindings.write().expect("container lock poisoned");
144        bindings.insert(TypeId::of::<T>(), binding);
145    }
146
147    /// Resolve `T`, building it if necessary.
148    ///
149    /// Fails when `T` was never bound, when its factory fails, or when
150    /// resolving it re-enters itself (a dependency cycle).
151    pub fn resolve<T: Send + Sync + 'static>(&self) -> Result<Arc<T>> {
152        let binding = {
153            let bindings = self.bindings.read().expect("container lock poisoned");
154            bindings.get(&TypeId::of::<T>()).cloned()
155        };
156
157        let Some(binding) = binding else {
158            return Err(Error::internal(format!(
159                "nothing is bound for `{}` — register it in a service provider before resolving",
160                type_name::<T>()
161            )));
162        };
163
164        let value = self.build::<T>(binding)?;
165        value.downcast::<T>().map_err(|_| {
166            // Unreachable: bindings are keyed by the TypeId of the value the
167            // factory produces. Reported rather than unwrapped so a future
168            // untyped registration path cannot turn into a panic.
169            Error::internal(format!("binding for `{}` produced the wrong type", type_name::<T>()))
170        })
171    }
172
173    fn build<T: Send + Sync + 'static>(&self, binding: Binding) -> Result<Shared> {
174        // The cycle guard wraps *everything*, including the "already built?"
175        // check. It has to: that check takes the memoisation mutex, and
176        // `std::sync::Mutex` is not reentrant, so a factory for `T` that
177        // re-resolves `T` would deadlock on its own lock before any cycle could
178        // be detected. Checking the guard first turns that into an error.
179        let _guard = ResolutionGuard::enter::<T>()?;
180
181        match binding {
182            Binding::Transient(factory) => factory(self),
183            Binding::Shared { factory, instance, .. } => {
184                let mut slot = instance.lock().expect("container lock poisoned");
185                if let Some(existing) = slot.clone() {
186                    return Ok(existing);
187                }
188                let built = factory(self)?;
189                *slot = Some(built.clone());
190                Ok(built)
191            }
192        }
193    }
194
195    /// Resolve `T`, or `None` if it is unbound or its factory failed.
196    pub fn try_resolve<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
197        self.resolve::<T>().ok()
198    }
199
200    /// Resolve `T`, panicking with the resolution error if it is not
201    /// available. For bootstrap code and facades, where an unbound service is
202    /// a configuration bug rather than a runtime condition.
203    pub fn expect_resolve<T: Send + Sync + 'static>(&self) -> Arc<T> {
204        match self.resolve::<T>() {
205            Ok(value) => value,
206            Err(e) => panic!("{e}"),
207        }
208    }
209
210    /// Whether anything is bound for `T`.
211    pub fn bound<T: 'static>(&self) -> bool {
212        self.bindings.read().expect("container lock poisoned").contains_key(&TypeId::of::<T>())
213    }
214
215    /// Drop the binding for `T`, if any.
216    pub fn forget<T: 'static>(&self) {
217        self.bindings.write().expect("container lock poisoned").remove(&TypeId::of::<T>());
218    }
219
220    /// Drop the memoised instance of every [`scoped`](Self::scoped) binding.
221    /// The bindings survive; only the built values are discarded.
222    pub fn flush_scoped(&self) {
223        let bindings = self.bindings.read().expect("container lock poisoned");
224        for binding in bindings.values() {
225            if let Binding::Shared { instance, scoped: true, .. } = binding {
226                *instance.lock().expect("container lock poisoned") = None;
227            }
228        }
229    }
230
231    /// How many types are bound.
232    pub fn len(&self) -> usize {
233        self.bindings.read().expect("container lock poisoned").len()
234    }
235
236    /// Whether nothing is bound.
237    pub fn is_empty(&self) -> bool {
238        self.len() == 0
239    }
240}
241
242impl std::fmt::Debug for Container {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.debug_struct("Container").field("bindings", &self.len()).finish()
245    }
246}
247
248/// Marks a type as "being resolved on this thread" for as long as it lives.
249///
250/// Detects same-thread dependency cycles. A cycle spread across two threads
251/// (thread A builds `T` which needs `U`, thread B builds `U` which needs `T`)
252/// still blocks on the memoisation mutexes — that is a genuine deadlock in the
253/// application's dependency graph, and no per-thread bookkeeping can see it.
254struct ResolutionGuard;
255
256impl ResolutionGuard {
257    fn enter<T: 'static>() -> Result<Self> {
258        let id = TypeId::of::<T>();
259        let name = type_name::<T>();
260
261        RESOLVING.with(|stack| {
262            let mut stack = stack.borrow_mut();
263            if stack.iter().any(|(seen, _)| *seen == id) {
264                let mut chain: Vec<&str> = stack.iter().map(|(_, n)| *n).collect();
265                chain.push(name);
266                return Err(Error::internal(format!(
267                    "circular dependency while resolving: {}",
268                    chain.join(" -> ")
269                )));
270            }
271            stack.push((id, name));
272            Ok(())
273        })?;
274
275        Ok(Self)
276    }
277}
278
279impl Drop for ResolutionGuard {
280    fn drop(&mut self) {
281        // Popping on drop rather than after the call keeps the stack correct
282        // when a factory panics and unwinds; otherwise a stale entry would make
283        // every later resolution of that type look like a cycle.
284        RESOLVING.with(|stack| {
285            stack.borrow_mut().pop();
286        });
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use std::sync::atomic::{AtomicUsize, Ordering};
294
295    #[derive(Debug)]
296    struct Counter(usize);
297    #[derive(Debug)]
298    struct Dependent(usize);
299
300    #[test]
301    fn instance_bindings_come_back_as_given() {
302        let c = Container::new();
303        c.instance(Counter(42));
304        assert_eq!(c.resolve::<Counter>().unwrap().0, 42);
305    }
306
307    #[test]
308    fn unbound_types_report_their_name() {
309        let c = Container::new();
310        let err = c.resolve::<Counter>().unwrap_err();
311        assert!(err.message().contains("Counter"), "{}", err.message());
312    }
313
314    #[test]
315    fn bind_rebuilds_every_time() {
316        static BUILDS: AtomicUsize = AtomicUsize::new(0);
317        let c = Container::new();
318        c.bind(|_| Ok(Counter(BUILDS.fetch_add(1, Ordering::SeqCst))));
319
320        assert_eq!(c.resolve::<Counter>().unwrap().0, 0);
321        assert_eq!(c.resolve::<Counter>().unwrap().0, 1);
322        assert_eq!(BUILDS.load(Ordering::SeqCst), 2);
323    }
324
325    #[test]
326    fn singleton_builds_once_and_shares() {
327        static BUILDS: AtomicUsize = AtomicUsize::new(0);
328        let c = Container::new();
329        c.singleton(|_| {
330            BUILDS.fetch_add(1, Ordering::SeqCst);
331            Ok(Counter(7))
332        });
333
334        let a = c.resolve::<Counter>().unwrap();
335        let b = c.resolve::<Counter>().unwrap();
336        assert!(Arc::ptr_eq(&a, &b));
337        assert_eq!(BUILDS.load(Ordering::SeqCst), 1);
338    }
339
340    #[test]
341    fn factories_can_resolve_their_dependencies() {
342        let c = Container::new();
343        c.instance(Counter(5));
344        c.singleton(|c: &Container| Ok(Dependent(c.resolve::<Counter>()?.0 * 2)));
345        assert_eq!(c.resolve::<Dependent>().unwrap().0, 10);
346    }
347
348    #[test]
349    fn a_failing_factory_surfaces_its_error() {
350        let c = Container::new();
351        c.singleton(|_| Err::<Counter, _>(Error::internal("no database")));
352        assert_eq!(c.resolve::<Counter>().unwrap_err().message(), "no database");
353    }
354
355    #[test]
356    fn dependency_cycles_error_instead_of_deadlocking() {
357        let c = Container::new();
358        // Counter needs Dependent, Dependent needs Counter.
359        c.singleton(|c: &Container| Ok(Counter(c.resolve::<Dependent>()?.0)));
360        c.singleton(|c: &Container| Ok(Dependent(c.resolve::<Counter>()?.0)));
361
362        let err = c.resolve::<Counter>().unwrap_err();
363        assert!(err.message().contains("circular dependency"), "{}", err.message());
364    }
365
366    #[test]
367    fn a_singleton_that_resolves_itself_errors_rather_than_self_deadlocking() {
368        // Regression guard: the memoisation mutex is not reentrant, so this
369        // must be caught by the cycle guard *before* the lock is taken.
370        let c = Container::new();
371        c.singleton(|c: &Container| Ok(Counter(c.resolve::<Counter>()?.0)));
372
373        let err = c.resolve::<Counter>().unwrap_err();
374        assert!(err.message().contains("circular dependency"), "{}", err.message());
375    }
376
377    #[test]
378    fn a_panicking_factory_does_not_poison_later_resolutions() {
379        let c = Arc::new(Container::new());
380        c.bind(|_| -> Result<Counter> { panic!("boom") });
381
382        let panicked = {
383            let c = Arc::clone(&c);
384            std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
385                let _ = c.resolve::<Counter>();
386            }))
387        };
388        assert!(panicked.is_err());
389
390        // The resolution stack must have unwound with the panic; if it had not,
391        // this second attempt would be misreported as a cycle.
392        c.forget::<Counter>();
393        c.instance(Counter(1));
394        assert_eq!(c.resolve::<Counter>().unwrap().0, 1);
395    }
396
397    #[test]
398    fn scoped_bindings_rebuild_after_a_flush() {
399        static BUILDS: AtomicUsize = AtomicUsize::new(0);
400        let c = Container::new();
401        c.scoped(|_| Ok(Counter(BUILDS.fetch_add(1, Ordering::SeqCst))));
402
403        assert_eq!(c.resolve::<Counter>().unwrap().0, 0);
404        assert_eq!(c.resolve::<Counter>().unwrap().0, 0);
405        c.flush_scoped();
406        assert_eq!(c.resolve::<Counter>().unwrap().0, 1);
407    }
408
409    #[test]
410    fn flush_scoped_leaves_singletons_alone() {
411        static BUILDS: AtomicUsize = AtomicUsize::new(0);
412        let c = Container::new();
413        c.singleton(|_| Ok(Counter(BUILDS.fetch_add(1, Ordering::SeqCst))));
414
415        assert_eq!(c.resolve::<Counter>().unwrap().0, 0);
416        c.flush_scoped();
417        assert_eq!(c.resolve::<Counter>().unwrap().0, 0);
418    }
419
420    #[test]
421    fn bound_and_forget_track_the_registry() {
422        let c = Container::new();
423        assert!(!c.bound::<Counter>());
424        c.instance(Counter(1));
425        assert!(c.bound::<Counter>());
426        c.forget::<Counter>();
427        assert!(!c.bound::<Counter>());
428    }
429
430    #[test]
431    fn resolution_is_safe_across_threads() {
432        let c = Arc::new(Container::new());
433        c.singleton(|_| Ok(Counter(99)));
434
435        let handles: Vec<_> = (0..8)
436            .map(|_| {
437                let c = Arc::clone(&c);
438                std::thread::spawn(move || c.resolve::<Counter>().unwrap())
439            })
440            .collect();
441
442        let first = c.resolve::<Counter>().unwrap();
443        for handle in handles {
444            assert!(Arc::ptr_eq(&first, &handle.join().unwrap()));
445        }
446    }
447}