actix_web_lab/
lazy_data_shared.rs

1use std::{
2    fmt,
3    future::{Future, Ready, ready},
4    sync::Arc,
5};
6
7use actix_web::{Error, FromRequest, HttpRequest, dev, error};
8use futures_core::future::BoxFuture;
9use tokio::sync::{Mutex, OnceCell};
10use tracing::debug;
11
12/// A lazy extractor for globally shared data.
13///
14/// Unlike, [`LazyData`], this type implements [`Send`] and [`Sync`].
15///
16/// Using `SharedLazyData` as an extractor will not initialize the data; [`get`](Self::get) must be
17/// used.
18///
19/// [`LazyData`]: crate::extract::LazyData
20pub struct LazyDataShared<T> {
21    inner: Arc<LazyDataSharedInner<T>>,
22}
23
24struct LazyDataSharedInner<T> {
25    cell: OnceCell<T>,
26    fut: Mutex<Option<BoxFuture<'static, T>>>,
27}
28
29impl<T> Clone for LazyDataShared<T> {
30    fn clone(&self) -> Self {
31        Self {
32            inner: self.inner.clone(),
33        }
34    }
35}
36
37impl<T: fmt::Debug> fmt::Debug for LazyDataShared<T> {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        let Self { inner } = self;
40        let LazyDataSharedInner { cell, fut: _ } = &**inner;
41
42        f.debug_struct("SharedLazyData")
43            .field("cell", &cell)
44            .field("fut", &"..")
45            .finish()
46    }
47}
48
49impl<T> LazyDataShared<T> {
50    /// Constructs a new `LazyData` extractor with the given initialization function.
51    ///
52    /// Initialization functions must return a future that resolves to `T`.
53    pub fn new<F, Fut>(init: F) -> LazyDataShared<T>
54    where
55        F: FnOnce() -> Fut,
56        Fut: Future<Output = T> + Send + 'static,
57    {
58        Self {
59            inner: Arc::new(LazyDataSharedInner {
60                cell: OnceCell::new(),
61                fut: Mutex::new(Some(Box::pin(init()))),
62            }),
63        }
64    }
65
66    /// Returns reference to result of lazy `T` value, initializing if necessary.
67    pub async fn get(&self) -> &T {
68        self.inner
69            .cell
70            .get_or_init(|| async move {
71                match &mut *self.inner.fut.lock().await {
72                    Some(fut) => fut.await,
73                    None => panic!("LazyData instance has previously been poisoned"),
74                }
75            })
76            .await
77    }
78}
79
80impl<T: 'static> FromRequest for LazyDataShared<T> {
81    type Error = Error;
82    type Future = Ready<Result<Self, Error>>;
83
84    #[inline]
85    fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future {
86        if let Some(lazy) = req.app_data::<LazyDataShared<T>>() {
87            ready(Ok(lazy.clone()))
88        } else {
89            debug!(
90                "Failed to extract `SharedLazyData<{}>` for `{}` handler. For the Data extractor to \
91                work correctly, wrap the data with `SharedLazyData::new()` and pass it to \
92                `App::app_data()`. Ensure that types align in both the set and retrieve calls.",
93                core::any::type_name::<T>(),
94                req.match_name().unwrap_or_else(|| req.path())
95            );
96
97            ready(Err(error::ErrorInternalServerError(
98                "Requested application data is not configured correctly. \
99                View/enable debug logs for more details.",
100            )))
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use std::time::Duration;
108
109    use actix_web::{
110        App, HttpResponse,
111        http::StatusCode,
112        test::{TestRequest, call_service, init_service},
113        web,
114    };
115
116    use super::*;
117
118    static_assertions::assert_impl_all!(LazyDataShared<()>: Send, Sync);
119
120    #[actix_web::test]
121    async fn lazy_data() {
122        let app = init_service(
123            App::new()
124                .app_data(LazyDataShared::new(|| async { 10usize }))
125                .service(web::resource("/").to(|_: LazyDataShared<usize>| HttpResponse::Ok())),
126        )
127        .await;
128        let req = TestRequest::default().to_request();
129        let resp = call_service(&app, req).await;
130        assert_eq!(resp.status(), StatusCode::OK);
131
132        let app = init_service(
133            App::new()
134                .app_data(LazyDataShared::new(|| async {
135                    actix_web::rt::time::sleep(Duration::from_millis(40)).await;
136                    10_usize
137                }))
138                .service(web::resource("/").to(
139                    #[expect(clippy::async_yields_async)]
140                    |lazy_num: LazyDataShared<usize>| async move {
141                        if *lazy_num.get().await == 10 {
142                            HttpResponse::Ok()
143                        } else {
144                            HttpResponse::InternalServerError()
145                        }
146                    },
147                )),
148        )
149        .await;
150        let req = TestRequest::default().to_request();
151        let resp = call_service(&app, req).await;
152        assert_eq!(StatusCode::OK, resp.status());
153
154        let app = init_service(
155            App::new()
156                .app_data(LazyDataShared::new(|| async { 10u32 }))
157                .service(web::resource("/").to(|_: LazyDataShared<usize>| HttpResponse::Ok())),
158        )
159        .await;
160        let req = TestRequest::default().to_request();
161        let resp = call_service(&app, req).await;
162        assert_eq!(StatusCode::INTERNAL_SERVER_ERROR, resp.status());
163    }
164
165    #[actix_web::test]
166    async fn lazy_data_web_block() {
167        let app = init_service(
168            App::new()
169                .app_data(LazyDataShared::new(|| async {
170                    web::block(|| std::thread::sleep(Duration::from_millis(40)))
171                        .await
172                        .unwrap();
173
174                    10usize
175                }))
176                .service(web::resource("/").to(|_: LazyDataShared<usize>| HttpResponse::Ok())),
177        )
178        .await;
179        let req = TestRequest::default().to_request();
180        let resp = call_service(&app, req).await;
181        assert_eq!(StatusCode::OK, resp.status());
182    }
183}