actix_web_lab/
lazy_data_shared.rs1use 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
12pub 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 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 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}