Skip to main content

ipfrs_storage/
batch.rs

1//! Batch operation utilities for efficient bulk processing
2//!
3//! This module provides utilities for performing batch operations on block stores
4//! with features like parallel processing, error handling, and progress tracking.
5
6use crate::traits::BlockStore;
7use ipfrs_core::{Block, Cid};
8use std::sync::Arc;
9use tokio::sync::Semaphore;
10
11/// Batch operation configuration
12#[derive(Debug, Clone)]
13pub struct BatchConfig {
14    /// Maximum concurrent operations
15    pub max_concurrency: usize,
16    /// Batch size for chunking operations
17    pub batch_size: usize,
18    /// Whether to stop on first error or continue
19    pub fail_fast: bool,
20}
21
22impl Default for BatchConfig {
23    fn default() -> Self {
24        Self {
25            max_concurrency: 10,
26            batch_size: 100,
27            fail_fast: false,
28        }
29    }
30}
31
32impl BatchConfig {
33    /// Create a new batch config with custom settings
34    pub fn new(max_concurrency: usize, batch_size: usize) -> Self {
35        Self {
36            max_concurrency,
37            batch_size,
38            fail_fast: false,
39        }
40    }
41
42    /// Set whether to fail fast on first error
43    pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
44        self.fail_fast = fail_fast;
45        self
46    }
47
48    /// Optimized for high throughput
49    pub fn high_throughput() -> Self {
50        Self {
51            max_concurrency: 50,
52            batch_size: 500,
53            fail_fast: false,
54        }
55    }
56
57    /// Optimized for low latency
58    pub fn low_latency() -> Self {
59        Self {
60            max_concurrency: 20,
61            batch_size: 50,
62            fail_fast: false,
63        }
64    }
65
66    /// Conservative settings for resource-constrained environments
67    pub fn conservative() -> Self {
68        Self {
69            max_concurrency: 5,
70            batch_size: 20,
71            fail_fast: false,
72        }
73    }
74}
75
76/// Result of a batch operation
77#[derive(Debug, Clone)]
78pub struct BatchResult<T> {
79    /// Successfully processed items
80    pub successful: Vec<T>,
81    /// Failed items with their errors
82    pub failed: Vec<(T, String)>,
83    /// Total number of items processed
84    pub total: usize,
85}
86
87impl<T> BatchResult<T> {
88    /// Create a new batch result
89    pub fn new() -> Self {
90        Self {
91            successful: Vec::new(),
92            failed: Vec::new(),
93            total: 0,
94        }
95    }
96
97    /// Check if all operations succeeded
98    pub fn is_success(&self) -> bool {
99        self.failed.is_empty()
100    }
101
102    /// Get success rate (0.0 to 1.0)
103    pub fn success_rate(&self) -> f64 {
104        if self.total == 0 {
105            1.0
106        } else {
107            self.successful.len() as f64 / self.total as f64
108        }
109    }
110
111    /// Get number of successful operations
112    pub fn success_count(&self) -> usize {
113        self.successful.len()
114    }
115
116    /// Get number of failed operations
117    pub fn failure_count(&self) -> usize {
118        self.failed.len()
119    }
120}
121
122impl<T> Default for BatchResult<T> {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128/// Batch put blocks with concurrency control
129///
130/// Puts multiple blocks efficiently with configurable parallelism.
131/// Returns a result indicating success/failure for each block.
132pub async fn batch_put<S: BlockStore + Send + Sync + 'static>(
133    store: Arc<S>,
134    blocks: Vec<Block>,
135    config: BatchConfig,
136) -> BatchResult<Cid> {
137    let mut result = BatchResult::new();
138    result.total = blocks.len();
139
140    let semaphore = Arc::new(Semaphore::new(config.max_concurrency));
141    let mut handles = Vec::new();
142
143    for chunk in blocks.chunks(config.batch_size) {
144        for block in chunk {
145            let permit = semaphore
146                .clone()
147                .acquire_owned()
148                .await
149                .expect("semaphore is never explicitly closed");
150            let block = block.clone();
151            let cid = *block.cid();
152            let store = store.clone();
153
154            let handle = tokio::spawn(async move {
155                let _permit = permit; // Hold permit until task completes
156                (cid, store.put(&block).await)
157            });
158
159            handles.push(handle);
160        }
161
162        // Wait for this chunk to complete
163        for handle in handles.drain(..) {
164            match handle.await {
165                Ok((cid, Ok(_))) => result.successful.push(cid),
166                Ok((cid, Err(e))) => {
167                    result.failed.push((cid, e.to_string()));
168                    if config.fail_fast {
169                        return result;
170                    }
171                }
172                Err(e) => {
173                    // Task panicked or was cancelled
174                    result
175                        .failed
176                        .push((Cid::default(), format!("Task error: {e}")));
177                }
178            }
179        }
180    }
181
182    result
183}
184
185/// Batch get blocks with concurrency control
186///
187/// Retrieves multiple blocks efficiently with configurable parallelism.
188pub async fn batch_get<S: BlockStore + Send + Sync + 'static>(
189    store: Arc<S>,
190    cids: Vec<Cid>,
191    config: BatchConfig,
192) -> BatchResult<Block> {
193    let mut result = BatchResult::new();
194    result.total = cids.len();
195
196    let semaphore = Arc::new(Semaphore::new(config.max_concurrency));
197    let mut handles = Vec::new();
198
199    for chunk in cids.chunks(config.batch_size) {
200        for cid in chunk {
201            let permit = semaphore
202                .clone()
203                .acquire_owned()
204                .await
205                .expect("semaphore is never explicitly closed");
206            let cid = *cid;
207            let store = store.clone();
208
209            let handle = tokio::spawn(async move {
210                let _permit = permit;
211                (cid, store.get(&cid).await)
212            });
213
214            handles.push(handle);
215        }
216
217        // Wait for this chunk to complete
218        for handle in handles.drain(..) {
219            match handle.await {
220                Ok((_cid, Ok(Some(block)))) => result.successful.push(block),
221                Ok((cid, Ok(None))) => {
222                    result.failed.push((
223                        Block::from_parts(cid, bytes::Bytes::new()),
224                        "Block not found".to_string(),
225                    ));
226                }
227                Ok((cid, Err(e))) => {
228                    result
229                        .failed
230                        .push((Block::from_parts(cid, bytes::Bytes::new()), e.to_string()));
231                    if config.fail_fast {
232                        return result;
233                    }
234                }
235                Err(e) => {
236                    result.failed.push((
237                        Block::from_parts(Cid::default(), bytes::Bytes::new()),
238                        format!("Task error: {e}"),
239                    ));
240                }
241            }
242        }
243    }
244
245    result
246}
247
248/// Batch delete blocks with concurrency control
249pub async fn batch_delete<S: BlockStore + Send + Sync + 'static>(
250    store: Arc<S>,
251    cids: Vec<Cid>,
252    config: BatchConfig,
253) -> BatchResult<Cid> {
254    let mut result = BatchResult::new();
255    result.total = cids.len();
256
257    let semaphore = Arc::new(Semaphore::new(config.max_concurrency));
258    let mut handles = Vec::new();
259
260    for chunk in cids.chunks(config.batch_size) {
261        for cid in chunk {
262            let permit = semaphore
263                .clone()
264                .acquire_owned()
265                .await
266                .expect("semaphore is never explicitly closed");
267            let cid = *cid;
268            let store = store.clone();
269
270            let handle = tokio::spawn(async move {
271                let _permit = permit;
272                (cid, store.delete(&cid).await)
273            });
274
275            handles.push(handle);
276        }
277
278        // Wait for this chunk to complete
279        for handle in handles.drain(..) {
280            match handle.await {
281                Ok((cid, Ok(_))) => result.successful.push(cid),
282                Ok((cid, Err(e))) => {
283                    result.failed.push((cid, e.to_string()));
284                    if config.fail_fast {
285                        return result;
286                    }
287                }
288                Err(e) => {
289                    result
290                        .failed
291                        .push((Cid::default(), format!("Task error: {e}")));
292                }
293            }
294        }
295    }
296
297    result
298}
299
300/// Batch check existence with concurrency control
301pub async fn batch_has<S: BlockStore + Send + Sync + 'static>(
302    store: Arc<S>,
303    cids: Vec<Cid>,
304    config: BatchConfig,
305) -> BatchResult<(Cid, bool)> {
306    let mut result = BatchResult::new();
307    result.total = cids.len();
308
309    let semaphore = Arc::new(Semaphore::new(config.max_concurrency));
310    let mut handles = Vec::new();
311
312    for chunk in cids.chunks(config.batch_size) {
313        for cid in chunk {
314            let permit = semaphore
315                .clone()
316                .acquire_owned()
317                .await
318                .expect("semaphore is never explicitly closed");
319            let cid = *cid;
320            let store = store.clone();
321
322            let handle = tokio::spawn(async move {
323                let _permit = permit;
324                (cid, store.has(&cid).await)
325            });
326
327            handles.push(handle);
328        }
329
330        // Wait for this chunk to complete
331        for handle in handles.drain(..) {
332            match handle.await {
333                Ok((cid, Ok(exists))) => result.successful.push((cid, exists)),
334                Ok((cid, Err(e))) => {
335                    result.failed.push(((cid, false), e.to_string()));
336                    if config.fail_fast {
337                        return result;
338                    }
339                }
340                Err(e) => {
341                    result
342                        .failed
343                        .push(((Cid::default(), false), format!("Task error: {e}")));
344                }
345            }
346        }
347    }
348
349    result
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::MemoryBlockStore;
356    use bytes::Bytes;
357
358    #[tokio::test]
359    async fn test_batch_put() {
360        let store = Arc::new(MemoryBlockStore::new());
361        let mut blocks = Vec::new();
362
363        for i in 0..10 {
364            let data = format!("block {}", i);
365            let block = Block::new(Bytes::from(data)).unwrap();
366            blocks.push(block);
367        }
368
369        let config = BatchConfig::default();
370        let result = batch_put(store.clone(), blocks.clone(), config).await;
371
372        assert!(result.is_success());
373        assert_eq!(result.success_count(), 10);
374        assert_eq!(result.failure_count(), 0);
375        assert_eq!(result.success_rate(), 1.0);
376    }
377
378    #[tokio::test]
379    async fn test_batch_get() {
380        let store = Arc::new(MemoryBlockStore::new());
381        let mut blocks = Vec::new();
382        let mut cids = Vec::new();
383
384        for i in 0..5 {
385            let data = format!("block {}", i);
386            let block = Block::new(Bytes::from(data)).unwrap();
387            cids.push(*block.cid());
388            store.put(&block).await.unwrap();
389            blocks.push(block);
390        }
391
392        let config = BatchConfig::default();
393        let result = batch_get(store.clone(), cids, config).await;
394
395        assert!(result.is_success());
396        assert_eq!(result.success_count(), 5);
397    }
398
399    #[tokio::test]
400    async fn test_batch_has() {
401        let store = Arc::new(MemoryBlockStore::new());
402        let mut cids = Vec::new();
403
404        for i in 0..5 {
405            let data = format!("block {}", i);
406            let block = Block::new(Bytes::from(data)).unwrap();
407            cids.push(*block.cid());
408            store.put(&block).await.unwrap();
409        }
410
411        let config = BatchConfig::default();
412        let result = batch_has(store.clone(), cids, config).await;
413
414        assert!(result.is_success());
415        assert_eq!(result.success_count(), 5);
416
417        // All blocks should exist
418        for (_, exists) in result.successful {
419            assert!(exists);
420        }
421    }
422
423    #[tokio::test]
424    async fn test_batch_delete() {
425        let store = Arc::new(MemoryBlockStore::new());
426        let mut cids = Vec::new();
427
428        for i in 0..5 {
429            let data = format!("block {}", i);
430            let block = Block::new(Bytes::from(data)).unwrap();
431            cids.push(*block.cid());
432            store.put(&block).await.unwrap();
433        }
434
435        let config = BatchConfig::default();
436        let result = batch_delete(store.clone(), cids.clone(), config).await;
437
438        assert!(result.is_success());
439        assert_eq!(result.success_count(), 5);
440
441        // Verify blocks are deleted
442        for cid in cids {
443            assert!(!store.has(&cid).await.unwrap());
444        }
445    }
446
447    #[test]
448    fn test_batch_config_presets() {
449        let high_throughput = BatchConfig::high_throughput();
450        assert_eq!(high_throughput.max_concurrency, 50);
451        assert_eq!(high_throughput.batch_size, 500);
452
453        let low_latency = BatchConfig::low_latency();
454        assert_eq!(low_latency.max_concurrency, 20);
455        assert_eq!(low_latency.batch_size, 50);
456
457        let conservative = BatchConfig::conservative();
458        assert_eq!(conservative.max_concurrency, 5);
459        assert_eq!(conservative.batch_size, 20);
460    }
461
462    #[test]
463    fn test_batch_result() {
464        let mut result = BatchResult::<i32>::new();
465        result.total = 10;
466        result.successful = vec![1, 2, 3, 4, 5];
467        result.failed = vec![(6, "error".to_string())];
468
469        assert!(!result.is_success());
470        assert_eq!(result.success_count(), 5);
471        assert_eq!(result.failure_count(), 1);
472        assert_eq!(result.success_rate(), 0.5);
473    }
474}