armature_cache/parallel.rs
1//! Parallel batch operations for cache stores.
2
3use crate::error::{CacheError, CacheResult};
4use crate::traits::CacheStore;
5use futures::future::{join_all, try_join_all};
6use serde::{Serialize, de::DeserializeOwned};
7use std::collections::HashMap;
8use std::time::Duration;
9
10/// Parallel batch operations for cache stores.
11///
12/// This module provides high-performance batch operations that execute
13/// multiple cache operations concurrently, significantly reducing total latency.
14///
15/// # Performance
16///
17/// - **get_many**: 10-100x faster than sequential gets (depending on network latency)
18/// - **set_many**: 10-100x faster than sequential sets
19/// - **delete_many**: Similar performance gains
20///
21/// # Examples
22///
23/// ```no_run
24/// use armature_cache::*;
25/// use armature_cache::parallel::*;
26///
27/// # async fn example() -> CacheResult<()> {
28/// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
29///
30/// // Get multiple keys in parallel
31/// let keys = vec!["user:1", "user:2", "user:3"];
32/// let values = get_many_json(&cache, &keys).await?;
33///
34/// // Set multiple keys in parallel
35/// let items = vec![
36/// ("key1", "value1".to_string()),
37/// ("key2", "value2".to_string()),
38/// ];
39/// set_many_json(&cache, &items, None).await?;
40/// # Ok(())
41/// # }
42/// ```
43pub struct ParallelCacheOps;
44
45impl ParallelCacheOps {
46 /// Get multiple JSON values in parallel.
47 ///
48 /// # Arguments
49 ///
50 /// * `store` - The cache store
51 /// * `keys` - Slice of keys to fetch
52 ///
53 /// # Returns
54 ///
55 /// A vector of optional values in the same order as keys.
56 ///
57 /// # Examples
58 ///
59 /// ```no_run
60 /// use armature_cache::*;
61 /// use armature_cache::parallel::ParallelCacheOps;
62 ///
63 /// # async fn example() -> CacheResult<()> {
64 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
65 ///
66 /// let keys = vec!["key1", "key2", "key3"];
67 /// let values = ParallelCacheOps::get_many_json(&cache, &keys).await?;
68 ///
69 /// for (key, value) in keys.iter().zip(values.iter()) {
70 /// println!("{}: {:?}", key, value);
71 /// }
72 /// # Ok(())
73 /// # }
74 /// ```
75 pub async fn get_many_json<S: CacheStore>(
76 store: &S,
77 keys: &[&str],
78 ) -> CacheResult<Vec<Option<String>>> {
79 // Delegate to the store's native batch primitive. Backends like Redis
80 // collapse this into a single `MGET` round-trip; others fall back to the
81 // concurrent per-key loop. Order matches `keys` in both cases.
82 store.mget(keys).await
83 }
84
85 /// Get multiple typed values in parallel.
86 ///
87 /// # Type Parameters
88 ///
89 /// * `T` - The type to deserialize into
90 ///
91 /// # Examples
92 ///
93 /// ```no_run
94 /// use armature_cache::*;
95 /// use armature_cache::parallel::ParallelCacheOps;
96 /// use serde::{Deserialize, Serialize};
97 ///
98 /// #[derive(Serialize, Deserialize)]
99 /// struct User {
100 /// id: u64,
101 /// name: String,
102 /// }
103 ///
104 /// # async fn example() -> CacheResult<()> {
105 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
106 ///
107 /// let keys = vec!["user:1", "user:2", "user:3"];
108 /// let users: Vec<Option<User>> = ParallelCacheOps::get_many(&cache, &keys).await?;
109 /// # Ok(())
110 /// # }
111 /// ```
112 pub async fn get_many<S: CacheStore, T: DeserializeOwned>(
113 store: &S,
114 keys: &[&str],
115 ) -> CacheResult<Vec<Option<T>>> {
116 let json_values = Self::get_many_json(store, keys).await?;
117
118 json_values
119 .into_iter()
120 .map(|opt_json| {
121 opt_json
122 .map(|json| {
123 serde_json::from_str(&json)
124 .map_err(|e| CacheError::Deserialization(e.to_string()))
125 })
126 .transpose()
127 })
128 .collect()
129 }
130
131 /// Set multiple JSON values in parallel.
132 ///
133 /// # Arguments
134 ///
135 /// * `store` - The cache store
136 /// * `items` - Slice of (key, value) tuples
137 /// * `ttl` - Optional time-to-live for all items
138 ///
139 /// # Examples
140 ///
141 /// ```no_run
142 /// use armature_cache::*;
143 /// use armature_cache::parallel::ParallelCacheOps;
144 /// use std::time::Duration;
145 ///
146 /// # async fn example() -> CacheResult<()> {
147 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
148 ///
149 /// let items = vec![
150 /// ("key1", r#"{"value": 1}"#.to_string()),
151 /// ("key2", r#"{"value": 2}"#.to_string()),
152 /// ];
153 ///
154 /// ParallelCacheOps::set_many_json(&cache, &items, Some(Duration::from_secs(3600))).await?;
155 /// # Ok(())
156 /// # }
157 /// ```
158 pub async fn set_many_json<S: CacheStore>(
159 store: &S,
160 items: &[(&str, String)],
161 ttl: Option<Duration>,
162 ) -> CacheResult<()> {
163 // Delegate to the store's native batch primitive (e.g. Redis `MSET` /
164 // pipelined `SET ... EX`), falling back to the per-key loop otherwise.
165 store.mset(items, ttl).await
166 }
167
168 /// Set multiple typed values in parallel.
169 ///
170 /// # Type Parameters
171 ///
172 /// * `T` - The type to serialize from
173 ///
174 /// # Examples
175 ///
176 /// ```no_run
177 /// use armature_cache::*;
178 /// use armature_cache::parallel::ParallelCacheOps;
179 /// use serde::{Deserialize, Serialize};
180 ///
181 /// #[derive(Serialize, Deserialize)]
182 /// struct Counter {
183 /// count: u64,
184 /// }
185 ///
186 /// # async fn example() -> CacheResult<()> {
187 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
188 ///
189 /// let items = vec![
190 /// ("counter:1", Counter { count: 10 }),
191 /// ("counter:2", Counter { count: 20 }),
192 /// ];
193 ///
194 /// ParallelCacheOps::set_many(&cache, &items, None).await?;
195 /// # Ok(())
196 /// # }
197 /// ```
198 pub async fn set_many<S: CacheStore, T: Serialize>(
199 store: &S,
200 items: &[(&str, T)],
201 ttl: Option<Duration>,
202 ) -> CacheResult<()> {
203 let json_items: Result<Vec<_>, _> = items
204 .iter()
205 .map(|(key, value)| {
206 serde_json::to_string(value)
207 .map(|json| (*key, json))
208 .map_err(|e| CacheError::Serialization(e.to_string()))
209 })
210 .collect();
211
212 let json_items = json_items?;
213 let item_refs: Vec<_> = json_items.iter().map(|(k, v)| (*k, v.clone())).collect();
214
215 Self::set_many_json(store, &item_refs, ttl).await
216 }
217
218 /// Delete multiple keys in parallel.
219 ///
220 /// # Examples
221 ///
222 /// ```no_run
223 /// use armature_cache::*;
224 /// use armature_cache::parallel::ParallelCacheOps;
225 ///
226 /// # async fn example() -> CacheResult<()> {
227 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
228 ///
229 /// let keys = vec!["key1", "key2", "key3"];
230 /// ParallelCacheOps::delete_many(&cache, &keys).await?;
231 /// # Ok(())
232 /// # }
233 /// ```
234 pub async fn delete_many<S: CacheStore>(store: &S, keys: &[&str]) -> CacheResult<()> {
235 // Delegate to the store's native batch primitive (e.g. Redis variadic
236 // `DEL`), falling back to the concurrent per-key loop otherwise.
237 store.mdel(keys).await
238 }
239
240 /// Check if multiple keys exist in parallel.
241 ///
242 /// # Returns
243 ///
244 /// A vector of booleans indicating existence, in the same order as keys.
245 ///
246 /// # Examples
247 ///
248 /// ```no_run
249 /// use armature_cache::*;
250 /// use armature_cache::parallel::ParallelCacheOps;
251 ///
252 /// # async fn example() -> CacheResult<()> {
253 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
254 ///
255 /// let keys = vec!["key1", "key2", "key3"];
256 /// let exists = ParallelCacheOps::exists_many(&cache, &keys).await?;
257 ///
258 /// for (key, exists) in keys.iter().zip(exists.iter()) {
259 /// println!("{}: {}", key, exists);
260 /// }
261 /// # Ok(())
262 /// # }
263 /// ```
264 pub async fn exists_many<S: CacheStore>(store: &S, keys: &[&str]) -> CacheResult<Vec<bool>> {
265 let futures = keys.iter().map(|key| store.exists(key));
266 let results: Vec<CacheResult<bool>> = join_all(futures).await;
267
268 results.into_iter().collect()
269 }
270
271 /// Get TTL for multiple keys in parallel.
272 ///
273 /// # Returns
274 ///
275 /// A vector of optional durations, in the same order as keys.
276 ///
277 /// # Examples
278 ///
279 /// ```no_run
280 /// use armature_cache::*;
281 /// use armature_cache::parallel::ParallelCacheOps;
282 ///
283 /// # async fn example() -> CacheResult<()> {
284 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
285 ///
286 /// let keys = vec!["key1", "key2", "key3"];
287 /// let ttls = ParallelCacheOps::ttl_many(&cache, &keys).await?;
288 ///
289 /// for (key, ttl) in keys.iter().zip(ttls.iter()) {
290 /// println!("{}: {:?}", key, ttl);
291 /// }
292 /// # Ok(())
293 /// # }
294 /// ```
295 pub async fn ttl_many<S: CacheStore>(
296 store: &S,
297 keys: &[&str],
298 ) -> CacheResult<Vec<Option<Duration>>> {
299 let futures = keys.iter().map(|key| store.ttl(key));
300 let results: Vec<CacheResult<Option<Duration>>> = join_all(futures).await;
301
302 results.into_iter().collect()
303 }
304
305 /// Cache warming: preload multiple keys into cache.
306 ///
307 /// # Type Parameters
308 ///
309 /// * `T` - The type to serialize
310 /// * `F` - Factory function that returns data for a given key
311 ///
312 /// # Examples
313 ///
314 /// ```ignore
315 /// use armature_cache::*;
316 /// use armature_cache::parallel::ParallelCacheOps;
317 /// use std::time::Duration;
318 ///
319 /// # async fn example() -> CacheResult<()> {
320 /// let cache = RedisCache::new(CacheConfig::redis("redis://localhost:6379")?).await?;
321 ///
322 /// let keys = vec!["user:1", "user:2", "user:3"];
323 ///
324 /// ParallelCacheOps::warm_cache(
325 /// &cache,
326 /// &keys,
327 /// Some(Duration::from_secs(3600)),
328 /// |key: &str| async move {
329 /// // Fetch from database
330 /// let data = format!("Data for {}", key);
331 /// Ok::<String, CacheError>(data)
332 /// },
333 /// ).await?;
334 /// # Ok(())
335 /// # }
336 /// ```
337 pub async fn warm_cache<S, T, F, Fut>(
338 store: &S,
339 keys: &[&str],
340 ttl: Option<Duration>,
341 factory: F,
342 ) -> CacheResult<()>
343 where
344 S: CacheStore,
345 T: Serialize,
346 F: Fn(&str) -> Fut,
347 Fut: std::future::Future<Output = CacheResult<T>>,
348 {
349 let mut futures = Vec::new();
350
351 for key in keys {
352 let fut = async {
353 let value = factory(key).await?;
354 let json = serde_json::to_string(&value)
355 .map_err(|e| CacheError::Serialization(e.to_string()))?;
356 store.set_json(key, json, ttl).await?;
357 Ok::<(), CacheError>(())
358 };
359 futures.push(fut);
360 }
361
362 try_join_all(futures).await?;
363 Ok(())
364 }
365}
366
367/// Helper functions for parallel cache operations.
368///
369/// These functions provide a more convenient API than `ParallelCacheOps` methods.
370/// Get multiple JSON values in parallel.
371pub async fn get_many_json<S: CacheStore>(
372 store: &S,
373 keys: &[&str],
374) -> CacheResult<Vec<Option<String>>> {
375 ParallelCacheOps::get_many_json(store, keys).await
376}
377
378/// Get multiple typed values in parallel.
379pub async fn get_many<S: CacheStore, T: DeserializeOwned>(
380 store: &S,
381 keys: &[&str],
382) -> CacheResult<Vec<Option<T>>> {
383 ParallelCacheOps::get_many(store, keys).await
384}
385
386/// Set multiple JSON values in parallel.
387pub async fn set_many_json<S: CacheStore>(
388 store: &S,
389 items: &[(&str, String)],
390 ttl: Option<Duration>,
391) -> CacheResult<()> {
392 ParallelCacheOps::set_many_json(store, items, ttl).await
393}
394
395/// Set multiple typed values in parallel.
396pub async fn set_many<S: CacheStore, T: Serialize>(
397 store: &S,
398 items: &[(&str, T)],
399 ttl: Option<Duration>,
400) -> CacheResult<()> {
401 ParallelCacheOps::set_many(store, items, ttl).await
402}
403
404/// Delete multiple keys in parallel.
405pub async fn delete_many<S: CacheStore>(store: &S, keys: &[&str]) -> CacheResult<()> {
406 ParallelCacheOps::delete_many(store, keys).await
407}
408
409/// Build a HashMap from multiple keys fetched in parallel.
410pub async fn get_many_as_map<S: CacheStore, T: DeserializeOwned>(
411 store: &S,
412 keys: &[&str],
413) -> CacheResult<HashMap<String, T>> {
414 let values = get_many(store, keys).await?;
415
416 let map: HashMap<String, T> = keys
417 .iter()
418 .zip(values)
419 .filter_map(|(key, opt_value)| opt_value.map(|value| (key.to_string(), value)))
420 .collect();
421
422 Ok(map)
423}
424
425#[cfg(test)]
426mod tests {
427 #[test]
428 fn test_parallel_ops_exist() {
429 // Ensure the module compiles - this test validates the module is correctly defined
430 }
431}