Skip to main content

versatile_dataloader/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(clippy::pedantic)]
3#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
4
5mod cache;
6
7#[cfg(not(feature = "boxed-trait"))]
8use std::future::Future;
9use std::{
10    any::{Any, TypeId},
11    borrow::Cow,
12    collections::{HashMap, HashSet},
13    hash::Hash,
14    sync::{
15        Arc, Mutex,
16        atomic::{AtomicBool, Ordering},
17    },
18    time::Duration,
19};
20
21pub use cache::{CacheFactory, CacheStorage, HashMapCache, LruCache, NoCache};
22use fnv::FnvHashMap;
23use futures_channel::oneshot;
24use futures_timer::Delay;
25use futures_util::future::BoxFuture;
26#[cfg(feature = "tracing")]
27use tracing::{Instrument, info_span, instrument};
28#[cfg(feature = "tracing")]
29use tracinglib as tracing;
30
31#[allow(clippy::type_complexity)]
32struct ResSender<
33    K: Send + Sync + Hash + Eq + Clone + 'static,
34    V: Send + Sync + Clone + 'static,
35    T: Loader<K, V>,
36> {
37    use_cache_values: HashMap<K, V>,
38    tx: oneshot::Sender<Result<HashMap<K, V>, T::Error>>,
39}
40
41struct Requests<
42    K: Send + Sync + Hash + Eq + Clone + 'static,
43    V: Send + Sync + Clone + 'static,
44    T: Loader<K, V>,
45> {
46    keys: HashSet<K>,
47    pending: Vec<(HashSet<K>, ResSender<K, V, T>)>,
48    cache_storage: Box<dyn CacheStorage<Key = K, Value = V>>,
49    disable_cache: bool,
50}
51
52type KeysAndSender<K, V, T> = (HashSet<K>, Vec<(HashSet<K>, ResSender<K, V, T>)>);
53
54impl<
55    K: Send + Sync + Hash + Eq + Clone + 'static,
56    V: Send + Sync + Clone + 'static,
57    T: Loader<K, V>,
58> Requests<K, V, T>
59{
60    fn new<C: CacheFactory>(cache_factory: &C) -> Self {
61        Self {
62            keys: HashSet::default(),
63            pending: Vec::new(),
64            cache_storage: cache_factory.create::<K, V>(),
65            disable_cache: false,
66        }
67    }
68
69    fn take(&mut self) -> KeysAndSender<K, V, T> {
70        (
71            std::mem::take(&mut self.keys),
72            std::mem::take(&mut self.pending),
73        )
74    }
75}
76
77/// Trait for batch loading.
78#[cfg_attr(feature = "boxed-trait", async_trait::async_trait)]
79pub trait Loader<K: Send + Sync + Hash + Eq + Clone + 'static, V: Send + Sync + Clone + 'static>:
80    Send + Sync + 'static
81{
82    /// Type of error.
83    type Error: Send + Clone + 'static;
84
85    /// Load the data set specified by the `keys`.
86    #[cfg(feature = "boxed-trait")]
87    async fn load(&self, keys: &[K]) -> Result<HashMap<K, V>, Self::Error>;
88
89    /// Load the data set specified by the `keys`.
90    #[cfg(not(feature = "boxed-trait"))]
91    fn load(&self, keys: &[K]) -> impl Future<Output = Result<HashMap<K, V>, Self::Error>> + Send;
92}
93
94struct DataLoaderInner<T> {
95    requests: Mutex<FnvHashMap<TypeId, Box<dyn Any + Sync + Send>>>,
96    loader: T,
97}
98
99impl<T> DataLoaderInner<T> {
100    #[cfg_attr(feature = "tracing", instrument(skip_all))]
101    async fn do_load<K, V>(&self, disable_cache: bool, (keys, senders): KeysAndSender<K, V, T>)
102    where
103        K: Send + Sync + Hash + Eq + Clone + 'static,
104        V: Send + Sync + Clone + 'static,
105        T: Loader<K, V>,
106    {
107        let tid = TypeId::of::<(K, V)>();
108        let keys = keys.into_iter().collect::<Vec<_>>();
109
110        match self.loader.load(&keys).await {
111            Ok(values) => {
112                // update cache
113                let mut request = self.requests.lock().unwrap();
114                let typed_requests = request
115                    .get_mut(&tid)
116                    .unwrap()
117                    .downcast_mut::<Requests<K, V, T>>()
118                    .unwrap();
119                let disable_cache = typed_requests.disable_cache || disable_cache;
120                if !disable_cache {
121                    for (key, value) in &values {
122                        typed_requests
123                            .cache_storage
124                            .insert(Cow::Borrowed(key), Cow::Borrowed(value));
125                    }
126                }
127
128                // send response
129                for (keys, sender) in senders {
130                    let mut res = HashMap::new();
131                    res.extend(sender.use_cache_values);
132                    for key in &keys {
133                        res.extend(values.get(key).map(|value| (key.clone(), value.clone())));
134                    }
135                    sender.tx.send(Ok(res)).ok();
136                }
137            }
138            Err(err) => {
139                for (_, sender) in senders {
140                    sender.tx.send(Err(err.clone())).ok();
141                }
142            }
143        }
144    }
145}
146
147/// Data loader.
148///
149/// Reference: <https://github.com/facebook/dataloader>
150pub struct DataLoader<T, C = NoCache> {
151    inner: Arc<DataLoaderInner<T>>,
152    cache_factory: C,
153    delay: Duration,
154    max_batch_size: usize,
155    disable_cache: AtomicBool,
156    spawner: Box<dyn Fn(BoxFuture<'static, ()>) + Send + Sync>,
157}
158
159impl<T> DataLoader<T, NoCache> {
160    /// Use `Loader` to create a [`DataLoader`] that does not cache records.
161    pub fn new<S, R>(loader: T, spawner: S) -> Self
162    where
163        S: Fn(BoxFuture<'static, ()>) -> R + Send + Sync + 'static,
164    {
165        Self {
166            inner: Arc::new(DataLoaderInner {
167                requests: Mutex::new(HashMap::default()),
168                loader,
169            }),
170            cache_factory: NoCache,
171            delay: Duration::from_millis(1),
172            max_batch_size: 1000,
173            disable_cache: false.into(),
174            spawner: Box::new(move |fut| {
175                spawner(fut);
176            }),
177        }
178    }
179}
180
181impl<T, C: CacheFactory> DataLoader<T, C> {
182    /// Use `Loader` to create a [`DataLoader`] with a cache factory.
183    pub fn with_cache<S, R>(loader: T, spawner: S, cache_factory: C) -> Self
184    where
185        S: Fn(BoxFuture<'static, ()>) -> R + Send + Sync + 'static,
186    {
187        Self {
188            inner: Arc::new(DataLoaderInner {
189                requests: Mutex::new(HashMap::default()),
190                loader,
191            }),
192            cache_factory,
193            delay: Duration::from_millis(1),
194            max_batch_size: 1000,
195            disable_cache: false.into(),
196            spawner: Box::new(move |fut| {
197                spawner(fut);
198            }),
199        }
200    }
201
202    /// Specify the delay time for loading data, the default is `1ms`.
203    #[must_use]
204    pub fn delay(self, delay: Duration) -> Self {
205        Self { delay, ..self }
206    }
207
208    /// pub fn Specify the max batch size for loading data, the default is
209    /// `1000`.
210    ///
211    /// If the keys waiting to be loaded reach the threshold, they are loaded
212    /// immediately.
213    #[must_use]
214    pub fn max_batch_size(self, max_batch_size: usize) -> Self {
215        Self {
216            max_batch_size,
217            ..self
218        }
219    }
220
221    /// Get the loader.
222    #[inline]
223    pub fn loader(&self) -> &T {
224        &self.inner.loader
225    }
226
227    /// Enable/Disable cache of all loaders.
228    pub fn enable_all_cache(&self, enable: bool) {
229        self.disable_cache.store(!enable, Ordering::SeqCst);
230    }
231
232    /// Enable/Disable cache of specified loader.
233    pub fn enable_cache<K, V>(&self, enable: bool)
234    where
235        K: Send + Sync + Hash + Eq + Clone + 'static,
236        V: Send + Sync + Clone + 'static,
237        T: Loader<K, V>,
238    {
239        let tid = TypeId::of::<(K, V)>();
240        let mut requests = self.inner.requests.lock().unwrap();
241        let typed_requests = requests
242            .get_mut(&tid)
243            .unwrap()
244            .downcast_mut::<Requests<K, V, T>>()
245            .unwrap();
246        typed_requests.disable_cache = !enable;
247    }
248
249    /// Use this `DataLoader` load a data.
250    #[cfg_attr(feature = "tracing", instrument(skip_all))]
251    pub async fn load_one<K, V>(&self, key: K) -> Result<Option<V>, T::Error>
252    where
253        K: Send + Sync + Hash + Eq + Clone + 'static,
254        V: Send + Sync + Clone + 'static,
255        T: Loader<K, V>,
256    {
257        let mut values = self.load_many(std::iter::once(key.clone())).await?;
258        Ok(values.remove(&key))
259    }
260
261    /// Use this `DataLoader` to load some data.
262    #[cfg_attr(feature = "tracing", instrument(skip_all))]
263    pub async fn load_many<K, V, I>(&self, keys: I) -> Result<HashMap<K, V>, T::Error>
264    where
265        K: Send + Sync + Hash + Eq + Clone + 'static,
266        I: IntoIterator<Item = K>,
267        V: Send + Sync + Clone + 'static,
268        T: Loader<K, V>,
269    {
270        enum Action<
271            K: Send + Sync + Hash + Eq + Clone + 'static,
272            V: Send + Sync + Clone + 'static,
273            T: Loader<K, V>,
274        > {
275            ImmediateLoad(KeysAndSender<K, V, T>),
276            StartFetch,
277            Delay,
278        }
279
280        let tid = TypeId::of::<(K, V)>();
281
282        let (action, rx) = {
283            let mut requests = self.inner.requests.lock().unwrap();
284            let typed_requests = requests
285                .entry(tid)
286                .or_insert_with(|| Box::new(Requests::<K, V, T>::new(&self.cache_factory)))
287                .downcast_mut::<Requests<K, V, T>>()
288                .unwrap();
289            let prev_count = typed_requests.keys.len();
290            let mut keys_set = HashSet::new();
291            let mut use_cache_values = HashMap::new();
292
293            if typed_requests.disable_cache || self.disable_cache.load(Ordering::SeqCst) {
294                keys_set = keys.into_iter().collect();
295            } else {
296                for key in keys {
297                    if let Some(value) = typed_requests.cache_storage.get(&key) {
298                        // Already in cache
299                        use_cache_values.insert(key.clone(), value.clone());
300                    } else {
301                        keys_set.insert(key);
302                    }
303                }
304            }
305
306            if !use_cache_values.is_empty() && keys_set.is_empty() {
307                return Ok(use_cache_values);
308            } else if use_cache_values.is_empty() && keys_set.is_empty() {
309                return Ok(HashMap::default());
310            }
311
312            typed_requests.keys.extend(keys_set.clone());
313            let (tx, rx) = oneshot::channel();
314            typed_requests.pending.push((
315                keys_set,
316                ResSender {
317                    use_cache_values,
318                    tx,
319                },
320            ));
321
322            if typed_requests.keys.len() >= self.max_batch_size {
323                (Action::ImmediateLoad(typed_requests.take()), rx)
324            } else {
325                (
326                    if !typed_requests.keys.is_empty() && prev_count == 0 {
327                        Action::StartFetch
328                    } else {
329                        Action::Delay
330                    },
331                    rx,
332                )
333            }
334        };
335
336        match action {
337            Action::ImmediateLoad(keys) => {
338                let inner = self.inner.clone();
339                let disable_cache = self.disable_cache.load(Ordering::SeqCst);
340                let task = async move { inner.do_load(disable_cache, keys).await };
341                #[cfg(feature = "tracing")]
342                let task = task
343                    .instrument(info_span!("immediate_load"))
344                    .in_current_span();
345
346                (self.spawner)(Box::pin(task));
347            }
348            Action::StartFetch => {
349                let inner = self.inner.clone();
350                let disable_cache = self.disable_cache.load(Ordering::SeqCst);
351                let delay = self.delay;
352
353                let task = async move {
354                    Delay::new(delay).await;
355
356                    let keys = {
357                        let mut request = inner.requests.lock().unwrap();
358                        let typed_requests = request
359                            .get_mut(&tid)
360                            .unwrap()
361                            .downcast_mut::<Requests<K, V, T>>()
362                            .unwrap();
363                        typed_requests.take()
364                    };
365
366                    if !keys.0.is_empty() {
367                        inner.do_load(disable_cache, keys).await;
368                    }
369                };
370                #[cfg(feature = "tracing")]
371                let task = task.instrument(info_span!("start_fetch")).in_current_span();
372                (self.spawner)(Box::pin(task));
373            }
374            Action::Delay => {}
375        }
376
377        rx.await.unwrap()
378    }
379
380    /// Feed some data into the cache.
381    ///
382    /// **NOTE: If the cache type is [`NoCache`], this function will not take
383    /// effect. **
384    #[cfg_attr(feature = "tracing", instrument(skip_all))]
385    #[allow(clippy::unused_async)]
386    pub async fn feed_many<K, V, I>(&self, values: I)
387    where
388        K: Send + Sync + Hash + Eq + Clone + 'static,
389        I: IntoIterator<Item = (K, V)>,
390        V: Send + Sync + Clone + 'static,
391        T: Loader<K, V>,
392    {
393        let tid = TypeId::of::<(K, V)>();
394        let mut requests = self.inner.requests.lock().unwrap();
395        let typed_requests = requests
396            .entry(tid)
397            .or_insert_with(|| Box::new(Requests::<K, V, T>::new(&self.cache_factory)))
398            .downcast_mut::<Requests<K, V, T>>()
399            .unwrap();
400        for (key, value) in values {
401            typed_requests
402                .cache_storage
403                .insert(Cow::Owned(key), Cow::Owned(value));
404        }
405    }
406
407    /// Feed some data into the cache.
408    ///
409    /// **NOTE: If the cache type is [`NoCache`], this function will not take
410    /// effect. **
411    #[cfg_attr(feature = "tracing", instrument(skip_all))]
412    pub async fn feed_one<K, V>(&self, key: K, value: V)
413    where
414        K: Send + Sync + Hash + Eq + Clone + 'static,
415        V: Send + Sync + Clone + 'static,
416        T: Loader<K, V>,
417    {
418        self.feed_many(std::iter::once((key, value))).await;
419    }
420
421    /// Clears the cache.
422    ///
423    /// **NOTE: If the cache type is [`NoCache`], this function will not take
424    /// effect. **
425    #[cfg_attr(feature = "tracing", instrument(skip_all))]
426    pub fn clear<K, V>(&self)
427    where
428        K: Send + Sync + Hash + Eq + Clone + 'static,
429        V: Send + Sync + Clone + 'static,
430        T: Loader<K, V>,
431    {
432        let tid = TypeId::of::<(K, V)>();
433        let mut requests = self.inner.requests.lock().unwrap();
434        let typed_requests = requests
435            .entry(tid)
436            .or_insert_with(|| Box::new(Requests::<K, V, T>::new(&self.cache_factory)))
437            .downcast_mut::<Requests<K, V, T>>()
438            .unwrap();
439        typed_requests.cache_storage.clear();
440    }
441
442    /// Gets all values in the cache.
443    pub fn get_cached_values<K, V>(&self) -> HashMap<K, V>
444    where
445        K: Send + Sync + Hash + Eq + Clone + 'static,
446        V: Send + Sync + Clone + 'static,
447        T: Loader<K, V>,
448    {
449        let tid = TypeId::of::<(K, V)>();
450        let requests = self.inner.requests.lock().unwrap();
451        match requests.get(&tid) {
452            None => HashMap::new(),
453            Some(requests) => {
454                let typed_requests = requests.downcast_ref::<Requests<K, V, T>>().unwrap();
455                typed_requests
456                    .cache_storage
457                    .iter()
458                    .map(|(k, v)| (k.clone(), v.clone()))
459                    .collect()
460            }
461        }
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use fnv::FnvBuildHasher;
468    use tokio::join;
469
470    use super::*;
471
472    struct MyLoader;
473
474    #[cfg_attr(feature = "boxed-trait", async_trait::async_trait)]
475    impl Loader<i32, i32> for MyLoader {
476        type Error = ();
477
478        async fn load(&self, keys: &[i32]) -> Result<HashMap<i32, i32>, Self::Error> {
479            assert!(keys.len() <= 10);
480            Ok(keys.iter().copied().map(|k| (k, k)).collect())
481        }
482    }
483
484    #[cfg_attr(feature = "boxed-trait", async_trait::async_trait)]
485    impl Loader<i64, i64> for MyLoader {
486        type Error = ();
487
488        async fn load(&self, keys: &[i64]) -> Result<HashMap<i64, i64>, Self::Error> {
489            assert!(keys.len() <= 10);
490            Ok(keys.iter().copied().map(|k| (k, k)).collect())
491        }
492    }
493
494    #[tokio::test]
495    async fn test_dataloader() {
496        let loader = Arc::new(DataLoader::new(MyLoader, tokio::spawn).max_batch_size(10));
497        assert_eq!(
498            futures_util::future::try_join_all((0..100i32).map({
499                let loader = loader.clone();
500                move |n| {
501                    let loader = loader.clone();
502                    async move { loader.load_one(n).await }
503                }
504            }))
505            .await
506            .unwrap(),
507            (0..100).map(Option::Some).collect::<Vec<_>>()
508        );
509
510        assert_eq!(
511            futures_util::future::try_join_all((0..100i64).map({
512                let loader = loader.clone();
513                move |n| {
514                    let loader = loader.clone();
515                    async move { loader.load_one(n).await }
516                }
517            }))
518            .await
519            .unwrap(),
520            (0..100).map(Option::Some).collect::<Vec<_>>()
521        );
522    }
523
524    #[tokio::test]
525    async fn test_duplicate_keys() {
526        let loader = Arc::new(DataLoader::new(MyLoader, tokio::spawn).max_batch_size(10));
527        assert_eq!(
528            futures_util::future::try_join_all([1, 3, 5, 1, 7, 8, 3, 7].iter().copied().map({
529                let loader = loader.clone();
530                move |n| {
531                    let loader = loader.clone();
532                    async move { loader.load_one(n).await }
533                }
534            }))
535            .await
536            .unwrap(),
537            [1, 3, 5, 1, 7, 8, 3, 7]
538                .iter()
539                .copied()
540                .map(Option::Some)
541                .collect::<Vec<_>>()
542        );
543    }
544
545    #[tokio::test]
546    async fn test_dataloader_load_empty() {
547        let loader = DataLoader::new(MyLoader, tokio::spawn);
548        assert!(
549            loader
550                .load_many::<i32, _, _>(vec![])
551                .await
552                .unwrap()
553                .is_empty()
554        );
555    }
556
557    #[tokio::test]
558    async fn test_dataloader_with_cache() {
559        let loader = DataLoader::with_cache(MyLoader, tokio::spawn, HashMapCache::default());
560        loader.feed_many(vec![(1, 10), (2, 20), (3, 30)]).await;
561
562        // All from the cache
563        assert_eq!(
564            loader.load_many(vec![1, 2, 3]).await.unwrap(),
565            vec![(1, 10), (2, 20), (3, 30)].into_iter().collect()
566        );
567
568        // Part from the cache
569        assert_eq!(
570            loader.load_many(vec![1, 5, 6]).await.unwrap(),
571            vec![(1, 10), (5, 5), (6, 6)].into_iter().collect()
572        );
573
574        // All from the loader
575        assert_eq!(
576            loader.load_many(vec![8, 9, 10]).await.unwrap(),
577            vec![(8, 8), (9, 9), (10, 10)].into_iter().collect()
578        );
579
580        // Clear cache
581        loader.clear::<i32, _>();
582        assert_eq!(
583            loader.load_many(vec![1, 2, 3]).await.unwrap(),
584            vec![(1, 1), (2, 2), (3, 3)].into_iter().collect()
585        );
586    }
587
588    #[tokio::test]
589    async fn test_dataloader_with_cache_hashmap_fnv() {
590        let loader = DataLoader::with_cache(
591            MyLoader,
592            tokio::spawn,
593            HashMapCache::<FnvBuildHasher>::new(),
594        );
595        loader.feed_many(vec![(1, 10), (2, 20), (3, 30)]).await;
596
597        // All from the cache
598        assert_eq!(
599            loader.load_many(vec![1, 2, 3]).await.unwrap(),
600            vec![(1, 10), (2, 20), (3, 30)].into_iter().collect()
601        );
602
603        // Part from the cache
604        assert_eq!(
605            loader.load_many(vec![1, 5, 6]).await.unwrap(),
606            vec![(1, 10), (5, 5), (6, 6)].into_iter().collect()
607        );
608
609        // All from the loader
610        assert_eq!(
611            loader.load_many(vec![8, 9, 10]).await.unwrap(),
612            vec![(8, 8), (9, 9), (10, 10)].into_iter().collect()
613        );
614
615        // Clear cache
616        loader.clear::<i32, _>();
617        assert_eq!(
618            loader.load_many(vec![1, 2, 3]).await.unwrap(),
619            vec![(1, 1), (2, 2), (3, 3)].into_iter().collect()
620        );
621    }
622
623    #[tokio::test]
624    async fn test_dataloader_disable_all_cache() {
625        let loader = DataLoader::with_cache(MyLoader, tokio::spawn, HashMapCache::default());
626        loader.feed_many(vec![(1, 10), (2, 20), (3, 30)]).await;
627
628        // All from the loader
629        loader.enable_all_cache(false);
630        assert_eq!(
631            loader.load_many(vec![1, 2, 3]).await.unwrap(),
632            vec![(1, 1), (2, 2), (3, 3)].into_iter().collect()
633        );
634
635        // All from the cache
636        loader.enable_all_cache(true);
637        assert_eq!(
638            loader.load_many(vec![1, 2, 3]).await.unwrap(),
639            vec![(1, 10), (2, 20), (3, 30)].into_iter().collect()
640        );
641    }
642
643    #[tokio::test]
644    async fn test_dataloader_disable_cache() {
645        let loader = DataLoader::with_cache(MyLoader, tokio::spawn, HashMapCache::default());
646        loader.feed_many(vec![(1, 10), (2, 20), (3, 30)]).await;
647
648        // All from the loader
649        loader.enable_cache::<i32, _>(false);
650        assert_eq!(
651            loader.load_many(vec![1, 2, 3]).await.unwrap(),
652            vec![(1, 1), (2, 2), (3, 3)].into_iter().collect()
653        );
654
655        // All from the cache
656        loader.enable_cache::<i32, _>(true);
657        assert_eq!(
658            loader.load_many(vec![1, 2, 3]).await.unwrap(),
659            vec![(1, 10), (2, 20), (3, 30)].into_iter().collect()
660        );
661    }
662
663    #[tokio::test]
664    async fn test_dataloader_dead_lock() {
665        struct MyDelayLoader;
666
667        #[cfg_attr(feature = "boxed-trait", async_trait::async_trait)]
668        impl Loader<i32, i32> for MyDelayLoader {
669            type Error = ();
670
671            async fn load(&self, keys: &[i32]) -> Result<HashMap<i32, i32>, Self::Error> {
672                tokio::time::sleep(Duration::from_secs(1)).await;
673                Ok(keys.iter().copied().map(|k| (k, k)).collect())
674            }
675        }
676
677        let loader = Arc::new(
678            DataLoader::with_cache(MyDelayLoader, tokio::spawn, NoCache)
679                .delay(Duration::from_secs(1)),
680        );
681        let handle = tokio::spawn({
682            let loader = loader.clone();
683            async move {
684                loader.load_many(vec![1, 2, 3]).await.unwrap();
685            }
686        });
687
688        tokio::time::sleep(Duration::from_millis(500)).await;
689        handle.abort();
690        loader.load_many(vec![4, 5, 6]).await.unwrap();
691    }
692
693    #[tokio::test]
694    async fn test_load_different_keys() {
695        struct MyDelayLoader;
696        #[cfg_attr(feature = "boxed-trait", async_trait::async_trait)]
697        impl Loader<i32, i32> for MyDelayLoader {
698            type Error = ();
699
700            async fn load(&self, keys: &[i32]) -> Result<HashMap<i32, i32>, Self::Error> {
701                tokio::time::sleep(Duration::from_secs(1)).await;
702                Ok(keys.iter().copied().map(|k| (k, k)).collect())
703            }
704        }
705        #[cfg_attr(feature = "boxed-trait", async_trait::async_trait)]
706        impl Loader<i32, u32> for MyDelayLoader {
707            type Error = ();
708
709            async fn load(&self, keys: &[i32]) -> Result<HashMap<i32, u32>, Self::Error> {
710                tokio::time::sleep(Duration::from_secs(1)).await;
711                Ok(keys
712                    .iter()
713                    .copied()
714                    .map(|k| (k, k.try_into().unwrap()))
715                    .collect())
716            }
717        }
718
719        let loader = DataLoader::new(MyDelayLoader, tokio::spawn).delay(Duration::from_secs(1));
720
721        let x = join!(loader.load_one(1), loader.load_one(1));
722        let x1: u32 = x.0.unwrap().unwrap();
723        let x2: i32 = x.1.unwrap().unwrap();
724        assert_eq!(x2, 1i32);
725        assert_eq!(x1, 1u32);
726    }
727}