use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::OnceCell;
use crate::{Container, Injectable, Result};
type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub struct AsyncLazy<T> {
cell: OnceCell<Arc<T>>,
factory: Box<dyn Fn() -> BoxFuture<Arc<T>> + Send + Sync>,
}
impl Container {
#[inline]
pub fn lazy_async<T, F, Fut>(&self, factory: F)
where
T: Injectable,
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
#[cfg(feature = "logging")]
crate::debug!(
target: "dependency_injector",
service = std::any::type_name::<T>(),
lifetime = "async_lazy_singleton",
depth = self.depth(),
"Registering async lazy singleton service (factory awaited on first get_async)"
);
self.singleton(AsyncLazy::<T> {
cell: OnceCell::new(),
factory: Box::new(move || -> BoxFuture<Arc<T>> {
let fut = factory();
Box::pin(async move { Arc::new(fut.await) })
}),
});
}
pub async fn get_async<T: Injectable>(&self) -> Result<Arc<T>> {
if let Some(lazy) = self.try_get::<AsyncLazy<T>>() {
let value = lazy.cell.get_or_init(|| (lazy.factory)()).await;
return Ok(Arc::clone(value));
}
self.get::<T>()
}
pub async fn try_get_async<T: Injectable>(&self) -> Option<Arc<T>> {
self.get_async::<T>().await.ok()
}
}
#[cfg(all(test, feature = "async"))]
mod tests {
use super::*;
use crate::DiError;
use std::any::TypeId;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Clone, Debug)]
struct AsyncService {
value: u32,
}
#[derive(Clone)]
struct SyncService {
name: String,
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn lazy_async_initializes_exactly_once_under_concurrency() {
static CREATED: AtomicU32 = AtomicU32::new(0);
let container = Container::new();
container.lazy_async(|| async {
CREATED.fetch_add(1, Ordering::SeqCst);
tokio::task::yield_now().await;
AsyncService { value: 42 }
});
assert_eq!(CREATED.load(Ordering::SeqCst), 0, "factory must be lazy");
let mut handles = Vec::new();
for _ in 0..16 {
let container = container.clone();
handles.push(tokio::spawn(async move {
container.get_async::<AsyncService>().await.unwrap()
}));
}
let mut resolved = Vec::new();
for handle in handles {
resolved.push(handle.await.unwrap());
}
assert_eq!(CREATED.load(Ordering::SeqCst), 1);
for service in &resolved {
assert_eq!(service.value, 42);
assert!(Arc::ptr_eq(service, &resolved[0]));
}
}
#[tokio::test]
async fn get_async_returns_cached_instance_on_subsequent_resolves() {
static CREATED: AtomicU32 = AtomicU32::new(0);
let container = Container::new();
container.lazy_async(|| async {
CREATED.fetch_add(1, Ordering::SeqCst);
AsyncService { value: 7 }
});
let first = container.get_async::<AsyncService>().await.unwrap();
let second = container.get_async::<AsyncService>().await.unwrap();
assert_eq!(CREATED.load(Ordering::SeqCst), 1);
assert!(Arc::ptr_eq(&first, &second));
}
#[tokio::test]
async fn get_async_falls_back_to_sync_singletons() {
let container = Container::new();
container.singleton(SyncService {
name: "sync".into(),
});
let from_async_path = container.get_async::<SyncService>().await.unwrap();
let from_sync_path = container.get::<SyncService>().unwrap();
assert_eq!(from_async_path.name, "sync");
assert!(Arc::ptr_eq(&from_async_path, &from_sync_path));
}
#[tokio::test]
async fn async_registration_wins_over_sync_singleton() {
let container = Container::new();
container.singleton(AsyncService { value: 1 });
container.lazy_async(|| async { AsyncService { value: 2 } });
let resolved = container.get_async::<AsyncService>().await.unwrap();
assert_eq!(
resolved.value, 2,
"get_async must resolve the async registration when both exist"
);
let sync = container.get::<AsyncService>().unwrap();
assert_eq!(sync.value, 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn cancelled_initialization_restarts_factory_and_completes_once() {
use std::time::Duration;
use tokio::sync::Notify;
use tokio::time::timeout;
#[derive(Clone, Debug)]
struct GatedService {
value: u32,
}
static ENTERED: AtomicU32 = AtomicU32::new(0);
static COMPLETED: AtomicU32 = AtomicU32::new(0);
let gate = Arc::new(Notify::new());
let factory_entered = Arc::new(Notify::new());
let container = Container::new();
{
let gate = Arc::clone(&gate);
let factory_entered = Arc::clone(&factory_entered);
container.lazy_async(move || {
let gate = Arc::clone(&gate);
let factory_entered = Arc::clone(&factory_entered);
async move {
ENTERED.fetch_add(1, Ordering::SeqCst);
factory_entered.notify_one();
gate.notified().await;
COMPLETED.fetch_add(1, Ordering::SeqCst);
GatedService { value: 42 }
}
});
}
let task_a = {
let container = container.clone();
tokio::spawn(async move { container.get_async::<GatedService>().await })
};
timeout(Duration::from_secs(5), factory_entered.notified())
.await
.expect("factory should enter before the failsafe timeout");
assert_eq!(ENTERED.load(Ordering::SeqCst), 1);
task_a.abort();
let join_err = task_a.await.expect_err("aborted task must not complete");
assert!(join_err.is_cancelled());
assert_eq!(COMPLETED.load(Ordering::SeqCst), 0);
gate.notify_one();
let resolved = timeout(
Duration::from_secs(5),
container.get_async::<GatedService>(),
)
.await
.expect("restarted initialization should finish before the failsafe timeout")
.unwrap();
assert_eq!(resolved.value, 42);
assert_eq!(
ENTERED.load(Ordering::SeqCst),
2,
"factory should restart after the first run was cancelled"
);
assert_eq!(
COMPLETED.load(Ordering::SeqCst),
1,
"initialization should complete exactly once"
);
let again = timeout(
Duration::from_secs(5),
container.get_async::<GatedService>(),
)
.await
.expect("a cached resolve must not re-enter the factory")
.unwrap();
assert!(
Arc::ptr_eq(&resolved, &again),
"the cached value must be shared by every later resolve"
);
assert_eq!(
ENTERED.load(Ordering::SeqCst),
2,
"a cached resolve must not run the factory again"
);
}
#[tokio::test]
async fn get_async_not_found_matches_get_error_shape() {
let container = Container::new();
let err = container.get_async::<AsyncService>().await.unwrap_err();
match err {
DiError::NotFound { type_name, type_id } => {
assert_eq!(type_name, std::any::type_name::<AsyncService>());
assert_eq!(type_id, TypeId::of::<AsyncService>());
}
other => panic!("expected NotFound, got: {other:?}"),
}
}
#[tokio::test]
async fn try_get_async_none_when_missing_some_when_registered() {
let container = Container::new();
assert!(container.try_get_async::<AsyncService>().await.is_none());
container.lazy_async(|| async { AsyncService { value: 7 } });
let service = container.try_get_async::<AsyncService>().await.unwrap();
assert_eq!(service.value, 7);
}
#[tokio::test]
async fn child_scope_resolves_parent_async_lazy() {
let root = Container::new();
root.lazy_async(|| async { AsyncService { value: 1 } });
let child = root.scope();
let from_child = child.get_async::<AsyncService>().await.unwrap();
let from_root = root.get_async::<AsyncService>().await.unwrap();
assert!(Arc::ptr_eq(&from_child, &from_root));
}
#[tokio::test]
async fn async_registration_is_keyed_as_async_lazy_wrapper() {
let container = Container::new();
assert!(!container.contains::<AsyncLazy<AsyncService>>());
container.lazy_async(|| async { AsyncService { value: 3 } });
assert!(container.contains::<AsyncLazy<AsyncService>>());
assert!(!container.contains::<AsyncService>());
assert!(container.remove::<AsyncLazy<AsyncService>>());
assert!(container.get_async::<AsyncService>().await.is_err());
}
}